Skip to content

[NPU] Generalize target-specific operator provider staging - #734

Open
wangyao-i wants to merge 28 commits into
sgl-project:mainfrom
wangyao-i:feat/pr638-continue
Open

wangyao-i wants to merge 28 commits into
sgl-project:mainfrom
wangyao-i:feat/pr638-continue

Conversation

@wangyao-i

Copy link
Copy Markdown
Contributor

Motivation

Ascend generations may expose different low-level implementations for the same semantic operator.

PR #638 introduces build-time provider selection for Gemma RMSNorm, keeping SoC-specific operator selection inside sgl-kernel-npu while exposing a stable API to SGLang. However, the current staging logic is specific to Gemma RMSNorm: setup.py explicitly knows its provider files and how to select them.

As more cross-generation operator differences are introduced, repeating this pattern would make the build system increasingly operator-aware.

This PR generalizes the existing mechanism into a lightweight target-specific provider staging layer. Provider implementations are organized by target and stable module path. The build system selects one target provider tree while building the wheel, so adding another portable cross-generation operator does not require changes to setup.py or runtime SoC dispatch.

Design

The core principle: Build system knows targets, but does not know operators.

  • Directory layout is the registry. target_providers/<target>/<module>.py — the relative module path after the target directory is the registration key. No PROVIDERS dict, no explicit register() call:
target_providers/
├── Ascend910/
│   └── norm/gemma_rmsnorm.py
└── Ascend950/
    └── norm/gemma_rmsnorm.py

Both become sgl_kernel_npu/norm/gemma_rmsnorm.py in the wheel — the stable public API SGLang already consumes ((output, rstd) contract unchanged).

  • Provider selection is a build-time decision. build.sh keeps normalizing the SoC into SGL_KERNEL_NPU_BUILD_TARGET (Ascend910/Ascend950, unchanged from ✨ [NPU] Add portable Gemma RMSNorm API #638). build_tools/target_provider.py (~70 lines) validates the target, scans the provider tree, and stages every .py into build_lib/sgl_kernel_npu/ by relative path. It does exactly three things: validate target → find provider files → stage files.

  • Fail loudly, never fall back. Unknown target, missing provider directory, or a provider colliding with an existing common module all fail the build instead of silently falling back at runtime. There is no runtime dispatch, no registry, no fallback — if the wheel target is decided, a missing provider is a build configuration error.

  • Source tree ships no provider module. As in ✨ [NPU] Add portable Gemma RMSNorm API #638, norm/gemma_rmsnorm.py only exists inside a built wheel; a source checkout (or pip install -e .) raises ImportError rather than silently defaulting to one target's operator.

Key changes

  • Add build_tools/target_provider.py: generic stage_target_providers() + collect_provider_modules(); no operator names anywhere in the build framework
  • Move the Gemma providers: norm/_gemma_rmsnorm_native.py → target_providers/Ascend910/norm/gemma_rmsnorm.py, norm/_gemma_rmsnorm_aclnn.py → target_providers/Ascend950/norm/gemma_rmsnorm.py (git detects both as renames; implementations unchanged)
  • Rewrite TargetBuildPy in setup.py: drop GEMMA_PROVIDERS and all Gemma-specific copy/unlink logic; it now only reads SGL_KERNEL_NPU_BUILD_TARGET and calls stage_target_providers()
  • Exclude target_providers from find_namespace_packages so provider sources stay build input and never land in the wheel
  • SGL_KERNEL_NPU_BUILD_TARGET is now required when building a wheel (previously defaulted to Ascend910); build.sh always exports it, so the standard build flow is unaffected
  • CI enforces provider symmetry: target_providers/Ascend910/ and target_providers/Ascend950/ must expose the identical module set

Adding a new cross-generation operator

No build-framework changes needed — just:

  1. Confirm it qualifies: same semantic API + different target implementation + same input/output contract
  2. Add target_providers/Ascend910/<module>.py and target_providers/Ascend950/<module>.py with identical public signatures/returns
  3. Add a contract test (mock torch_npu) and the symmetry test picks up the new paths automatically

Testing

  • New tests/python/sgl_kernel_npu/test_target_provider.py: real-tree staging per target (910 → npu_gemma_rms_norm, 950 → npu_rms_norm with 1.0 + weight), unknown target / missing directory / common-module conflict all rejected, provider symmetry enforced
  • Updated test_build_targets.py: TargetBuildPy staging end-to-end, wheel content (has norm/gemma_rmsnorm.py, no target_providers/), missing env var rejected, package exclusion verified; build.sh SoC tests untouched
  • Updated test_gemma_rmsnorm_provider.py: provider paths now under target_providers/, mock contract unchanged
  • All three suites: 15 passed, 9 skipped (POSIX-shell cases on Windows), pre-commit clean

TallMessiWu and others added 28 commits July 31, 2026 15:35
1. Canonicalize every A5 spelling (950, Ascend950, Ascend950PR_*, Ascend950DT_*) onto Ascend950; they were accepted then discarded, since the C++ bundle compiles against the 910C compatibility target either way
2. Drop the is_a5_soc_version helper for a plain equality check and merge the two adjacent identical if-blocks
3. Document the four aliases the script actually accepts (910B/910/910C/950)
4. Rename gemma_rmsnorm.py to _gemma_rmsnorm_native.py so the source tree ships no staged provider; a source or editable install now fails loudly instead of silently defaulting to the 910 operator on A5
5. Record the bf16 1.0+weight trade-off in both provider docstrings
6. Drop the guard asserting absence of never-shipped draft symbols, and the A5-vacuous npu_rms_norm equivalence test
7. Run the two mock-only build-target suites in lint CI; they needed no NPU but nothing executed them
1. normalize_soc_version only folds the documented shorthands and now lets any other value through, so -a kernels Ascend910B2 / Ascend910B4 / Ascend910_9391 / ascend910_93 reach the AscendC compiler exactly as they did before; the allow-list introduced with the alias table silently turned this pass-through into a hard error
2. Restore the deepep three-SoC check that the allow-list had been standing in for, and apply it to 'all' as well since that target builds deep_ep too
3. Document in the help text that kernels accepts concrete compiler targets while all/deepep do not
1. all/kernels now call detect_soc_version instead of hardcoding Ascend910_9382, so an A5 host builds the ACLNN Gemma wheel without anyone remembering to pass 950; deepep already did this
2. Rename detect_deepep_soc_version and its messages, which are no longer deepep-specific
3. Hosts without npu-smi keep falling back to Ascend910_9382, so release containers produce byte-identical artifacts
4. Fall back rather than abort when npu-smi reports an unrecognized chip: all/kernels never had a failure path here. deepep still aborts via a strict flag, since it picks its ops variant from the SoC and guessing wrong is worse than stopping
5. Cover detection per chip by running build.sh against a faked npu-smi; skipped on Windows, where bash resolves to WSL and cannot see the repo path
1. Restore the deepep usage line and the Chip mapping block to their upstream wording; both were already accurate and my rewrites carried no new information
2. Keep the kernels usage line updated, since that target genuinely gained auto-detection and the old text no longer describes it
3. Condense the alias block to what the new behaviour actually requires
Replace the Gemma-specific GEMMA_PROVIDERS staging in setup.py with a generic target_providers/<target>/ tree staged by build_tools. The relative module path is the registration key, so the build system knows targets but not operators: adding a cross-generation operator is now just two provider files plus tests, with no build-framework changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants