Skip to content

Commit 5178686

Browse files
jhchouuuclaude
andcommitted
[cco] Bind the calling thread to its GPU's NUMA node
BindCallingThreadToGpuNumaOnce() has existed since the shmem NUMA-affinity work, and src/shmem/init.cpp:746 was its ONLY caller -- described there as "the single bind site for the shmem/EP path". A job that used CCO instead of shmem, which is every EP v2 job, ran completely unbound. ccoCommCreateImpl now calls it, at the top, before the bootstrap and Context and any worker thread, since new threads inherit the affinity. The caller has already hipSetDevice()'d -- the same contract step 2 relies on when it caches comm->hipDev. This is the cause of the bistable slow regime in docs/EP_INTERNODE_V2_TAIL.md. Unbound, the scheduler can place two of the eight per-node ranks on the two SMT siblings of one physical core (2 sockets x 96 cores, sibling of c is c+192), and both then run at about half speed. Measured as an exact 2x on the host loop of the affected ranks -- hdis 16 -> 30us, hcom 27 -> 50us -- and no change on the others. That ~36us matches the measured 35-40us phase offset, and reaches the peer through the 1:1 host-delay-to-peer-wait transfer already documented: the late ranks stall their node's intra-node barrier, and the peer node then waits at the cross-node rendezvous. Eight runs each at 4 tokens, MORI_IGNORE_CPU_AFFINITY=1 as the control: unbound bound median total 88.0us 84.8us worst total 126.0us 97.4us runs with a doubled rank 1/4 0/8 It also improves the aligned case (median hwal 74.5 -> 72.8), which is the cross-socket cost going away. Consequence to act on: every EP v2 measurement against v1/shmem so far was made across this difference, because the v1 harness calls shmem_torch_process_group_init and was bound while the v2 path was not. The "v2 degrades ~3x further than #625" result needs re-running before it means anything. Eight clean runs do not prove a zero residual rate -- the bind confines four ranks to one socket's 192 CPUs, so a collision is unlikely, not impossible. Internode correctness 30/30, 0 of 16 ranks disagreeing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f60256c commit 5178686

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

docs/EP_INTERNODE_V2_TAIL.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,57 @@ synchronize + gloo barrier every 20 rounds) still ended one of two runs in the
333333
offset state (node0 c_spin 21.7, node1 cs_bar 32.0): the offset re-forms inside
334334
20 rounds. Re-aligning treats the symptom and the loop walks straight back.
335335

336+
ROOT CAUSE FOUND: THE CCO PATH NEVER BOUND ITS THREAD
337+
-----------------------------------------------------
338+
The per-rank host loop is the whole story. In every run that landed in the slow
339+
regime, a few ranks show an EXACT 2x on their host loop and the rest show
340+
nothing:
341+
342+
slow run, hwal per rank (us)
343+
node0 r0..r7 75 76 77 77 77 78 77 76
344+
node1 r8 r9 r10 75 77 75 | r11 r12 r13 r14 = 146 149 146 146 | r15 78
345+
346+
fast run: all sixteen ranks 74-78, no rank doubled
347+
348+
`hdis` goes 16 -> 30 and `hcom` 27 -> 50 on exactly those ranks. The extra
349+
~36us of host time per round IS the phase offset (measured 35-40us), and it
350+
reaches the peer through the transfer function above: the affected ranks arrive
351+
late, their node's INTRA-node barrier (`cs_bar`) makes the other seven wait, and
352+
the peer node then waits at the cross-node rendezvous (`c_spin`).
353+
354+
The 2x is the giveaway. The box is 2 sockets x 96 cores with SMT (sibling of
355+
physical core c is c+192), so two unbound processes can land on the two
356+
hyperthreads of ONE physical core and each runs at about half speed.
357+
358+
**The fix already existed in the tree and was wired to the wrong path.**
359+
`application::BindCallingThreadToGpuNumaOnce()`
360+
(`include/mori/application/utils/cpu_affinity.hpp`) binds a thread to the CPUs
361+
local to its GPU's NUMA node, from the same sysfs `local_cpulist` NCCL uses,
362+
intersected with the existing cpuset. Its ONLY caller was `src/shmem/init.cpp:746`
363+
-- "the single bind site for the shmem/EP path". A job that used CCO instead of
364+
shmem ran completely unbound. `ccoCommCreateImpl` now calls it too.
365+
366+
This also means every EP v2 measurement against v1/shmem was unfair in v1's
367+
favour: the v1 harness calls `shmem_torch_process_group_init`
368+
(`examples/ops/dispatch_combine/test_dispatch_combine_internode.py:606`) and was
369+
bound; ours was not. The "we degrade ~3x further than #625" result above was
370+
measured across that difference and needs re-running before it means anything.
371+
372+
Measured, eight runs each, `MORI_IGNORE_CPU_AFFINITY=1` as the control:
373+
374+
unbound bound
375+
median total 88.0us 84.8us
376+
worst total 126.0us 97.4us
377+
runs with a
378+
doubled rank 1 of 4 (and ~1 in 0 of 8
379+
3 across the day)
380+
381+
Eight clean runs do not prove the residual rate is zero: the bind confines four
382+
ranks to one socket's 192 CPUs, which makes an SMT collision much less likely
383+
but not impossible. Strict per-rank disjoint pinning also gave 4 of 4 clean. If
384+
the regime ever reappears, check the per-rank `hwal` series first -- the
385+
correlation with a doubled rank has been perfect in every run so far.
386+
336387
WHAT WOULD ACTUALLY HELP
337388
------------------------
338389
Not geometry tuning: no kernel is slower, so no schedule can win the time back.

src/cco/cco_init.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <vector>
3535

3636
#include "hip/hip_runtime_api.h"
37+
#include "mori/application/utils/cpu_affinity.hpp"
3738
#include "mori/application/application.hpp" // Context, BootstrapNetwork
3839
#include "mori/application/bootstrap/local_bootstrap.hpp"
3940
#include "mori/application/bootstrap/socket_bootstrap.hpp"
@@ -422,6 +423,26 @@ static hipError_t CcoZeroWindowMem(void* ptr, size_t bytes) {
422423

423424
static int ccoCommCreateImpl(application::BootstrapNetwork* bootNet, size_t perRankVmmSize,
424425
ccoComm** outComm) {
426+
// Pin this thread to its GPU's NUMA-local CPUs before the bootstrap, the
427+
// Context and any worker thread below (new threads inherit the affinity).
428+
// This is the single bind site for the CCO path, mirroring shmem's in
429+
// src/shmem/init.cpp -- which was, until now, the ONLY caller of this helper,
430+
// so a job that used CCO instead of shmem ran unbound.
431+
//
432+
// It is not a tuning knob. Unbound, the scheduler is free to place two of the
433+
// eight per-node ranks on the two SMT siblings of one physical core; both then
434+
// run at about half speed, measured as an exact 2x on the EP host loop
435+
// (~43 -> ~80us a round) for the affected ranks and no change for the others.
436+
// Those ranks arrive ~36us late, their node's intra-node barrier makes the
437+
// other seven wait, and the peer node then waits at the cross-node rendezvous
438+
// -- which is the whole of the bistable slow regime documented in
439+
// docs/EP_INTERNODE_V2_TAIL.md. It also made every EP v2 measurement against
440+
// the v1/shmem path unfair in v1's favour, since v1 bound and v2 did not.
441+
//
442+
// The caller has already hipSetDevice()'d -- the same contract step 2 below
443+
// relies on when it caches comm->hipDev. MORI_IGNORE_CPU_AFFINITY=1 disables.
444+
application::BindCallingThreadToGpuNumaOnce();
445+
425446
auto* comm = new ccoComm();
426447
*outComm = comm;
427448

0 commit comments

Comments
 (0)