Repository navigation
Conversation
| _native_dynamic_per_token_quant_fp8(output, input, scale) | ||
| else: | ||
| scale = torch.zeros(1, device=input.device, dtype=torch.float32) | ||
| # NOTE: the zero-fill of `scale` is only load-bearing for the |
There was a problem hiding this comment.
Can you clean up the comments or delete the comments directly?
…quant dynamic_per_tensor_quant_fp8 zero-fills its one-element `scale` buffer for every backend, but only vLLM's kernel reads the incoming value: its segmented_max_reduction accumulates with atomicMax. AITER opens dynamic_per_tensor_quant with its own initializeScale(scale, 1, 0.0f), and the native fallback ends in a full scale.view(-1).copy_(), so on those two paths the fill is dead work -- one vectorized_elementwise_kernel<FillFunctor<float>> launch per quantized GEMM input, captured into the decode CUDA graph and replayed every step. Allocate inside each branch and zero only where a backend depends on it. Verified against the shipped AITER wheel rather than its source: pre-filling scale with +inf, NaN and +/-1e30 yields bit-identical quantized output and scale on every shape tested. 8x MI325X, SGLANG_USE_AITER=1, --quantization fp8, bench_serving 1024/1024 at concurrency 64, median of 3 reps: NeuralPizza-7B +6.14%, R1-Distill-Llama-8B +4.37%, WeirdCompound-24B +3.15%, Human-Like-Qwen2.5-7B +2.33%, Olmo-3-7B +1.86%. Mean TPOT falls on all five.
2f47bd9 to
49f61f0
Compare
|
Thanks for the review @BBuf — trimmed. The block is down to three lines covering the one thing the code cannot show: why the vLLM branch has to keep its pre-zero (its The diff is +6/-1 on one file. Could you also add the |
Done |
[AMD] Drop the redundant
scalezero-fill before AITER per-tensor FP8 quantMotivation
dynamic_per_tensor_quant_fp8zero-fills its one-elementscalebuffer beforehanding it to the backend:
Only one of those three backends reads the incoming value.
scaleis writtensegmented_max_reductionatomicMax-es into itinitializeScale(scale, 1, 0.0f)firstscale.view(-1).copy_(...), a full overwriteFor AITER,
csrc/kernels/quant_kernels.cudynamic_per_tensor_quantopens with its own fill in both the fp8 and int8branches:
So on the AITER path the buffer is zeroed twice per call, once by torch and
once by the kernel that was going to do it anyway. The torch one is a real
vectorized_elementwise_kernel<FillFunctor<float>>launch to write a singlefloat, and it happens on every quantized GEMM input — four per decoder layer,
so 128 per decode step on a 32-layer model, replayed out of the captured decode
CUDA graph for the life of the server.
Change
Move the allocation inside each branch and zero only where a backend depends on
it. The vLLM branch is untouched in behaviour and keeps
torch.zeros; AITER andthe native fallback take
torch.empty.Correctness
Reading the source is not enough, since the shipped AITER is a binary wheel, so
the installed artifact was tested adversarially: pre-fill
scalewith valuesthat would visibly corrupt a max-reduction, then compare against the
zero-filled baseline.
Bit-identical quantized output and identical scale for every poison value, on
all three shapes tested (4096x4096, 1024x5120, 17x4096). If the kernel ever
read the incoming buffer,
+infandNaNwould not survive a max-reductionunchanged.
The profiler shows the launch that disappears, and the one that stays:
aiter::initializeScaleis present on both sides — the zeroing still happens,just once.
Results
8x MI325X (gfx942), sglang
main,SGLANG_USE_AITER=1,--quantization fp8,sglang.bench_servingrandom 1024/1024, concurrency 64, 320 prompts, TP 8(TP 4 where the head count does not divide by 8). Three reps per variant
against one server boot; the table reports the median of reps 2-3, with rep 1
dropped as server warm-up on both sides.
Ten of ten improve, mean +3.76%, median +3.00%, spread +1.39% to +9.36%. The
top five rows are stock upstream checkpoints across four architecture families
and two sparsity regimes; the rest are community fine-tunes that widen the
architecture coverage.
Mean TPOT falls on every one of the ten, which is the signal the mechanism
predicts: the removed work is per decode step, not per request. The dense
8B models gain most, consistent with a fixed per-step launch cost amortised
against a smaller step.
Scope and limits
because
segmented_max_reductionaccumulates into the buffer, and it isunexercised by the numbers above (all runs are
SGLANG_USE_AITER=1).dynamic_per_token_quant) is untouched.roughly 3.7 us of device time against a ~30 us call, but its totals were too
noisy to quote as the headline; the serving numbers above are the claim.
CI States
Latest PR Test (Base): ⏳ Run #35695900578
Latest PR Test (Extra): ⏳ Run #35695900183
Latest PR Test (AMD ROCm 10): ⏳ Run #35695900526