[macOS] NVMe Enclosure 속도 비교

측정 환경

– Device: Macbook Pro M5, CPU 10C/GPU 10C, 32GB RAM
– SSD: WD SN740 M.2 2230 NVMe (2TB)

[1] Sharge Disk

  • USB 3.2 Gen2 (10Gbps) Enclosure
  • Chipset: Realtek RTL9210

[2] OWC Express 1M2

  • Thunderbolt 4 (40Gbps) Enclosure
  • Chipset: ASMedia ASM2464PD

결과 비교

※ 높을 수록 좋음

[WordPress] Cloudflare + Kubernetes 환경에서 Real IP 얻기

Preface

최근들어 부쩍 스팸성 댓글이 활발한것 같다. WordPress에서 글이나 댓글이 작성되면, 스팸 필터링 등을 위해 작성자 IP등의 정보가 기록된다. 스팸으로 의심되는 IP나 댓글 본문은 자동으로 스팸 필터링이 적용되는데, 이 때 구축 환경 때문에 실제 IP (Real IP)가 기록되지 않는 문제가 종종 있는것 같다.

댓글이 작성되면 실제 IP가 아닌 내부 IP가 기록된다.

지금 구축된 환경에서는 사실 실제 IP(Real IP)가 직접 전달되기는 어렵다. 이를 해결하려면 끝점에 있는 WordPress에서 HTTP Header를 파싱하여 실제 IP를 가져올 필요가 있다.

Experiment #1. Cloudflare without Proxy

Cloudflare Proxy를 끄고 HTTP Request와 Header가 변형되는 과정. 물론 환경마다 다를수 있다.
직접 testbed.dailylime.dev로 테스트해볼 수 있다.

Experiment #2. Cloudflare with Proxy

Cloudflare Proxy를 켠 상태로 HTTP Request와 Header가 변형되는 과정. 물론 환경마다 다를수 있다.
직접 testbed.dailylime.dev로 테스트해볼 수 있다.

Forwarded Headers

이를 해결하기 위해서는 Header로 포워딩된 Original IP를 파싱할 필요가 있다. Cloudflare는 Origin IP를 CF-Connecting-IPX-Original-Forwarded-For 두 가지로 Forward한다. 여기서는 CF-Connecting-IP를 가지고 Origin IP를 파싱하는 방법을 사용한다.

WordPress에서의 세팅 방법은 간단하다. wp-config.php에 아래와 같이 세팅한다.

/**
 * Fixing original IP detection behind Nginx proxy
 */
if(isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
    $list = explode(',',$_SERVER['HTTP_CF_CONNECTING_IP']);
    $_SERVER['REMOTE_ADDR'] = $list[0];
}

Conclusion

결국 wp-config.php에 몇 줄만 추가되면 되는 내용인데. 사실상 정확히 알고자 하면 복잡한 부분이다. 현재 구축되어 있는 Reverse proxy에 대한 구조에 대한 이해 등이 일부 필요한 부분이 있다. 실제로 Reference [1]에서는 X-Forwarded-For를 사용하는 방법을 명시하고 있는데, Kubernetes Ingress 뒤에 있는 경우 이 값도 사실상 Internal IP이거나 Private IP일 확률이 높다. 그렇기 때문에, 정확하게 어떤 Header가 전달되는지를 확인할 필요가 있다.

아래 jungin500/http-request-tester 레포지토리는 필요한 경우 테스트 목적으로 활용 가능하다.

  • jungin500/http-request-tester:python
  • jungin500/http-request-tester:cplusplus

처음에는 Python 구현체로 만들고, 다시 C++로 변환하였다. Claude 만만세

Reference

[1] Technicus, Configure WordPress to report true IP addresses when behind a reverse proxy: https://techblog.jeppson.org/2014/09/configure-wordpress-to-report-true-ip-addresses-when-behind-a-reverse-proxy/

[2] Cloudflare Docs, Restoring original visitor IPs: https://developers.cloudflare.com/support/troubleshooting/restoring-visitor-ips/restoring-original-visitor-ips/#nginx-1

[3] Cloudflare, IP Ranges: https://www.cloudflare.com/ko-kr/ips/

[VSCode] Markdown PDF LaTex 포함 변환하기 (+M1 속도 향상)

