onnxruntime
8cf10e7b - [CUDA] Pass Concat per-input metadata by value (#32119)

Commit
51 days ago
[CUDA] Pass Concat per-input metadata by value (#32119) ## Description CUDA `Concat`'s ragged path (inputs of unequal size along the concat axis) uploaded four `CudaAsyncBuffer`s to the device on every `Run`, one of which is sized by the *output* axis dimension. This change recovers the input index by scanning prefix sums instead, which makes it possible to pass the per-input metadata by value in `TArray`s — matching what the equal-size path already does. The ragged by-value path is dispatched before any `CudaAsyncBuffer` is created, like the equal-size path after #32410. So for up to 32 inputs, Concat allocates no pinned host memory at all. That matters under CUDA graph capture: `cudaHostAlloc` is not allowed while a stream is capturing, and the failed allocation surfaced as a misleading `alloc failed` (seen with a DFlash2 drafter graph that concatenates `[1, H]` and `[N - 1, H]` slices). The result is also a large reduction in host-to-device traffic for graphs that concatenate many small tensors, which is common in decode-time LLM workloads. ## Summary of Changes | File | Change | |------|--------| | `onnxruntime/core/providers/cuda/tensor/concat.cc` | For up to 32 inputs, dispatch both equal-size and ragged inputs by value before the pinned pointer buffer is allocated; keep the buffer path for wider concats. | | `onnxruntime/core/providers/cuda/tensor/concat_impl.cu` | Add the by-value ragged kernel; recover the input index by scanning prefix sums rather than reading the output-sized mapping table. | | `onnxruntime/core/providers/cuda/tensor/concat_impl.h` | Declare the by-value launcher. | | `onnxruntime/test/providers/cpu/tensor/concat_op_test.cc` | Ragged CUDA tests at 32 inputs (by-value) and 33 inputs (buffer fallback). | | `onnxruntime/test/python/onnxruntime_test_python_cudagraph.py` | Ragged Concat on axis 0 and axis 1 under CUDA graph capture, with the inputs updated before each replay. | Notes: - The existing buffer-based path is retained unchanged for concats wider than 32 inputs, so nothing regresses for large fan-in. - Passing parameters by value leaves no host staging buffer for a captured memcpy node to re-read on replay. ## Testing - `concat_op_test.cc`: `CudaRagged32InputsUsesByValueMetadata`, `CudaRagged33InputsUsesGpuMetadataFallback`. - `onnxruntime_test_python_cudagraph.py`: `test_concat_unequal_sizes_cuda_graph_replay` (all 6 tests in the file pass on H200, CUDA 13.0). - Kernel output is bit-exact against NumPy across fp16/fp32/int64/int8, with zero-length inputs and 32/33/40-input ragged cases, with and without CUDA graph. - End-to-end on DeepSeek-V4-Flash (8xH200, tensor parallel, CUDA graph): MMLU-Pro (800 samples) unchanged at 0.67000. - End-to-end on a Qwen3.8-27B DFlash2 model with the drafter's CUDA graph enabled: previously failed at the drafter's ragged Concat on engine step 2; now runs cleanly (54/54 draft tokens accepted). ## Motivation and Context Measured on a DeepSeek-V4-Flash decode step (8xH200, tensor parallel, CUDA graph): | Metric | Before | After | |---|---|---| | Host-to-device copies per step | 417 | 1 | | Step time | 20.2 ms | 18.8 ms | | Batch-1 decode throughput | 49.49 tok/s | 53.21 tok/s | The saving is 0.46 ms of transfer plus 1.21 ms of the stalls that trailed those transfers. The benefit is general: any model whose graph concatenates a moderate number of tensors per step pays this cost today. ## Checklist - [x] Tests added/updated - [x] No breaking changes - [ ] Documentation updated (not applicable — no schema or API change)
Author
Parents
Loading