Repository navigation
Conversation
Grouped MXFP8 quantization stored its per-tensor TMA descriptor storage in a file-global `static __device__` buffer shared by every kernel launch. When two `group_quantize`/`group_dequantize` calls run concurrently on different CUDA streams, one stream's `update_tma_descriptors` overwrites tensor maps another stream is still consuming, producing incorrect output (~90/100 failures on Blackwell sm100 with two concurrent grouped-quantize calls). Allocate a private `TensorMapStorage` per invocation with `cudaMallocAsync` and pass it to both the descriptor-update and consumer kernels, then release it with `cudaFreeAsync`. The storage stays in global memory (required for TMA to consume the descriptors) but is disjoint across concurrent invocations; the stream-ordered allocate/free keeps the descriptors alive until the kernels that use them finish. Apply the same fix to the sibling `group_dequantize` path, which had the identical file-global buffer. The single-tensor path is unchanged. Add a regression test that runs grouped MXFP8 quantization on two CUDA streams concurrently and checks every result against a single-stream reference. Signed-off-by: zhaoye <772971548@qq.com>
for more information, see https://pre-commit.ci
Split the grouped MXFP8 dequantize race fix into its own PR (NVIDIA#3775) so the quantize and dequantize fixes can be reviewed independently. This reverts the dequantize change from this branch; the quantize fix (grouped_tma.cuh + group_quantize_mxfp8.cuh) and its regression test remain here. Signed-off-by: zhaoye <772971548@qq.com>
[Medium impact] The change affects the grouped MXFP8 quantization path used by callers running grouped workloads. The descriptor fix appears safe to merge, with non-blocking fixes needed for test ordering and cleanup after errors. FindingsSummaryGrouped MXFP8 quantization now gives each invocation its own descriptor storage instead of sharing a device-global buffer.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Allocate private descriptors] --> B[Update descriptors]
B --> C[Quantize using descriptors]
C --> D[Free descriptors]
E[One CUDA stream per invocation] --- A
E --- D
Reviews (1) · Last reviewed commit: "Move dequantize fix to a separate PR (#3..." · Reviewed by Greptile |
| s1 = torch.cuda.Stream() | ||
| s2 = torch.cuda.Stream() |
There was a problem hiding this comment.
The test creates x1 and x2 on the current stream, then reads them on s1 and s2 without waiting for their writes to finish. The waits at the end only order the comparisons; they do not protect those input reads. This can cause failures unrelated to the descriptor race. Before launching either call, make both streams wait on torch.cuda.current_stream().
| s1 = torch.cuda.Stream() | |
| s2 = torch.cuda.Stream() | |
| s1 = torch.cuda.Stream() | |
| s2 = torch.cuda.Stream() | |
| s1.wait_stream(torch.cuda.current_stream()) | |
| s2.wait_stream(torch.cuda.current_stream()) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| if (tensor_maps_storage != nullptr) { | ||
| NVTE_CHECK_CUDA(cudaFreeAsync(tensor_maps_storage, stream)); | ||
| } |
There was a problem hiding this comment.
Failed calls leak device memory
The new allocation is freed only on the success path. If cudaFuncSetAttribute or the launch check fails, NVTE_CHECK_CUDA throws before cudaFreeAsync runs, leaving the allocation owned by nobody. Repeated failures in a process that catches the exception can keep consuming device memory. Use cleanup that also runs when an exception is thrown, while keeping the free ordered on stream.
|
Hi @XFDG, thanks for identifying and addressing this issue. |
Problem
Grouped MXFP8 quantization stored its per-tensor TMA descriptor storage in a file-global
static __device__buffer (g_tensor_mapsingrouped_tma.cuh;g_tensor_maps_input/g_tensor_maps_outputingroup_dequantize_mxfp8.cuh). That buffer is shared by every launch of the kernel across all CUDA streams. When twogroup_quantizecalls run concurrently on different streams (e.g. per-microbatch in a pipelined or comm-overlap schedule), one stream'supdate_tma_descriptorsoverwrites tensor maps that another stream is still consuming, so the second stream reads clobbered descriptors and produces incorrect output. On Blackwell (sm100) this reproduces ~90/100 of the time with two concurrent grouped-quantize calls.Fix
Make the descriptor storage per-invocation instead of file-global. The launcher now allocates a private
TensorMapStorage(quantize) orCUtensorMaparrays (dequantize) withcudaMallocAsyncand passes them to both the descriptor-update kernel and the consumer kernel, then releases them withcudaFreeAsync. This keeps the storage in global memory (required for TMA to consume the descriptors) while making it disjoint across concurrent invocations.cudaMallocAsync/cudaFreeAsyncare stream-ordered, so the descriptors stay alive until the kernels that use them have finished and the pool never hands the block to another stream in the meantime. This is the same idiom already used incomm_gemm_overlap.cpp.The single-tensor path is unchanged — it uses static per-CTA descriptors and never touched the global buffer.
Validation
tests/pytorch/mxfp8/test_mxfp8_group_quantize_multistream.pyruns grouped MXFP8 quantization on two CUDA streams concurrently and checks every result against a single-stream reference.Files
transformer_engine/common/cast/core/grouped_tma.cuh—update_tma_descriptorstakes a per-callTensorMapStorage *; remove the file-globalg_tensor_maps.transformer_engine/common/cast/mxfp8/group_quantize_mxfp8.cuh— allocate/pass/free a per-callTensorMapStorage.transformer_engine/common/cast/mxfp8/group_dequantize_mxfp8.cuh— same fix for the sibling dequantize path, which had the identical file-global buffer.tests/pytorch/mxfp8/test_mxfp8_group_quantize_multistream.py— new regression test.