VSCode에는 Markdown을 PDF로 export할 수 있는 유용한 plugin(링크)이 있다. 해당 플러그인을 활용해서 LaTeX 수식이 있는 Markdown 파일을 PDF로 export해보자. 마지막 부분에는 Apple Silicon(M1) macOS에서 발생하는 timeout 이슈를 해결할 수 있는 별도의 chromium 다운로드 과정 또한 설명한다.

목차

  1. “Markdown PDF” VSCode plugin 설치
  2. Markdown 파일에 MathJax script 추가
  3. (macOS Apple silicon/Windows ARM64) Chromium 다운로드 및 경로 설정

1. “Markdown PDF” VSCode plugin 설치

VSCode에 Markdown PDF 라이브러리를 설치한다: 설치 바로가기 링크

사용법 보기 (Overview 페이지 참고)

2. Markdown 파일에 MathJax script 추가

이제 위 사용법에 따라 간단하게 PDF를 export할 수 있다. 하지만 LaTeX가 그대로 출력되는 문제가 있는데, 이렇게 되면 완성된 PDF에서 수식 TeX 코드가 렌더링되지 않고 소스코드가 그대로 보이게 된다.

PDF에서 이런 식으로 코드가 보인다.

물론 yzane/vscode-markdown-pdf#21에서 해당 문제에 대한 해결 방법을 이미 다루고 있다. 하지만 여기에 더해 자연스러운 TeX 폰트까지 적용해 보려 한다.

사용 방법은, 각 Markdown 파일의 맨 아래 끝에 아래 script를 추가하고 PDF export를 진행하면 된다.


<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    tex2jax: {inlineMath: [['$', '$']]},
    messageStyle: "none",
    "HTML-CSS": { availableFonts: "TeX", preferredFont: "TeX" },
  });
</script>

위 HTML tag를 추가한 뒤에 export를 진행하면, 아래와 같이 깔끔하게 TeX가 렌더링된 것을 확인할 수 있다!

렌더링이 잘 된 결과 PDF의 일부.

3. (ARM64) Chromium 다운로드 및 경로 설정

macOS (M1, Apple Silicon) 또는 ARM64 Windows에서는 별도로 해당 아키텍쳐 전용 Chromium을 다운로드해 주는 것이 속도 향상에 큰 영향을 준다. 특히나 macOS에서 PDF Export에 2~3배 이상의 체감 속도 향상이 있었으니 반드시 설정해주도록 하자. 여기서는 macOS Apple Silicon 기준으로 설명한다.

그런데 왜 Chromium일까, 잘 보니 내부적으로 Markdown을 chromium으로 렌더링한 후 이를 PDF로 출력하는듯 하다. 결과물이 깔끔하니 잘 활용한 듯 하다.

먼저 Chromium release page(링크)에 접속하여 하단 릴리즈들 중 “Chromium for Mac ARM”을 선택하고 다운로드를 진행한다. 압축을 해제한 후 /Applications 폴더로 Chromium.app 어플리케이션을 이동하여 설치하도록 하자.

Chromium 손상된 어플리케이션 등의 오류 발생 시 해결방법

macOS Ventura에서는 해당 문제가 확정적으로 발생하였다. macOS에서 실행하는 binary는 모두 signature가 필요한데, 여기서 다운로드한 snapshot 빌드에는 사인이 되어있지 않는듯 하다. 아래 명령어를 입력한 뒤 실행하면 문제없이 실행할 수 있다.

sudo xattr -c /Applications/Chromium.app
sudo xattr -c /Applications/Chromium.app/Contents/MacOS/Chromium

위 명령어는 해당 바이너리의 출처 정보(Attribute)를 삭제한다. 일반적으로는 문제가 없겠지만 보안 이슈가 있을 수 있다는 것은 알아두어야 할 듯 하다.

설치와 실행이 문제없이 된다면, 이제 VSCode의 설정(커맨트 팔레트 Cmd+Shift+P 등으로 “사용자 설정 (User Settings)”에서 설정을 변경해주자. 상단 검색창에 Markdown PDF를 검색하면 친절하게(?) 관련 설정이 필터링된다. 우리가 관심있는 설정은 “Markdown-pdf: Executable Path“이다. 해당 설정을 아래와 같이 변경해주도록 하자.

기본적으로 extension이 다운로드하는 Chromium(Intel)을 사용하지 않겠다는 의미이기도 하다.
/Applications/Chromium.app/Contents/MacOS/Chromium

위 값을 입력해주었다면, 자동으로 저장되므로 탭을 닫고 원하는 Markdown을 PDF로 export 해보도록 하자.

[ML] Cross Entropy Loss는 Negative Class를 고려할까?

일반적으로 사용되는 아래 CE Loss의 구현을 Softmax를 빼고 보면 Loss Term에 Negative Class에 대한 고려가 들어가있지 않다. (마지막 정리부에서 다루지만 사실 CE Loss와 Softmax를 분리해서 보는 생각이 잘못된 것 같다.) 이 의문에서 시작해서 답을 찾아갔던 과정을 정리해보았다.

아래 수식에서 $p(x_{i})$는 정답값 분포 [0.0, 0.0, 1.0, 0.0, 0.0]를, $\hat{p}(x_{i})$는 Softmax를 거쳐나온 각 클래스별 확률 분포 $0<\hat{p}(x_{i})<1$를 의미한다.

$$-\sum_{i=1}^{C}{\{p(x_{i}){\cdot}log(\hat{p}(x_{i}))\}}$$

import numpy as np

# log(x) where 0 <= x < 1
# will return NaN or Inf if x == 0!
# We then add epsilon to x term.
eps = 1e-9

# Pseudo-outputs of softmax: softmax(logits)
# We can also calculate `logits` by applying
# inverse of the softmax: np.log(outputs)!
outputs = np.array([0.03, 0.05, 0.8, 0.1, 0.02])
label   = np.array([0.0,  0.0,  1.0, 0.0, 0.0 ])

# CE Loss calculation
ce_loss = -np.sum(label * np.log(outputs + eps))

# > ce_loss: 0.22314355006420974

CE Loss의 구현부를 보면 -np.sum(label * np.log(outputs + eps))으로 구성되는데, Negative Class의 Term은 label과 곱해지면서 상쇄되어 최종 Loss에 기여하지 않는다. 그러면 Softmax가 일반적으로 객체 분류문제에서 잘 작동하는 이유는 무엇일까? 내가 내린 결론은 Backpropagation에 있었다.

먼저, Stochastic Gradient Descent(SGD) 기법에서는 모델의 학습을 위한 방법으로 모델의 입력값에 대한 모델 가중치의 미분값Gradient을 구한다. 이를 더 쉽게 구하기 위하여 Chain Rule을 이용하며, 모델의 출력부부터 시작하여 모델의 입력부에 이르기까지 각 레이어별로 미분값을 구한다.

CE Loss $\mathcal{L}(s, y)$[1]만 고려했을 때, 이 Loss 함수를 입력값 $z$[2]에 미분하게 되면 다음과 같다. 상세한 미분 과정은 towardsdatascience 블로그(링크)에서 자세히 설명해주셨다.

$${{\partial{\mathcal{L}}}\over{\partial{\pmb{z}}}}=\pmb{s}-\pmb{y}$$

결국 Softmax 출력값과 레이블 값의 차이가 미분값이었던 것이다. 이렇게 함으로써 역전파Backpropagation 과정에서는 정답값이 아닌 클래스(0, 1, 3, 4번 클래스)의 확률도 잘 반영되어 학습이 진행되는 것이다.

Q. 그러면 CE Loss 값에는 Negative Class 값이 반영되지 않는가?

CE Loss는 Softmax 함수를 거친 뒤에 사용한다. 이를 중점으로 생각해 보면, Negative Class(0, 1, 3, 4번 Class)의 Logitsbefore softmax 값이 Positive Class(2번 Class)의 Probabilityafter softmax에 영향을 준다. 예를 들어, 아래 예시에서는 Negative Class의 영향력이 커질수록 Positive Class의 Probability는 작아진다.

import numpy as np

eps = 1e-9

# Define softmax (not used earlier)
def softmax(inputs: np.ndarray) -> np.ndarray:
    exps = np.exp(inputs)
    sums = np.sum(exps, axis=-1, keepdims=True)
    return exps / sums

# Pseudo-outputs of softmax: softmax(logits)
# We can also calculate `logits` by applying
# inverse of the softmax: np.log(outputs)!
outputs = np.array([0.03, 0.05, 0.8, 0.1, 0.02])
label   = np.array([0.0,  0.0,  1.0, 0.0, 0.0 ])

# Add arbitary value to logits and re-apply softmax
logits  = np.log(outputs)
logits[0:2] += 0.4
logits[3:5] += 0.3
outputs_m = softmax(logits)

# Show differences between `outputs` and `outputs_m`
print(outputs_m)
print(outputs_m - outputs)
# > outputs_m:
#   [0.04138864 0.06898107 0.73983032 0.12483331 0.02496666]
# > outputs_m - outputs:
#   [0.01138864  0.01898107 -0.06016968  0.02483331  0.00496666]

# CE Loss calculation
ce_loss = -np.sum(label * np.log(outputs + eps))
# > ce_loss: 0.22314355006420974

ce_loss_m = -np.sum(label * np.log(outputs_m + eps))
# > ce_loss_m: 0.30133442040075237

결국 CE Loss의 정의에서 input feature로는 probabilityoutput of softmax가 아니라, logitsbefore softmax를 중점으로 보았어야 했던 것이었다.

Conclusion

생각을 정리하다 보니, CE Loss와 Softmax를 한 묶음으로 생각하니까 모든 의문이 해결되었다. 처음에는 CE Loss term만 따로 두고 봐서 생겼던 의문이었지만, 결국 CE Loss의 정의에서 input feature로는 probabilityoutput of softmax가 아니라, logitsbefore softmax를 중점으로 보았어야 했던 것이었다.

본래 이 의문이 생겼던 것은, CE loss의 input probability를 변경하다가, 다른 값들을 넣어도 같은 loss value가 나왔던 것이 계기였다. 명백하게 확률의 분포가 다른데도 불구하고 loss 값이 동일하게 나와서 이에 의문이 들었었다.

outputs_1 = [0.05, 0.03, 0.8, 0.02, 0.1]
outputs_1 = [0.0, 0.0, 0.8, 0.2, 0.0]

다시금 생각해보면 CE loss를 계산할 때 이러한 분포의 차이는 중요하지 않았다. label이 두 개 이상이라면 다르게 판단해야 할지도 모르겠지만, 하나인 이상 다른 클래스의 확률이 얼마나 큰 지는 중요하지 않은 것이다.


  • [1] $s$: outputs of softmax, $y$: one hot vector of label
  • [2] $z$: input logits of softmax

  • References

    1. https://stackoverflow.com/questions/47377222/what-is-the-problem-with-my-implementation-of-the-cross-entropy-function
    2. https://towardsdatascience.com/derivative-of-the-softmax-function-and-the-categorical-cross-entropy-loss-ffceefc081d1

    [Object Detection] COCOEval에서 -1.000이 뜰 때 해결법

    Problem: Precision이나 Recall이 -1.000 (잘못된 값)으로 나타남

    Precision이나 Recall이 -1일 때는 areaRng의 small, medium, large가 데이터셋 분포에 맞게 잘 설정되었는지 확인해보자.

    Average forward time: 0.54 ms, Average NMS time: 0.43 ms, Average inference time: 0.97 ms
     Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.287
     Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.443
     Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.326
     Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
     Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
     Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.287
     Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.483
     Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.535
     Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.535
     Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
     Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
     Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.535

    잘 보면 area=small과 area=medium에서만 -1.000이 표시되는 것을 확인할 수 있다. 간단하게 COCOeval의 areaRng parameter를 변경하여 문제를 해결할 수 있다. 넣어야 하는 값은 데이터셋 내 Bounding box의 distribution을 직접 분석하여 구하자. (여기에서 COCO 데이터셋 내 객체들은 small이 41%, medium이 34%, 나머지 large가 24%라고 한다.)

    하지만 이렇게 되면 일반적으로 여러 논문에서 표시하는 $AP_S$, $AP_M$, $AP_L$ metric과 비교할 수 없게 된다는 문제점이 있으므로 주의하자.

    # COCOeval 초기화 부분
    ...
    cocoEval = COCOeval(cocoGt, cocoDt, annType)
    
    # areaRng parameter를 변경한다.
    obj_area_sm = 777268
    obj_area_md = 93240
    cocoEval.params.areaRng = [
        [0, 1e5**2],
        [0, obj_area_sm],
        [obj_area_sm, obj_area_md],
        [obj_area_md, 1e5**2],
    ]
    ...

    Further Research

    기본 COCO Python Evaluator COCOEval에서는 아래 3개의 기준으로 all, small, medium, large 크기를 구분한다. COCO paper에서는 이러한 언급이 별도로 없지만 COCO Evaluation API에서 해당 내용을 구현한다(원본 코드 보러가기)

    • all: $0$ ~ $10000^2$
    • small: $0$ ~ $32^2$
    • medium: $32^2$ ~ $96^2$
    • large: $96^2$ ~ $10000^2$

    Custom COCO-style Dataset이 이러한 크기보다 훨씬 큰 Object만을 가지고 있다면 이중 일부는 -1.000으로 표시되는 것으로 파악된다.

    [Golang] 외부 라이브러리 의존없이 Go 정적 빌드(Static Build)하기

    go build 명령어 실행 시 아래와 같이 지정해주면, 시스템 라이브러리 (libstdc 등)에 의존하지 않는 정적 프로그램으로 빌드할 수 있다.

    CGO_ENABLED=0 GOOS=linux GOARCH=$(dpkg --print-architecture) go build -a -ldflags '-w -extldflags "-static"' -o executable_name *.go

    Environment Variables

    • CGO_ENABLED=0: [필수] 외부 C 라이브러리에 의존하는 CGO를 비활성화한다.
    • GOOS=linux: linux OS를 타겟으로 빌드한다. (가능한 값 보기)
    • GOARCH=$(dpkg --print-architecture): 현재 빌드하는 시스템 아키텍쳐를 대상으로 빌드한다 (가능한 값 보기)
      • dpkg 명령어에 의존하므로 빌드하는 시스템은 Debian-based여야 한다. Ubuntu OS나 일반적인 golang latest Docker image의 경우 Debian-based이므로 문제가 없다.

    Build Arguments

    • -a: 이미 빌드된 패키지라고 해도 다시 빌드한다.
    • -ldflags '-w': DWARF 디버깅 정보를 삭제(strip)한다.
    • -ldflags '-extldflags "-static"': [필수] 외부 링커 (ld)에게 정적 빌드를 진행함을 알린다.

    (Optional) Extra Arguments

    • -tags netgo: 시스템 Network 라이브러리 (C-based)를 사용하지 않고 Go 내부의 Network 구현을 사용한다. CGO_ENABLED=0을 세팅하지 않을 경우 개별 라이브러리를 비활성화할 수 있는듯 하다.

    Advantages

    정적 프로그램으로 빌드할 경우 libc를 포함한 외부 라이브러리에 대한 의존이 없어서 단독으로 실행이 가능하다.

    예를 들어, 위에서 빌드한 프로그램 ./main을 아래와 같이 Docker scratch 이미지에 넣고 단독으로 실행이 가능하다 (scratch 이미지의 경우 내용물이 빈 이미지이다.)

    mkdir build
    cd build
    CGO_ENABLED=0 GOOS=linux GOARCH=$(dpkg --print-architecture) \
      go build -a -ldflags '-w -extldflags "-static"' -o main ../main.go
    
    cat > Dockerfile <<EOF
    FROM scratch
    
    COPY main /main
    ENTRYPOINT /main
    EOF
    
    docker build . -t my-image:latest
    docker run -it --rm my-image:latest

    jungin500/efinextboot – 멀티부팅을 좀더 간단하게

    Windows/Ubuntu를 멀티부팅할 일이 잦아서 자주 사용하던 chengxuncc/booToLinux 유틸리티에는 몇가지 문제점이 존재한다.

    1. 한글이 깨지는 문제
    2. 느린 실행속도

    해당 레포지토리 소스코드를 살펴본 결과 Golang으로 작성되었으며 생각보다 간단하게 구성되었다. 각 문제의 해결 방법을 찾을 수 있을것 같아 직접 밑바닥부터 다시 구현하였다.

    Features

    • 한글 깨짐 문제 수정 (cmd 인코딩 변경)
    • Powershell 라이브러리 사용하지 않아 속도 향상