Repository navigation
Fix a race condition in contiguous k-grouped GEMM where in-flight tensormaps are updated in-place - #343
Conversation
|
I believe that, at the memory-model level, @xay5421 can help review and correct me if I missed something. |
I'm totally fine with replacing
The guide also says that the behavior is "invalid or undefined" only if "A non-exited thread specified in the I think that my usage is conceptually this: Which seems equivalent to the example from the docs. |
|
Thanks for pointing to the CUDA programming guide. I agree my previous wording was too strong: this is not unconditionally undefined behavior. The precise point is: this pattern is only valid if all non-elected lanes have truly exited before the elected lane reaches the full-mask if (... && cute::elect_one_sync()) {
__syncwarp(0xffffffff);
}
So this would be valid if the final control flow is really equivalent to: if (threadIdx.x != ELECTED_THREAD_ID)
return;
__syncwarp(0xffffffff);But the source pattern above does not guarantee that. For example, even if there is no explicit code after the if, a C++ destructor / cleanup for an object declared before the if can still be emitted at scope exit. More generally, the compiler could lower the skipped path to a common exit block: ELECT P0
@!P0 BRA common_exit
WARPSYNC 0xffffffff // elected lane only
common_exit:
// cleanup / epilogue / setmaxnreg-related code
EXITIn that case, the non-elected lanes are still non-exited when the elected lane executes |
|
Just to confirm, the next required action here is for Ivan @dfyz to update the MR based on the suggestion above? |
Yeah, I think so. I'm still not 100% sure I understand the logic about |
|
@RayWang96 I've tried using In general, I'm not sure if any form of What is the right tool for the job? I believe that, conceptually, we need to order the generic proxy write for the next group ( Of course, |
Have you checked the SASS code already? I believe all the
|
Right, and then we can wait for the DS team to review and merge it. If you want to get it merged quickly, you can merge it into the nv_dev branch first. |
After What we need is to prevent STG from being reordered before |
Yes I did (after seeing the test fail). To clarify, I'm using the following To reproduce the desired behavior, you can run the Here, the If you do the same for commit Again, you can inspect the control-flow graph to be sure.
Fair enough, but if I take commit
I think there's no rush! The training codebase where this issue was initially found has a workaround applied internally, and it would be good to fix it and review it in a proper way upstream. |
Indeed, my earlier approach was incorrect; the issue here cannot be resolved with a memory barrier. I believe warp sync is the correct fix, but I would recommend using __syncwarp(1 << lane_idx); so that it acts purely as a instruction scheduling barrier and avoids the risk of causing other side effects. |
Thanks, this sounds like a good idea. I rebased my branch on top of |
Do we maybe need to tag someone from the DS team to look at this PR? |
|
I've rebased my PR after #364 introduced a minor merge conflict. Nothing changed substantially. |
Brings deepseek-ai PR #377 (559d79f, including #364 and #343) onto nv_dev (a6b593d = #369 merge). Merge base is 88965b0. Conflict resolution highlights: * MQA: adopt #377's unified SM100 implementation and common MMA/PTX refactor while preserving nv_dev's SM90 next_n=4 multicast and block_kv=32 support, dedicated SM120 paths, FP16/BF16 weights, and 16-head indexer coverage. * Scheduler safety: retain zero-context and empty-varlen handling; drive producer prefetch from the scheduler's actual next query; bound the SM90 block_kv=32 odd-page tail before reading block tables. * FP16 weights: port the implementation to the new headers and preserve padded host offsets, per-warpgroup row indexing, and per-row compressed window bounds. * GEMM/layout: combine #377's grouped scale-factor, PSUM, zero-padding, and Mega-MoE changes with nv_dev's SM120 dense/grouped dispatch. * Tests: merge upstream coverage with nv_dev's SM90/SM100/SM120 cases and add regressions for empty metadata, skipped requests, FP16 compressed windows, and the SM90 odd physical-page tail. Validation: * All Git-tracked Python files pass py_compile. * No unresolved conflicts, conflict markers, missing project includes, or references to removed headers were found. * CUDA build/JIT and GPU numerical tests were not run because this host has no nvcc, PyTorch, or visible GPU tooling.
Syncs 9 upstream commits (54e2261..8b1392b): - Multiple updates and refactorings (deepseek-ai#364) - Fix race condition in in-flight tensormaps (deepseek-ai#343) - Public release 26/07 (deepseek-ai#377) - Fix SM100 FP16 MQA synchronization - SiTU support for FP8 FP4 MegaMoE Signed-off-by: greg pereira <grpereir@redhat.com>
…ivisible by the cluster size For GemmType::KGroupedContiguous the device scheduler lays the groups' block grids back-to-back, while SM90 TMA multicast clusters are formed from consecutive block indices. If the heuristic picks a multicast config whose per-group block grid count is odd, every cluster inside the later groups is parity-shifted and one cluster straddles a group boundary: the two CTAs disagree on is_tma_multicast_valid(), only cluster rank 0 issues SM90_TMA_LOAD_MULTICAST_2D, and the peer's full barrier never receives its transaction bytes. Depending on the ks this either hangs the kernel or silently consumes the wrong B tile and produces incorrect output. Example on 132 SMs: num_groups=2, m=192, n=3072, ks=[128, 256] selects BLOCK_M=64, BLOCK_N=72, cluster size 2, giving a per-group grid of 3 * 43 = 129 blocks, so group 1 starts on an odd global block index. Porting the exact SM90 candidate enumeration and cost model to a script reproduces 269 such (groups<=4, m, n) combos in a small scan; with this filter each falls back to a legal even-grid or cluster-1 config (for the example shape, BLOCK_M=64, BLOCK_N=152, cluster 1). The SM100 heuristic already enforces this cluster-divisibility invariant (csrc/jit_kernels/heuristics/sm100.hpp:103 rejects ceil_div(m, block_m) % cluster_m != 0 and :115 does the same for N); this applies the equivalent rule to the SM90 enumerator, which also serves the SM90 BF16 k-grouped path. PR deepseek-ai#238 (issue deepseek-ai#237) fixed the previous group-boundary hazard in this same SM90 k-grouped kernel, and PR deepseek-ai#343 fixed the in-place tensormap update race there. Also adds a k-grouped test shape (2 groups, m=192, n=3072) whose per-group grid is odd under the old selection, mirroring the existing stress shape for the SM90 in-place tensor map update.
* Fix a race condition where in-flight tensormaps are updated in-place (deepseek-ai#343) * Public release 26/07 (deepseek-ai#377) * Public Release 26/09 (deepseek-ai#432) * Public release 26/09 * Update News --------- Co-authored-by: Ivan Komarov <Ivan.Komarov@dfyz.info> Co-authored-by: Pengle Zhang <53843527+guyan364@users.noreply.github.com> Co-authored-by: Zhean Xu <94977922+zheanxu@users.noreply.github.com>
…ring (#16) * Fix a race condition where in-flight tensormaps are updated in-place (deepseek-ai#343) * Public release 26/07 (deepseek-ai#377) * Public Release 26/09 (deepseek-ai#432) * Public release 26/09 * Update News * Fix Mega MoE task info slot release ordering (deepseek-ai#441) --------- Co-authored-by: Ivan Komarov <Ivan.Komarov@dfyz.info> Co-authored-by: Pengle Zhang <53843527+guyan364@users.noreply.github.com> Co-authored-by: Zhean Xu <94977922+zheanxu@users.noreply.github.com>
The weight gradient kernel for SM90 modifies the A/B tensormaps stored in GMEM when the scheduler decides to switch groups, but it doesn't make sure that previous TMA loads have already finished reading the tensormaps at that point. A TMA load reading from a tensormap being modified concurrently corrupts the output for obvious reasons.
A reliable way to reproduce this corruption (at least on an H200) is to run the
test_k_grouped_gemm_contiguoustest a couple of times with the shape added in this PR. Eventually the corruption will be large enough to make the test fail:Even when the test doesn't fail, the corruption still happens. You can see that by printing the
torch.norm()of the result here and seeing that it differs between the test runs:The fix for this is conceptually straightforward: section 9.7.9.26.5.2 from the PTX docs says that bulk async-group based completion mechanism can be used "for the completion of reading of the tensormap object" in (otherwise mbarrier-based) TMA loads. So running
cp.async.bulk.commit_group+cp.async.bulk.wait_group.read 0before modifying GMEM ensures all in-flight TMA loads have finished reading their tensormaps, which fixes the race condition.The only catch is that writing to a TMA descriptor from a single thread generates multiple
STG.E.128instructions, andptxasdecides to move some of them before theDEPBAR.LE SB0, 0x0generated bycp.async.bulk.wait_group.read 0, which re-introduces the race condition. CUTLASS does a__syncwarp()before updating tensormaps in GMEM, and inserting a warp sync indeed preventsptxasfrom reordering instructions, but this seems more like a coincidence, since we only have one thread from the warp anyway and I don't see a good way to explain this in term of the PTX memory model. In other words, this is an ugly hack, so if you have better solutions, I will gladly implement them. :)