Skip to content

feat: fused FP8 quantization output for LayerNorm - #3962

Merged
kahyunnam merged 4 commits into
flashinfer-ai:mainfrom
elwhyjay:feat/layernorm-fp8-quant
Jul 24, 2026
Merged

kahyunnam merged 4 commits into
flashinfer-ai:mainfrom
elwhyjay:feat/layernorm-fp8-quant

Conversation

@elwhyjay

@elwhyjay elwhyjay commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

📌 Description

FlashInfer provides fused norm + FP8 quantization for the RMSNorm family (rmsnorm_quant, fused_add_rmsnorm_quant), but not for LayerNorm. Models that use LayerNorm (OPT, Falcon, BERT family, ViT/DiT variants, Whisper/CLIP style encoders) currently have to run layernorm and quantization as two kernels, which costs about 7 bytes of traffic per element instead of 3.

The kernel side is already there: generalLayerNorm in include/flashinfer/norm.cuh implements per-tensor and per-token quantization paths, but the LayerNorm host launcher passes nullptr for all quant arguments, with a note:

// TODO(kaixih): add support for fp8 quantization if needed

This PR connects the existing per-tensor path rather than writing a new kernel, so the diff stays small.

  • Adds flashinfer.layernorm_quant(out, input, gemma, beta, scale, eps). Output dtype (float8_e4m3fn or float8_e5m2) is taken from the preallocated out tensor, same as rmsnorm_quant.
  • Adds fp8 QuantTypeStaticVals specializations (constants match TRT-LLM quantTypeUtils.cuh) and the missing e5m2 cuda_cast specializations. The FP8 dispatch macro instantiates both e4m3 and e5m2, so the e5m2 casts are needed for compilation.
  • Adds a LayerNormQuant host launcher, guarded by ENABLE_FP8. The csrc launcher keeps a non-FP8 build working (test_norm_compilation_without_fp8 passes).
  • Changes the per-tensor scale in generalLayerNorm to be applied as division (out = normed / scale). The kernel originally multiplied, but this path was unreachable from every existing call site (the pointer is always nullptr), so no current behavior changes. Keeping the multiply semantics would make layernorm_quant and rmsnorm_quant disagree on what scale means, which seemed worse for fusion passes that target both. Happy to adjust if there is a concern I am missing here.
  • Trace template, docs entry, tests, and a layernorm_quant benchmark routine.

Performance

RTX 5090, bf16 input, e4m3 output, eps 1e-6. Fused kernel vs. unfused baseline (flashinfer.layernorm followed by eager torch div/clamp/cast), median over 100 iterations under CUDA graph:

batch hidden unfused (us) fused (us) speedup
256 4096 13.2 6.9 1.91x
1024 4096 36.9 21.4 1.73x
4096 4096 178.9 71.8 2.49x
256 8192 21.1 8.4 2.52x
1024 8192 69.2 24.9 2.77x
4096 8192 499.6 80.6 6.20x
1024 16384 167.2 43.8 3.82x
4096 16384 1066.5 148.7 7.17x

The eager baseline materializes intermediate tensors for div/clamp/cast, so large shapes gain more than the pure traffic ratio (7B vs 3B per element, about 2.3x). At batch 4096 / hidden 16384 the fused kernel reaches about 1.35 TB/s on this card.

Reproduce with:

python benchmarks/flashinfer_benchmark.py --routine layernorm_quant \
    --batch_size 1024 --hidden_size 4096 --refcheck

🔍 Related Issues

🚀 Pull Request Checklist

Thank you for contributing to FlashInfer! Before we review your pull request, please make sure the following items are complete.

✅ Pre-commit Checks

  • I have installed pre-commit by running pip install pre-commit (or used your preferred method).
  • I have installed the hooks with pre-commit install.
  • I have run the hooks manually with pre-commit run --all-files and fixed any reported issues.

If you are unsure about how to set up pre-commit, see the pre-commit documentation.

🧪 Tests

  • tests/utils/test_norm.py::test_layernorm_quant: e4m3 and e5m2, batch 1 to 989, hidden 111 to 16384 (odd sizes cover the non-vectorized path), scale 0.01/1.0/10.0, tolerance rtol=1/atol=1 following the existing FP8 norm tests. 168 cases pass.

  • test_layernorm_quant_invalid_inputs: rejects fp16 input, non-contiguous input, and non-scalar scale.

  • test_norm_compilation_without_fp8 passes unchanged (norm module still builds without ENABLE_FP8).

  • Full tests/utils/test_norm.py passes (2887 tests).

  • pytest tests/trace/ template consistency and end-to-end tests pass for the new trace.

  • CUDA graph capture/replay smoke test passes; scale is read on device at kernel execution time, so updating the scale tensor between replays works.

  • Tests have been added or updated as needed.

  • All tests are passing (unittest, etc.).

Reviewer Notes

