Skip to content

[XPU] Dispatch nn.LayerNorm to fused SYCL kernel via CustomOp - #57172

Merged
hmellor merged 12 commits into
vllm-project:mainfrom
mganczarenko:xpu-layernorm-sycl-wiring
Sep 30, 2026
Merged

hmellor merged 12 commits into
vllm-project:mainfrom
mganczarenko:xpu-layernorm-sycl-wiring

Conversation

@mganczarenko

@mganczarenko mganczarenko commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

vllm-xpu-kernels exposes fused SYCL kernels for standard (non-RMS) LayerNorm (vllm-xpu-kernels#577) - layer_norm, fused_add_layer_norm, nemotron_layer_norm, fused_add_nemotron_layer_norm - but nothing in vLLM dispatched to them yet. Every model with a bare
nn.LayerNorm fell back to torch-xpu-ops' generic kernel on XPU.

  • Adds StandardLayerNorm, a CustomOp (registered "layer_norm") - drop-in for a bare nn.LayerNorm, dispatching via forward_native/forward_cuda/forward_xpu, matching RMSNorm/GemmaRMSNorm in the same file.
  • Reworks NemotronLayerNorm1P (Nemotron's "+1 to weights" variant) from an nn.LayerNorm subclass into a CustomOp sibling ("nemotron_layer_norm"); its XPU path also dispatches the fused-residual-add kernel when a residual is present.
  • Wires both into GPT-2, GPT-J, OPT, SigLIP (PaliGemma's vision tower), and Nemotron.
  • Adds replace_layernorm_class (transformers/utils.py), matching replace_linear_class/replace_conv_class/replace_embedding_class, so the Transformers modeling backend's generic recursive module-replace picks this up too - covers StarCoder2-style models with no dedicated vLLM model file.
  • Kernel calls are wrapped as real torch.ops.vllm.xpu_* custom ops (_xpu_ops.py, via direct_register_custom_op), the same way GemmaRMSNorm wraps its kernel - compile-safe by construction, gated only by the standard CustomOp +/-custom_ops toggle.
  • Every dispatch path is hasattr(torch.ops._C, ...)-guarded and falls back to native F.layer_norm when the op isn't present - currently a no-op everywhere, since no vllm-xpu-kernels release yet contains Incorrect output when using hf-internal-testing/llama-tokenizer #577 (just merged) and requirements/xpu.txt's pin predates it.

Test Plan

  • ruff check, ruff format, full pre-commit (incl. mypy) on every touched file.
  • Kernel correctness on Intel B70 hardware: parametrized cases across hidden sizes (incl. non-power-of-2), bf16/fp16, weight/bias/weight_bias combinations, plus fused-add cases, for both layer_norm and nemotron_layer_norm/fused_add_nemotron_layer_norm, checked against a plain-PyTorch fp32 reference.
  • End-to-end serve (--enforce-eager) on the same hardware, per affected model.
  • End-to-end performance via the official vllm bench serve/throughput/latency, baseline vs patched, N=3 median.
  • Not yet added: a correctness test in the repo (tests/kernels/core/test_layernorm.py covers RMSNorm/GemmaRMSNorm, nothing exercised plain LayerNorm before this PR). Not yet done: lm_eval accuracy check for any affected model. A reproducible kernel-level benchmark (benchmark/benchmark_layer_norm.py) is already
    committed on the vllm-xpu-kernels PR itself.

Test Result

Correctness: 64 parametrized cases + 2 fused-add cases, all pass vs plain-PyTorch fp32 reference.

Kernel microbenchmark (native F.layer_norm vs SYCL, bf16, hidden_size=4096):

tokens native SYCL speedup
1 8.35us 3.60us 2.32x
128 9.63us 3.70us 2.60x
512 28.20us 5.90us 4.78x
1024 51.73us 10.83us 4.78x (peak)
2048 103.91us 56.61us 1.84x

End-to-end serve: GPT-J, GPT-2, OPT-125m, Nemotron/Minitron-8B, PaliGemma (SigLIP), and StarCoder2-3B all served and generated correctly with --enforce-eager; SYCL dispatch confirmed firing inside the real engine, not just via a direct standalone op call.

End-to-end performance:

Model Path Throughput Latency Serve
GPT-2-124M direct swap +9.1% -9.5% (faster) +5.3%
OPT-125M direct swap +9.2% -1.2% (faster) +9.4%
GPT-J-405M direct swap +6.1% -4.4% (faster) +9.2%
StarCoder2-3B generic swap +1.0% -1.2% (faster) +1.3%
Minitron-8B (Nemotron) direct swap +0.8% -0.9% (faster) +0.5%

Caveat: the above was measured against an earlier revision of this dispatch code (direct torch.ops._C.* calls, gated by torch.compiler.is_compiling()). This PR wraps those same calls through torch.ops.vllm.xpu_* instead, for torch.compile safety - a thin pass-through to the identical kernel call, so behavior should
be unchanged, but hasn't itself been re-run on hardware.

mganczarenko and others added 4 commits September 15, 2026 11:22
Adds EagerLayerNorm(CustomOp) and NemotronLayerNorm1P(CustomOp),
matching RMSNorm/GemmaRMSNorm's forward_native/forward_cuda/forward_xpu
dispatch (vllm-xpu-kernels layer_norm/nemotron_layer_norm, PR vllm-project#577).
Wires GPT-2/GPT-J/OPT/SigLIP/Nemotron, plus a replace_layernorm_class
helper for the Transformers backend's generic module-replace.

hasattr-guarded: no-op until vllm-xpu-kernels cuts a release past vllm-project#577
and requirements/xpu.txt is bumped to it.

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nels

empty_like preserves the input's strides; the kernel needs a contiguous
out, same as GemmaRMSNorm.forward_xpu right above it in this file.

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… dodge

forward_xpu called torch.ops._C.layer_norm/nemotron_layer_norm directly,
which isn't traceable, and worked around that with a torch.compiler.
is_compiling() runtime check to skip the kernel under compile. GemmaRMSNorm
avoids needing this: its kernel calls go through torch.ops.vllm.xpu_*,
registered via direct_register_custom_op in _xpu_ops.py, which makes them
compile-safe by construction - the standard CustomOp +/-custom_ops toggle
is then the only gate needed, same as every other CustomOp here.

Adds the matching xpu_layer_norm/xpu_fused_add_layer_norm/
xpu_nemotron_layer_norm/xpu_fused_add_nemotron_layer_norm registrations
and switches both forward_xpu methods to call through them instead.

Renames EagerLayerNorm -> StandardLayerNorm now that the "eager" dodge
it was named for is gone.

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measured -3.1% throughput / -1.6% serve on XPU, reproduced twice with
tight non-overlapping spreads - a real regression, not noise. Other
Transformers-backend models (StarCoder2) gain through the identical
swap, so exclude just this architecture rather than the whole path.

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added the intel-gpu Related to Intel GPU label Sep 16, 2026
Comment thread vllm/model_executor/models/transformers/utils.py
Comment thread vllm/model_executor/layers/layernorm.py Outdated


@CustomOp.register("layer_norm")
class StandardLayerNorm(CustomOp):

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.

Why not just call it LayerNorm?

Suggested change
class StandardLayerNorm(CustomOp):
class LayerNorm(CustomOp):

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.

There is already LayerNorm class being nn.Module at the end of the file, but I am open for alternatives for the name

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.

Could we port the other LayerNorm to be a custom op instead of adding a new one with a different name?

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.

I think we could. Please, take a look and let me know if it looks good to you - ff208db

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.

@hmellor

hmellor commented Sep 16, 2026

Copy link
Copy Markdown
Member

Given that this is directly swapping nn.LayerNorm, why not put the fused kernel in torch-xpu-ops?

@mganczarenko

Copy link
Copy Markdown
Contributor Author

Given that this is directly swapping nn.LayerNorm, why not put the fused kernel in torch-xpu-ops?

I've tried to match the precedents already set by RMSNorm/GemmaRMSNorm in the same file (both dispatch fused kernels living in vllm-xpu-kernels), so I've chosen vllm-xpu-kernels at first. This is the place where vLLM's XPU-specific fast paths for attention/MoE/quant/norm ops already live.

Nothing rules out upstreaming this to torch-xpu-ops, but this is another process.

@hmellor

hmellor commented Sep 16, 2026

Copy link
Copy Markdown
Member

IIRC the reason that the RMSNorm/GemmaRNSNorm classes exist was so that they could be picked up by the custom op machinery in vLLM and used in vLLM's fusion passes.

Since there are no users of this new layer there are no fusion passes for it to be captured by.

@mergify

mergify Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @mganczarenko.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 17, 2026
@mganczarenko

Copy link
Copy Markdown
Contributor Author

IIRC the reason that the RMSNorm/GemmaRNSNorm classes exist was so that they could be picked up by the custom op machinery in vLLM and used in vLLM's fusion passes.

Since there are no users of this new layer there are no fusion passes for it to be captured by.

Thanks for the clarification. Following up on the question about why this couldn't just be a kernel contribution to torch-xpu-ops instead of a vLLM-specific CustomOp + vllm-xpu-kernels kernel.

Why this kernel is faster than native?

torch-xpu-ops already has a LayerNorm kernel for XPU (at::native::xpu::VectorizedLayerNormKernelFunctor) - the pre-patch path in this PR already uses it, via aten::native_layer_norm. So this isn't about filling a gap in torch-xpu-ops. I profiled both paths end-to-end (GPT-J-405M and OPT-125m, B60, torch profiler traces down to the individual event level) to see where the improvement comes from, and it splits into two pieces:

  1. The new SYCL kernel itself is modestly faster than torch-xpu-ops's native kernel at this scale, but small in
    absolute terms for these model sizes.
  2. The bigger effect comes from dispatch, not the kernel. aten::native_layer_norm is a full ATen "native" op - it's registered with autograd and functionalization support, because torch-xpu-ops has to serve every PyTorch consumer, including training. Every call walks that dispatcher-key machinery before reaching the backend kernel, and none of it shows up as a named event in a profile (it's just unlabeled C++ time between the last visible op and the driver call) - but it's real, unavoidable cost tied to the op's schema, not to which kernel runs underneath it. vllm.model_executor.layers.layernorm.StandardLayerNorm's CustomOp registers xpu_layer_norm/_C::layer_norm as an inference-only op with no autograd/functionalization layering -there's structurally less dispatcher work to do, and it's Python-visible (torch.ops.* frames) rather than a black-box gap.

Why it does not meet requirements to be native?

That second part is the actual reason this lives outside torch-xpu-ops: it's not a limitation of torch-xpu-ops's kernel quality, it's that any backend registered for aten::native_layer_norm inherits the full differentiable-op dispatch contract, which vLLM's inference-only serving path never needs. Bypassing it requires an op outside that contract - which is what CustomOp + vllm-xpu-kernels gives us, and what a general-purpose PyTorch backend project can't do without breaking autograd for everyone else using aten::native_layer_norm. Same pattern already used here for GemmaRMSNorm/RMSNorm, so this isn't a new precedent, just extending an existing one to LayerNorm.

Resolve conflict in transformers/base.py: upstream added
orig_to_new_stacked (used by register_fusion's weight mapper), this
branch added the swap_layernorm/arch OLMo-exclusion check at the same
insertion point. Kept both, no functional change to either.

Also fix a mypy failure surfaced by the merge: opt.py's
final_layer_norm needs the same explicit `StandardLayerNorm | None`
pre-declaration project_out/project_in already use, since the first
assignment (StandardLayerNorm) otherwise narrows the inferred type
before the None branch is considered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
@mergify mergify Bot removed the needs-rebase label Sep 18, 2026
mganczarenko and others added 2 commits September 22, 2026 11:30
Replace the `nn.Module` `LayerNorm` with the renamed `layer_norm` CustomOp, deriving the compute dtype from `torch.promote_types(x.dtype, weight.dtype)` so fp32 parameters keep the old fp32 reduction.

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mergify mergify Bot added the deepseek Related to DeepSeek models label Sep 22, 2026
Comment thread vllm/model_executor/models/transformers/base.py Outdated
…Norm never subclassed nn.LayerNorm, so the isinstance-gated swap it guarded was already unreachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Signed-off-by: Michal Ganczarenko <michal.ganczarenko@intel.com>
@hmellor

hmellor commented Sep 30, 2026

Copy link
Copy Markdown
Member

/ci run

@hmellor
hmellor enabled auto-merge (squash) September 30, 2026 11:03
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 30, 2026
@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #92117 for commit e9f2f1c3702d.

@hmellor
hmellor merged commit df5668e into vllm-project:main Sep 30, 2026
183 of 184 checks passed
pawel-olejniczak added a commit to pawel-olejniczak/vllm-gaudi that referenced this pull request Oct 15, 2026
…LM LayerNorm in the regional compilation unit test

Root cause: vllm#57172 replaced torch nn.LayerNorm in OPT with vllm's CustomOp LayerNorm, so the test's torch LayerNorm filter matches nothing
Upstream: vllm-project/vllm#57172
Fix: import LayerNorm from vllm.model_executor.layers.layernorm, which OPT now builds its norms from

Signed-off-by: Paweł Olejniczak <pawelx.olejniczak@intel.com>
pawel-olejniczak added a commit to pawel-olejniczak/vllm-gaudi that referenced this pull request Oct 15, 2026
…LM LayerNorm in the regional compilation unit test

Root cause: vllm#57172 replaced torch nn.LayerNorm in OPT with vllm's CustomOp LayerNorm, so the test's torch LayerNorm filter matches nothing
Upstream: vllm-project/vllm#57172
Fix: import LayerNorm from vllm.model_executor.layers.layernorm, which OPT now builds its norms from

Signed-off-by: Paweł Olejniczak <pawelx.olejniczak@intel.com>
pawel-olejniczak added a commit to pawel-olejniczak/vllm-gaudi that referenced this pull request Oct 16, 2026
…LM LayerNorm in the regional compilation unit test

Root cause: vllm#57172 replaced torch nn.LayerNorm in OPT with vllm's CustomOp LayerNorm, so the test's torch LayerNorm filter matches nothing
Upstream: vllm-project/vllm#57172
Fix: import LayerNorm from vllm.model_executor.layers.layernorm, which OPT now builds its norms from

Signed-off-by: Paweł Olejniczak <pawelx.olejniczak@intel.com>
pawel-olejniczak added a commit to pawel-olejniczak/vllm-gaudi that referenced this pull request Oct 16, 2026
…LM LayerNorm in the regional compilation unit test

Root cause: vllm#57172 replaced torch nn.LayerNorm in OPT with vllm's CustomOp LayerNorm, so the test's torch LayerNorm filter matches nothing
Upstream: vllm-project/vllm#57172
Fix: import LayerNorm from vllm.model_executor.layers.layernorm, which OPT now builds its norms from

Signed-off-by: Paweł Olejniczak <pawelx.olejniczak@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepseek Related to DeepSeek models intel-gpu Related to Intel GPU ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants