Summary
As Ascend A5 support is being added to SGLang, generation differences are no longer limited to kernel availability. They increasingly affect operator providers, signatures, tensor dtypes/layouts, KV representations, and quantization metadata.
If these differences continue to be handled directly in Attention, MoE, Quantization, Model, and Memory components:
if is_a5():
...
else:
...
generation-specific logic will keep leaking into feature code.
This RFC proposes a two-layer adaptation model:
Model / Attention / MoE / Quant / Memory
│
▼
SGLang DeviceOperator
runtime-visible device contract
│
▼
sgl-kernel-npu Stable Operator
│
▼
target-specific provider
Ascend910 / Ascend950
│
▼
torch_npu / ACLNN / AscendC / Triton
The core rule is:
Hardware differences that can be fully hidden behind a stable standalone operator contract should be handled in sgl-kernel-npu. Differences that change SGLang runtime-visible dtype, layout, metadata, or cross-operator state should be handled by SGLang DeviceOperator.
Motivation
Current A5 support already exposes two fundamentally different kinds of generation differences.
Kernel-local differences
For example, Gemma RMSNorm uses different implementations across generations:
A2/A3:
torch_npu.npu_gemma_rms_norm
A5:
torch_npu.npu_rms_norm(input, 1 + weight)
However, both can expose the same semantic API:
npu_gemma_rms_norm(input, weight, eps)
sgl-kernel-npu#638 demonstrates this model: provider selection happens while building a target-specific wheel, while SGLang only consumes a stable operator API and does not select providers at runtime.
Runtime-visible differences
Other generation differences cannot be hidden inside a standalone operator.
For example, an A5 KV/attention path may change:
INT8 KV -> FP8 KV
scale format -> different metadata representation
KV layout -> packed representation
attention API -> different operator contract
These differences affect Memory Pool, Attention, and possibly other runtime components before a kernel is invoked.
Therefore, we need an explicit ownership boundary between kernel implementation differences and runtime device contract differences.
Goals
This RFC aims to:
- reduce scattered Ascend generation checks in SGLang;
- keep kernel/provider differences inside
sgl-kernel-npu whenever possible;
- centralize runtime-visible generation contracts in SGLang
DeviceOperator;
- preserve existing ownership of Attention, MoE, Memory, and Quantization components;
- provide a consistent extension model for future Ascend generations;
- avoid introducing a large device abstraction that wraps every NPU operator.
Ownership Model
The default decision rule is:
Generation difference
│
▼
Can it be hidden behind one stable
standalone operator API?
│
┌────┴────┐
│ Yes │ No
▼ ▼
sgl-kernel Does runtime require different
-npu dtype/layout/metadata/state?
│
┌────┴────┐
│ Yes │ No
▼ ▼
SGLang Feature /
DeviceOp capability
Typical ownership:
| Difference |
Owner |
| torch_npu / ACLNN / AscendC / Triton provider difference |
sgl-kernel-npu |
| Same semantic operator with different kernel implementations |
sgl-kernel-npu |
| Operator-local reshape/cast |
sgl-kernel-npu |
| Runtime KV dtype/layout |
SGLang DeviceOperator |
| Runtime metadata contract |
SGLang DeviceOperator |
| Representation shared across multiple operators |
SGLang DeviceOperator |
| Enable MXFP4 / FP8 KV |
Feature / Config |
| KV allocation and lifetime |
Memory Pool |
| DSA/MLA or TP/EP policy |
Feature |
An operator signature difference alone does not require SGLang DeviceOperator.
If the difference can be cleanly normalized behind a stable standalone API, it should remain in sgl-kernel-npu.
sgl-kernel-npu: Stable Operator + Target Provider
We do not propose another large abstraction such as:
class BaseDeviceOps:
...
class A5DeviceOps(BaseDeviceOps):
...
inside sgl-kernel-npu.
Instead, the kernel layer follows:
Stable Operator Contract + Target-specific Provider
For example:
npu_gemma_rms_norm()
│
┌────────┴────────┐
▼ ▼
Ascend910 provider Ascend950 provider
native ACLNN
SGLang only depends on:
and does not need to know which provider is used.
A stable operator should remain standalone:
Tensor / scalar / attributes
│
▼
Stable Operator
│
▼
Tensor / outputs
It should not depend on SGLang runtime objects such as:
ForwardBatch
MemoryPool
Scheduler
ModelRunner
AttentionBackend
Target-specific Provider Staging
sgl-kernel-npu #734 generalizes the Gemma-specific mechanism introduced in#638 into generic target-provider staging.
Its core principle is:
The build system knows targets, but does not know operators.
Providers are organized by target and stable module path:
target_providers/
├── Ascend910/
│ └── norm/
│ └── gemma_rmsnorm.py
│
└── Ascend950/
└── norm/
└── gemma_rmsnorm.py
During wheel build:
target_providers/Ascend950/norm/gemma_rmsnorm.py
│
▼
sgl_kernel_npu/
norm/
gemma_rmsnorm.py
The installed public API therefore remains:
sgl_kernel_npu.norm.gemma_rmsnorm
regardless of the target implementation.
Directory as Registry
There is intentionally no global operator registry such as:
PROVIDERS = {
"norm.gemma_rmsnorm": ...,
"quant.dynamic_mx": ...,
}
Instead, the directory layout itself acts as the registration mechanism.
Adding another portable operator only requires matching module paths:
target_providers/
├── Ascend910/
│ └── quant/dynamic_mx.py
└── Ascend950/
└── quant/dynamic_mx.py
The generic build framework does not need to know whether the module implements Gemma RMSNorm, MXFP, MoE, or Attention.
#734 reduces provider staging to:
validate target
↓
scan provider tree
↓
stage provider modules
No operator-specific build logic is required.
Build-time, Not Runtime Dispatch
Provider selection should happen while building the target-specific wheel:
Concrete SoC
│
▼
normalize provider target
│
├── Ascend910
└── Ascend950
│
▼
stage provider tree
│
▼
target-specific wheel
The runtime does not contain:
or:
provider = registry[current_device]
There is also no silent runtime fallback.
Unknown targets, missing provider directories, and conflicts with common modules should fail during the build.
This keeps provider compatibility out of the inference hot path and makes target mismatches explicit.
Provider Contract
A target-specific provider is appropriate when all of the following are true:
same semantic operation
+
different target implementation
+
same logical input/output contract
For example:
Ascend910:
npu_gemma_rms_norm(input, weight, eps)
Ascend950:
npu_rms_norm(input, 1 + weight, eps)
Both expose:
npu_gemma_rms_norm(input, weight, eps)
-> (output, rstd)
#734 also enforces that target provider trees expose the same module set, keeping target differences at the implementation layer rather than the public API layer.
SGLang DeviceOperator
When generation differences cross the standalone operator boundary, they should be handled in SGLang.
For example:
A3:
INT8 KV
FP16 scale
legacy sparse-attention contract
A5:
FP8 packed KV
different scale metadata
different sparse-attention contract
In this case, the Memory Pool must allocate a different representation before the kernel executes. A kernel provider alone cannot hide the difference.
The proposed responsibility is:
SGLang DeviceOperator
│
├── runtime dtype contract
├── layout contract
├── metadata contract
└── semantic operator adaptation
│
▼
sgl-kernel-npu stable ops
DeviceOperator should not own:
kernel provider selection
feature enable/disable
KV lifetime
algorithm selection
In short:
DeviceOperator defines how an already-selected feature runs on the current device generation. It does not decide whether the feature should be enabled.
Representative Cases
Gemma RMSNorm
different implementation
same standalone semantic contract
→ sgl-kernel-npu target provider
#638 + #734provide the reference implementation.
MXFP scale layout
If normalization is operator-local:
kernel output
↓
local reshape / normalization
↓
stable public representation
→ sgl-kernel-npu
If the representation is stored and consumed across multiple runtime components:
producer
↓
runtime state
↓
multiple consumers
→ SGLang DeviceOperator
KV / Sparse Attention
If generation differences affect:
KV dtype
physical KV layout
scale metadata
memory allocation contract
→ SGLang DeviceOperator
The concrete standalone kernels used by that path remain in:
Rollout
The two repositories can evolve independently under the same ownership model.
sgl-kernel-npu
#638(https://github.com/sgl-project/sgl-kernel-npu/pull/638)
Gemma stable operator
↓
#734(https://github.com/sgl-project/sgl-kernel-npu/pull/734)
generic target-provider staging
↓
additional portable operators
After #734, adding a new cross-generation operator only requires:
- confirm that it has the same semantic and logical I/O contract;
- add the same module path under the required
target_providers/<target>/ directories;
- add contract tests.
No generic build-system changes should be required.
SGLang
Introduce a minimal DeviceOperator and migrate only existing runtime-visible differences, such as:
KV representation
Sparse Attention contract
Indexer dtype/layout
cross-operator quantization metadata
This RFC does not propose wrapping every NPU operator.
Acceptance Criteria
The intended long-term rules are:
- SGLang feature code does not directly select kernel providers.
- Standalone generation differences are handled in
sgl-kernel-npu whenever possible.
sgl-kernel-npu exposes stable semantic/module contracts.
- Target-specific providers are selected during wheel build.
- The build framework knows targets, not individual operators.
- Provider dispatch is kept out of the inference hot path.
- SGLang
DeviceOperator only handles runtime-visible device contracts.
- Feature, Memory, and Scheduler ownership remains unchanged.
- Neither repository introduces a God Object covering all NPU operators.
Open Questions
- Do we agree on the following default ownership rule?
standalone kernel/provider difference
→ sgl-kernel-npu
runtime-visible representation/contract difference
→ SGLang DeviceOperator
- Which existing runtime-visible A5 difference should be the first minimal SGLang
DeviceOperator migration to validate the abstraction?
References
Summary
As Ascend A5 support is being added to SGLang, generation differences are no longer limited to kernel availability. They increasingly affect operator providers, signatures, tensor dtypes/layouts, KV representations, and quantization metadata.
If these differences continue to be handled directly in Attention, MoE, Quantization, Model, and Memory components:
generation-specific logic will keep leaking into feature code.
This RFC proposes a two-layer adaptation model:
The core rule is:
Motivation
Current A5 support already exposes two fundamentally different kinds of generation differences.
Kernel-local differences
For example, Gemma RMSNorm uses different implementations across generations:
However, both can expose the same semantic API:
sgl-kernel-npu#638 demonstrates this model: provider selection happens while building a target-specific wheel, while SGLang only consumes a stable operator API and does not select providers at runtime.Runtime-visible differences
Other generation differences cannot be hidden inside a standalone operator.
For example, an A5 KV/attention path may change:
These differences affect Memory Pool, Attention, and possibly other runtime components before a kernel is invoked.
Therefore, we need an explicit ownership boundary between kernel implementation differences and runtime device contract differences.
Goals
This RFC aims to:
sgl-kernel-npuwhenever possible;DeviceOperator;Ownership Model
The default decision rule is:
Typical ownership:
sgl-kernel-npusgl-kernel-npusgl-kernel-npuDeviceOperatorDeviceOperatorDeviceOperatorAn operator signature difference alone does not require SGLang
DeviceOperator.If the difference can be cleanly normalized behind a stable standalone API, it should remain in
sgl-kernel-npu.sgl-kernel-npu: Stable Operator + Target Provider
We do not propose another large abstraction such as:
inside
sgl-kernel-npu.Instead, the kernel layer follows:
For example:
SGLang only depends on:
npu_gemma_rms_norm(...)and does not need to know which provider is used.
A stable operator should remain standalone:
It should not depend on SGLang runtime objects such as:
Target-specific Provider Staging
sgl-kernel-npu#734 generalizes the Gemma-specific mechanism introduced in#638 into generic target-provider staging.Its core principle is:
Providers are organized by target and stable module path:
During wheel build:
The installed public API therefore remains:
regardless of the target implementation.
Directory as Registry
There is intentionally no global operator registry such as:
Instead, the directory layout itself acts as the registration mechanism.
Adding another portable operator only requires matching module paths:
The generic build framework does not need to know whether the module implements Gemma RMSNorm, MXFP, MoE, or Attention.
#734 reduces provider staging to:
No operator-specific build logic is required.
Build-time, Not Runtime Dispatch
Provider selection should happen while building the target-specific wheel:
The runtime does not contain:
or:
There is also no silent runtime fallback.
Unknown targets, missing provider directories, and conflicts with common modules should fail during the build.
This keeps provider compatibility out of the inference hot path and makes target mismatches explicit.
Provider Contract
A target-specific provider is appropriate when all of the following are true:
For example:
Both expose:
#734 also enforces that target provider trees expose the same module set, keeping target differences at the implementation layer rather than the public API layer.
SGLang DeviceOperator
When generation differences cross the standalone operator boundary, they should be handled in SGLang.
For example:
In this case, the Memory Pool must allocate a different representation before the kernel executes. A kernel provider alone cannot hide the difference.
The proposed responsibility is:
DeviceOperatorshould not own:In short:
Representative Cases
Gemma RMSNorm
→
sgl-kernel-nputarget provider#638 + #734provide the reference implementation.
MXFP scale layout
If normalization is operator-local:
→
sgl-kernel-npuIf the representation is stored and consumed across multiple runtime components:
→ SGLang
DeviceOperatorKV / Sparse Attention
If generation differences affect:
→ SGLang
DeviceOperatorThe concrete standalone kernels used by that path remain in:
Rollout
The two repositories can evolve independently under the same ownership model.
sgl-kernel-npu
After #734, adding a new cross-generation operator only requires:
target_providers/<target>/directories;No generic build-system changes should be required.
SGLang
Introduce a minimal
DeviceOperatorand migrate only existing runtime-visible differences, such as:This RFC does not propose wrapping every NPU operator.
Acceptance Criteria
The intended long-term rules are:
sgl-kernel-npuwhenever possible.sgl-kernel-npuexposes stable semantic/module contracts.DeviceOperatoronly handles runtime-visible device contracts.Open Questions
DeviceOperatormigration to validate the abstraction?References
device_op.py— prior art for runtime device adaptation