Summary by CodeRabbit

  • New Features
    • Added flashinfer.layernorm_quant (LayerNorm + FP8 quantization) with CUDA support across supported compute capabilities.
    • Exposed the API at both flashinfer.norm and the top-level flashinfer namespace.
    • Added benchmark and trace coverage for the quantized LayerNorm operation.
  • Documentation
    • Updated normalization API docs to include layernorm_quant.
  • Tests
    • Added correctness/invalid-input tests, plus trace example coverage and expected outputs for layernorm_quant.
  • Bug Fixes
    • Improved trace output dtype resolution when traced tensors are not available.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e30ec17-1b4c-42bc-9d1a-5556bd7ad554

📥 Commits

Reviewing files that changed from the base of the PR and between 14666f4 and bd3a295.

📒 Files selected for processing (1)
  • tests/utils/test_norm.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/utils/test_norm.py

📝 Walkthrough

Walkthrough

Adds layernorm_quant for LayerNorm with per-tensor FP8 output, including Python and CUDA APIs, tracing, benchmarks, documentation, correctness tests, and invalid-input validation.

Changes

LayerNorm FP8 quantization

Layer / File(s) Summary
Public API and trace template
flashinfer/norm/__init__.py, flashinfer/__init__.py, flashinfer/trace/templates/norm.py, flashinfer/trace/template.py, docs/api/norm.rst
Adds the public layernorm_quant operation, fake-op registration, FP8 reference trace template, trace dtype fallback, and API documentation entry.
CUDA quantized execution
include/flashinfer/norm.cuh, include/flashinfer/trtllm/common/cudaTypeUtils.cuh, csrc/norm.cu, csrc/flashinfer_norm_binding.cu
Adds FP8 type support, quantized LayerNorm dispatch, dtype/shape/stride validation, CUDA execution, and TVM FFI export.
Benchmark registration and execution
benchmarks/routines/flashinfer_benchmark_utils.py, benchmarks/routines/norm.py
Registers supported compute capabilities and adds benchmark dispatch, validation, timing, metrics, and result emission.
Correctness and generated trace coverage
tests/utils/test_norm.py, tests/trace/example.py, tests/trace/fi_trace_out/layernorm_quant_h768.json
Adds reference-based FP8 correctness tests, invalid-input tests, trace generation, and the resulting LayerNorm quantization trace template.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant PythonAPI as flashinfer.norm.layernorm_quant
  participant FFIBinding as norm.cu FFI binding
  participant CUDAKernel as norm::LayerNormQuant
  Caller->>PythonAPI: input, gamma, beta, scale, eps
  PythonAPI->>FFIBinding: dispatch layernorm_quant
  FFIBinding->>CUDAKernel: normalize and quantize to FP8
  CUDAKernel-->>Caller: write quantized output
Loading

Suggested reviewers: bkryu, saltyminty, yzh119

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding fused FP8 quantization output for LayerNorm.
Description check ✅ Passed The description follows the template and includes the change summary, checklist, tests, and reviewer notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the layernorm_quant API, which performs Layer Normalization followed by FP8 quantization, including its CUDA kernels, Python bindings, benchmark routines, trace templates, and tests. The review feedback highlights two important issues: a potential signed/unsigned comparison warning in the C++ code (csrc/norm.cu) when comparing input.stride(0) with hidden_size, and a runtime AttributeError in the benchmark script (benchmarks/routines/norm.py) caused by referencing torch.float32.itemsize instead of element_size().

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread csrc/norm.cu
Comment on lines +299 to +300
TVM_FFI_ICHECK_EQ(input.stride(0), hidden_size) << "input must be contiguous";
TVM_FFI_ICHECK_EQ(output.stride(0), hidden_size) << "output must be contiguous";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Comparing input.stride(0) (which is int64_t) with hidden_size (which is unsigned int) can trigger signed/unsigned comparison warnings (e.g., -Wsign-compare). If -Werror is enabled, this will cause compilation to fail. It is safer to cast hidden_size to int64_t to avoid any compiler warnings.

  TVM_FFI_ICHECK_EQ(input.stride(0), static_cast<int64_t>(hidden_size)) << "input must be contiguous";
  TVM_FFI_ICHECK_EQ(output.stride(0), static_cast<int64_t>(hidden_size)) << "output must be contiguous";

