perf(osar): 콜렉티브 위상별 사이클 계측 — 32ms 를 잔차가 아니라 실측으로 가른다 - #99
Conversation
스텝 고정비 42.0ms 중 dense 가중치 9.9ms 를 뺀 32.1ms 가 아직 미귀속이다. 지금까지 이 값에 붙은 설명은 전부 잔차 귀속이었고 세 번 뒤집혔다: 커널 개수 가설(상한 15.4us/커널)은 실측 5.4us/커널 로 1/3 이 됐고, 호스트 병목 가설은 GPU 사용률 93% 로 기각됐다. 지금 서 있는 가설(콜렉티브 대기)도 사용률 93% + 전력 15W 평평이라는 간접 증거뿐이다. 직접 재는 편이 빠르다. - Ctrl 의 pad[8] 을 위상 누적기 5 개 + pad[3] 로 바꾼다. 총 64B 그대로라 tx/rx 오프셋이 안 변하고 피어의 rx_base/rxf_base 가 유효하다. 단조 증가이며 리셋하지 않는다 -- done_ctr 과 같은 이유로 cudagraph 재생이 리셋을 못 본다. - k_oneshot 이 clock64() 를 5 지점에서 읽어 guard / copy / wait / reduce 로 가른다. 기록은 블록 0 스레드 0 만 한다. #93 이후 블록 0 은 항상 owns 라 모든 위상을 걷는 유일하게 대표성 있는 표본이다. 콜렉티브당 한 스레드의 store 5 회. - 호스트 리더는 프록시 스레드에 둔다. 시임에 두면 안 된다 -- 풀 디코드 cudagraph 아래서 파이썬 진입점은 캡처 때 한 번 돌고 다시는 안 돌지만 커널은 재생마다 누적한다. 프록시는 부팅 내내 콜렉티브마다 도는 유일한 호스트 코드다. 랭크 0 만, 10 초마다, 직전 보고 이후의 델타로 찍는다(느린 워밍이 정상 상태를 뭉개지 않게). 절대 us 는 SM 클럭 1592MHz 가정만큼만 정확하다(nvidia-smi clocks.sm 이 부하 전 구간에서 평평했다). 다만 네 위상의 비는 그 가정에 전혀 의존하지 않고, 우리가 알고 싶은 것이 바로 그 비다. 성능 변경이 아니라 계측기다. 컴파일 검사 없이 부팅하지 말 것 -- 실패한 부팅이 13 분이다.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_d3dddf40-922c-4bcb-9d11-4d001237ecef) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
가정: 미귀속 32ms 는 콜렉티브 지연이다. #99 의 위상 계측이 이 가정을 곧 확정 하거나 기각하지만, 가정이 맞다면 손댈 곳은 커널 개수가 아니라 대기 방식이다. 두 스핀 루프가 읽는 플래그는 전부 피어가 쓴다 -- ack_seq 는 프록시가, rxf 는 피어 NIC 가 RDMA 로. 그 플래그를 최고 속도로 읽으면 그 쓰기가 가시화되는 바로 그 경로에 트래픽을 계속 얹는 셈이다. 기다리는 대상과 경쟁한다. - 백오프: SPIN_HOT(8) 회는 그대로 뜨겁게 돌고(짧은 대기는 대가를 안 낸다), 이후 __nanosleep 을 128ns 에서 두 배씩 4096ns 상한까지. 상한이 이 변경이 더할 수 있는 지연을 묶는다 -- 콜렉티브당 최악 4us 이고, 의심하는 대기는 그보다 두 자릿수 크다. - 링 가드에서 tx_seq 를 루프 밖으로. 이 런치는 아직 게시 전이고 같은 스트림의 직전 런치는 이미 회수됐으므로 스핀 중 움직일 수 없다. 재독할 이유가 있는 것은 프록시가 올리는 ack_seq 뿐이라 루프 트래픽이 절반이 된다. - 피어 대기에서 이미 도착한 플래그를 다시 읽지 않는다. 옛 형태는 매 패스마다 세 개를 전부 재평가했다 -- 답이 바뀔 수 없는 것까지. 도착 여부를 기억하고 아직 없는 첫 번째만 읽는다. 단축평가 모양은 그대로다. 정확성은 안 건드린다. 대기 조건도, 펜스 위치도, 카운터 불변식도 그대로이고 바뀐 것은 "얼마나 자주, 무엇을 읽는가" 뿐이다. 측정 순서 주의: #99(계측만) 부팅으로 위상 분해를 먼저 받고 그 다음 이걸 얹어야 한다. 둘을 한 부팅에 섞으면 wait 이 줄어든 것이 이 변경 덕인지 원래 그랬는지 못 가른다. tests/test_logic.py 669 검사 통과. 컴파일 미검증 -- 부팅 전 컴파일 검사 필수. Co-authored-by: choiceoh <astra7471@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b19ca82bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Peer wait: rxf is only ever written by the peers' NICs, never by a block | ||
| // of this kernel -- same independence argument as the guard above. Fence | ||
| // stays where it always was: after the wait, before reading peer data. | ||
| long long t2 = timer ? clock64() : 0; |
There was a problem hiding this comment.
Start peer-wait timing after grid-wide publication
With the fixed 256-block launch, whenever block 0 performs its atomic before the last block, this timestamp is recorded before tx_seq is published. Peers cannot update rxf until the proxy observes that publication, so t3 - t2, labeled as peer wait, necessarily includes the time for the remaining local blocks to run and for the last block to publish. A slow local grid or completion counter will therefore be misreported as collective/RDMA latency, invalidating the phase diagnosis; establish the wait boundary at actual publication or account for local completion separately.
Useful? React with 👍 / 👎.
| // is the only host code that runs per collective for the life of the boot. | ||
| // Rank 0 only, every REPORT_SEC seconds, deltas since the last report so a | ||
| // slow warmup does not smear the steady state. | ||
| if (g_rank == 0) { |
There was a problem hiding this comment.
Measure every rank before attributing collective latency
Restricting reports to rank 0 observes only the three inbound paths terminating at rank 0, while the distributed step is gated by the slowest rank and path. For example, if rank 0's proxy or outbound links are the bottleneck, the other ranks wait for rank 0 but rank 0 can report a small wait, leading this diagnostic to clear the collective even though it determines step latency. Emit per-rank measurements or aggregate the maximum across ranks before using these numbers for attribution.
Useful? React with 👍 / 👎.
| // Rank 0 only, every REPORT_SEC seconds, deltas since the last report so a | ||
| // slow warmup does not smear the steady state. | ||
| if (g_rank == 0) { | ||
| time_t now = time(nullptr); |
There was a problem hiding this comment.
Keep wall-clock checks out of the proxy polling loop
On rank 0, time(nullptr) executes after every iteration of the latency-critical busy-poll loop, including iterations with no new collective or completion. This proxy is the only code publishing rank 0's RDMA writes, so reducing its polling rate can delay every peer and inflate the very wait interval being measured. Check the reporting deadline only after a bounded number of iterations or move reporting to a separate thread so the instrumentation does not perturb the collective.
Useful? React with 👍 / 👎.
| time_t now = time(nullptr); | ||
| if (last_report == 0) last_report = now; | ||
| if (now - last_report >= REPORT_SEC) { | ||
| uint64_t calls = g_ctrl->t_calls, dn = calls - last_calls; |
There was a problem hiding this comment.
Snapshot phase counters at one call boundary
The GPU updates the phase accumulators concurrently with this unsynchronized host snapshot, but t_calls is loaded before the phase fields and the saved baselines are loaded again afterward. If a collective completes between those reads, its cycles can enter the current numerator without entering dn, while the following report counts the call after its cycles were already absorbed into the baseline; this can substantially distort intervals with a small dn. Read t_calls around a single snapshot of all counters and retry when it changes, with the required system-memory ordering.
Useful? React with 👍 / 👎.
#301 머지 후 대조 확인 — 145커밋 사이에 생긴 변화가 세 축의 전제를 갈랐다: - EXP-18(osar): #99 시대 copy+reduce 11.8µs 전제는 낡음(현 45.5µs/콜, 고정비 ~22.7µs 지배) → R1 천장 ~0.2-0.3ms 로 하향. R2 의 판정 근거는 L2 위생×EXP-13 프리페치 시너지(osar churn 26MB/스텝이 프리페치 예산 12MB 와 직접 경쟁하는 것을 stwt/ldcs 로 제거)로 승격. - EXP-17(MHC 패스설정): 무장 디코드가 mk_mhc 로 TileLang mhc 179발을 대체(185→14) — 노브의 주 전장은 프리필 big_fuse + 잔여 14발로 축소. - EXP-19(hc bf16): ONEPASS 미채택 + mk_mhc 초월 — 폴백 참조 측정으로 강등, 프로덉션 경로의 같은 질문은 mk_mhc 쪽 별도 축임을 명시.
#301 머지 후 대조 확인 — 145커밋 사이에 생긴 변화가 세 축의 전제를 갈랐다: - EXP-18(osar): #99 시대 copy+reduce 11.8µs 전제는 낡음(현 45.5µs/콜, 고정비 ~22.7µs 지배) → R1 천장 ~0.2-0.3ms 로 하향. R2 의 판정 근거는 L2 위생×EXP-13 프리페치 시너지(osar churn 26MB/스텝이 프리페치 예산 12MB 와 직접 경쟁하는 것을 stwt/ldcs 로 제거)로 승격. - EXP-17(MHC 패스설정): 무장 디코드가 mk_mhc 로 TileLang mhc 179발을 대체(185→14) — 노브의 주 전장은 프리필 big_fuse + 잔여 14발로 축소. - EXP-19(hc bf16): ONEPASS 미채택 + mk_mhc 초월 — 폴백 참조 측정으로 강등, 프로덉션 경로의 같은 질문은 mk_mhc 쪽 별도 축임을 명시.


Summary
스텝 고정비 42.0 ms 중 dense 가중치 9.9 ms 를 뺀 32.1 ms 가 아직 미귀속이다. 지금까지 이 값에 붙은 설명은 전부 잔차 귀속이었고 세 번 뒤집혔다:
직접 재는 편이 빠르다.
Changes
Ctrl의pad[8]→ 위상 누적기 5 개 +pad[3]. 총 64 B 그대로라tx/rx오프셋이 안 변하고 피어의rx_base/rxf_base가 유효하다. 단조 증가·리셋 없음 —done_ctr과 같은 이유로 cudagraph 재생이 리셋을 못 본다k_oneshot이clock64()를 5 지점에서 읽어 guard / copy / wait / reduce 로 가른다. 기록은 블록 0 스레드 0 만 — perf(osar): 페이로드를 안 가진 블록은 두 스핀을 건너뛴다 — 디코드에서 피어 폴러 256→16 #93 이후 블록 0 은 항상owns라 모든 위상을 걷는 유일하게 대표성 있는 표본이다. 콜렉티브당 한 스레드의 store 5 회무엇을 가르나
104 콜렉티브/step 이므로
total × 104가 곧 AR 이 스텝에서 차지하는 몫이다.wait이 지배적 → 32 ms 는 콜렉티브 지연. 수리는 커널 개수가 아니라 대기 방식(스핀 백오프·폴링 축소·프록시 게시 경로)guard가 큼 → 링(RING=4) 이 좁아 생산자가 막힌다. RING 을 키우는 게 답Verification
tests/test_logic.py669 검사 통과Ctrl크기 불변 확인 (5×8 + 3×8 = 옛pad[8]64 B)nvidia-smi clocks.sm이 부하 전 구간 평평). 다만 네 위상의 비는 그 가정에 전혀 의존하지 않고, 알고 싶은 것이 그 비다성능 변경이 아니라 계측기다.
🤖 Generated with Claude Code
Note
Low Risk
Measurement-only changes with preserved
Ctrlsize and offsets; minor extra work on one GPU thread and periodic stderr logging on rank 0.Overview
Adds instrumentation to split each one-shot AllReduce collective into guard / copy / wait / reduce time using
clock64(), aimed at attributing unexplained per-step overhead instead of inferring it from residuals.The
Ctrllayout reuses the oldpad[8]bytes as five monotonic accumulators pluspad[3], sotx/rxoffsets and peerrx_base/rxf_basestay unchanged.k_oneshotrecords phase deltas only from block 0, thread 0 (the block that always runs every phase).The proxy thread on rank 0 prints delta-based µs/collective every 10s to stderr (not the Python shim, so metrics still update under full CUDA graph replay). Absolute µs assume 1592 MHz SM clock; phase ratios do not depend on that assumption.
Reviewed by Cursor Bugbot for commit 1b19ca8. Bugbot is set up for automated code reviews on this repo. Configure here.