num_elements = np.prod(input_shape)
problem_bytes = (
num_elements * input_dtype.itemsize # input read
+ 2 * hidden_size * torch.float32.itemsize # gamma and beta read

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In standard PyTorch, torch.dtype objects (like torch.float32) do not have an itemsize attribute. Using torch.float32.itemsize will raise an AttributeError at runtime. It is safer and more standard to use torch.float32.element_size() or simply the constant 4 (since float32 is always 4 bytes).

Suggested change
+ 2 * hidden_size * torch.float32.itemsize # gamma and beta read
+ 2 * hidden_size * 4 # gamma and beta read (float32 is 4 bytes)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

torch.dtype does not seem to have element_size(). I also ran this routine end to end with --refcheck on torch 2.9 and it completed without errors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
flashinfer/norm/__init__.py (1)

534-536: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export layernorm_quant in __all__.

The new public API layernorm_quant is missing from the module's __all__ list (located around line 1783 in the file). Please export it so from flashinfer.norm import * behaves correctly and documentation engines capture it organically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashinfer/norm/__init__.py` around lines 534 - 536, Update the module’s
__all__ list to include the public layernorm_quant function defined by the
layernorm_quant declaration, preserving the existing export style and ordering
so wildcard imports and documentation discovery expose it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@csrc/norm.cu`:
- Around line 301-304: Add an explicit float32 dtype assertion for the scale
tensor alongside the existing checks in the layernorm binding, before its data
pointer is cast to float*. Use the scale symbol and match the validation style
and error behavior of the input, gamma, and beta checks.

In `@flashinfer/trace/templates/norm.py`:
- Around line 661-664: The quantized output tensor in the norm trace template is
missing its dtype source. In flashinfer/trace/templates/norm.py lines 661-664,
update the “out” Tensor constructor to use dtype_from="out"; then regenerate
tests/trace/fi_trace_out/layernorm_quant_h768.json at line 53 so its dtype is
float8_e4m3fn.

---

Nitpick comments:
In `@flashinfer/norm/__init__.py`:
- Around line 534-536: Update the module’s __all__ list to include the public
layernorm_quant function defined by the layernorm_quant declaration, preserving
the existing export style and ordering so wildcard imports and documentation
discovery expose it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b902070-3262-4b60-bf63-4680c9c2ac6b

📥 Commits

Reviewing files that changed from the base of the PR and between 41155ec and b2ee744.

📒 Files selected for processing (13)
  • benchmarks/routines/flashinfer_benchmark_utils.py
  • benchmarks/routines/norm.py
  • csrc/flashinfer_norm_binding.cu
  • csrc/norm.cu
  • docs/api/norm.rst
  • flashinfer/__init__.py
  • flashinfer/norm/__init__.py
  • flashinfer/trace/templates/norm.py
  • include/flashinfer/norm.cuh
  • include/flashinfer/trtllm/common/cudaTypeUtils.cuh
  • tests/trace/example.py
  • tests/trace/fi_trace_out/layernorm_quant_h768.json
  • tests/utils/test_norm.py

Comment thread csrc/norm.cu
Comment thread flashinfer/trace/templates/norm.py
@elwhyjay

Copy link
Copy Markdown
Contributor Author

Gentle ping @kahyunnam @yzh119 @bkryu @yongwww .Would you be able to review this when you get a chance? Thank you.

@bkryu

bkryu commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

/bot run tests/utils

@bkryu bkryu added the run-ci label Jul 16, 2026
@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !980 has been created, and the CI pipeline #58198596 is currently running. I'll report back once the pipeline job completes.

@bkryu

bkryu commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Thank you @elwhyjay for the PR. Adding the motivation in the description helps us understand.

@kahyunnam , the PR looks generally good to me, but can you take another pass?

@kahyunnam kahyunnam added the op: misc norm, activation, sampling, RoPE, quantization, etc. label Jul 16, 2026
@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #58198596: 13/20 passed

@kahyunnam kahyunnam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit but overall looks good! approving

Comment thread tests/utils/test_norm.py Outdated
y = torch.empty_like(x, dtype=quant_dtype)
flashinfer.norm.layernorm_quant(y, x, gamma, beta, scale)

torch.testing.assert_close(y_ref.float(), y.float(), rtol=1, atol=1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rtol/atol=1 seems a bit loose?

@elwhyjay elwhyjay Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review @kahyunnam . I agree with it. rtol/atol=1 only bounds a per-element FP8 step, so a systematic off-by-one would still pass. Since ref and kernel share the same quantize path, how about checking the fraction of mismatched elements instead?

tol = torch.finfo(quant_dtype).eps * y_ref.float().abs()  # one FP8 step at |ref|
mismatched = ((y_ref.float() - y.float()).abs() > tol).float().mean().item()
assert mismatched < 0.01

This keeps the FP8 rounding slack but catches a systematic error. Let me know what you think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finfo(fp8).eps can change a bit depending on e4m3 vs e5m2; let's reuse an existing helper: flashinfer.trace.default_check + default_tolerances (FP8 defaults: rtol=0.1, atol=0.1) supports max_mismatch_pct. Trace templates for FP8 norm use the same family of checks.

also, the ref and kernel slightly differ: right now the pytest ref is fp32-only, let's round normalized output to bf16 before / scale and FP8 cast (same as the benchmark refcheck)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kahyunnam. Applied it. PTAL

@kahyunnam

Copy link
Copy Markdown
Member

Thank you for your efforts here @elwhyjay ! Setting this to automerge when pre-merge passes.

@kahyunnam
kahyunnam enabled auto-merge (squash) July 24, 2026 18:41
@kahyunnam
kahyunnam merged commit 80c8e70 into flashinfer-ai:main Jul 24, 2026
49 of 60 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

op: misc norm, activation, sampling, RoPE, quantization, etc. run-ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants