From 6c91af95837dfd39af1cdacab99ab22ae7c3a2f4 Mon Sep 17 00:00:00 2001
From: dabuliu123 <270334047@qq.com>
Date: Fri, 21 Aug 2026 11:50:54 +0800
Subject: [PATCH 01/16] Merge branch 'wqw_base_lw_dev_0808' into
ascend-dev-0808
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
feat(docker): upgrade NPU env to CANN 9.0.0 with FLA and MindSpeed-Ops
# ⭐ Feature
## Upgrade CANN base image and toolchain
- Upgrade CANN base image from 8.5.1-a3 to 9.0.0-a3
- Upgrade torch_npu from v2.9.0-7.3.0 to v26.0.1-pytorch2.9.0
- Upgrade triton-ascend from 3.2.0 to 3.2.1
## Add AscendC Flash Linear Attention (FLA) support
- Clone and build fla_npu from flash-linear-attention-npu v26.1.0
- Compile causal_conv1d and gated_delta_rule ops for ascend910_93
## Add MindSpeed-Ops support
- Clone, checkout and install MindSpeed-Ops
- Add mindspeed-ops.patch for gated_delta_rule autotune key fix
---
# ♻️ Refactor
## Restructure Dockerfile build order
- Move torch/torch_npu install before repo clone
- Delay COPY . /root/Relax to just before patching
- Consolidate Megatron-Bridge into Megatron-LM via cp instead of separate path
- Install MindSpeed-Bridge with --no-deps to avoid circular dependency
## Migrate mindspeed-bridge to FLA ops
- Replace causal_conv1d from causal_conv1d with FLA implementation
- Replace mindspeed_ops l2norm with naive_l2norm fallback
- Switch to flash_gated_delta_rule when FLA is available
---
# 🐛 Bug Fix
## Fix various compatibility issues
- Fix autotune key in gated_delta_rule kernel by removing USE_G and IS_VARLEN
- Fix lambda closure bug in transformer_config_init_subclass (use default arg binding)
- Fix attention_mask dtype from int32 to bool in model preprocess
- Fix vision model config with MoE permute fusion disabled
- Handle OmegaConf DictConfig/ListConfig in remove_non_pickleables
- Guard apex MixedFusedLayerNorm import with is_npu_available check
## Update sgl-kernel-npu
- Upgrade sgl-kernel-npu checkout from 2026.04.15.rc3 to 2026.7.2
- Remove obsolete cherry-pick workaround
feat(docker): upgrade NPU env to CANN 9.0.0 with FLA and MindSpeed-Ops
feat(docker): upgrade NPU env to CANN 9.0.0 with FLA and MindSpeed-Ops
# ⭐ Feature
## Upgrade CANN base image and toolchain
- Upgrade CANN base image from 8.5.1-a3 to 9.0.0-a3
- Upgrade torch_npu from v2.9.0-7.3.0 to v26.0.1-pytorch2.9.0
- Upgrade triton-ascend from 3.2.0 to 3.2.1
## Add AscendC Flash Linear Attention (FLA) support
- Clone and build fla_npu from flash-linear-attention-npu v26.1.0
- Compile causal_conv1d and gated_delta_rule ops for ascend910_93
## Add MindSpeed-Ops support
- Clone, checkout and install MindSpeed-Ops
- Add mindspeed-ops.patch for gated_delta_rule autotune key fix
---
# ♻️ Refactor
## Restructure Dockerfile build order
- Move torch/torch_npu install before repo clone
- Delay COPY . /root/Relax to just before patching
- Consolidate Megatron-Bridge into Megatron-LM via cp instead of separate path
- Install MindSpeed-Bridge with --no-deps to avoid circular dependency
## Migrate mindspeed-bridge to FLA ops
- Replace causal_conv1d from causal_conv1d with FLA implementation
- Replace mindspeed_ops l2norm with naive_l2norm fallback
- Switch to flash_gated_delta_rule when FLA is available
---
# 🐛 Bug Fix
## Fix various compatibility issues
- Fix autotune key in gated_delta_rule kernel by removing USE_G and IS_VARLEN
- Fix lambda closure bug in transformer_config_init_subclass (use default arg binding)
- Fix attention_mask dtype from int32 to bool in model preprocess
- Fix vision model config with MoE permute fusion disabled
- Handle OmegaConf DictConfig/ListConfig in remove_non_pickleables
- Guard apex MixedFusedLayerNorm import with is_npu_available check
## Update sgl-kernel-npu
- Upgrade sgl-kernel-npu checkout from 2026.04.15.rc3 to 2026.7.2
- Remove obsolete cherry-pick workaround
[NPU] update sglang
---
docker/Dockerfile.npu | 98 +-
docker/npu_patch/megatron-bridge.patch | 56 +-
docker/npu_patch/mindspeed-bridge.patch | 132 +-
docker/npu_patch/mindspeed-ops.patch | 13 +
docker/npu_patch/mindspeed.patch | 14 +
docker/npu_patch/sgl-kernel-npu.patch | 1707 +++++++++++++++++++++++
docker/npu_patch/sglang-npu.patch | 433 +++++-
7 files changed, 2330 insertions(+), 123 deletions(-)
create mode 100644 docker/npu_patch/mindspeed-ops.patch
create mode 100644 docker/npu_patch/sgl-kernel-npu.patch
diff --git a/docker/Dockerfile.npu b/docker/Dockerfile.npu
index 89ed93f0c..dc9b05197 100644
--- a/docker/Dockerfile.npu
+++ b/docker/Dockerfile.npu
@@ -2,7 +2,7 @@
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
-FROM quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11
+FROM quay.io/ascend/cann:9.0.0-a3-ubuntu22.04-py3.11
ARG HTTP_PROXY
ARG HTTPS_PROXY
@@ -43,86 +43,112 @@ RUN ARCH=$(uname -m) && \
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/devlib/linux/x86_64/:$LD_LIBRARY_PATH; \
fi && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
- source /usr/local/Ascend/nnal/atb/set_env.sh
+ source /usr/local/Ascend/nnal/atb/set_env.sh && \
+ source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh
+
+
# Setting pip & git config. Global config (set once, persists across subsequent RUN layers)
ARG PIP_INDEX_URL="https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
RUN pip config set global.index-url ${PIP_INDEX_URL} && \
git config --global http.sslverify false && \
+ git config --global https.sslverify false && \
git config --global http.postBuffer 2147483648 && \
git config --global user.email "temp@example.com" && \
git config --global user.name "temp"
-WORKDIR /root
-COPY . /root/Relax
+# install torch
RUN pip install --upgrade pip packaging setuptools==80.10.2 && \
pip install torch==2.9.0 && \
pip install numpy==1.26.0
- # build torch_npu
+
+# build torch_npu
RUN pip install pyyaml && \
git clone https://gitcode.com/Ascend/pytorch.git /root/pytorch && \
cd /root/pytorch && \
- git checkout v2.9.0-7.3.0 && \
+ git checkout v26.0.1-pytorch2.9.0 && \
git cherry-pick -n f495de675bce38a2fa21edbf067b73d2a5f26733 && \
bash ci/build.sh --python=3.11 && \
- pip install dist/torch_npu-2.9.0*.whl
-RUN cd /root && rm -rf /root/pytorch && \
- pip install triton-ascend==3.2.0 && \
+ pip install dist/torch_npu*.whl
+
+# install triton/TQ
+RUN pip install triton-ascend==3.2.1 --extra-index-url=https://triton-ascend.osinfra.cn/pypi/simple && \
pip install tensordict==0.10.0 pyvers==0.1.0 --no-deps && \
pip install "transferqueue @ git+https://github.com/redai-infra/TransferQueue.git@58054a33834aadbcf76aacd6b1e32e25c030f2c9" --no-deps
- # Clone Megatron-LM, MindSpeed, MindSpeed-Bridge, Megatron-Bridge and install
+# Clone Megatron-LM, MindSpeed, MindSpeed-Bridge, Megatron-Bridge and install
RUN git clone https://gitcode.com/ascend/MindSpeed.git /root/MindSpeed && \
git clone https://github.com/NVIDIA/Megatron-LM.git /root/Megatron-LM && \
+ git clone https://gitcode.com/ascend/MindSpeed-Ops.git /root/MindSpeed-Ops && \
git clone https://gitcode.com/ascend/MindSpeed-Bridge.git /root/MindSpeed-Bridge && \
git clone https://github.com/NVIDIA-NeMo/Megatron-Bridge.git /root/Megatron-Bridge
-RUN cd /root/MindSpeed && git checkout core_r0.16.0 && pip install -r requirements.txt && pip install -e . && \
+RUN cd /root/MindSpeed && git checkout core_r0.16.0 && pip install -r requirements.txt && pip install -e . && git checkout e4772499 && \
cd /root/Megatron-LM && git checkout core_v0.16.1 && pip install -e . --no-build-isolation && \
- cd /root/Megatron-Bridge && git checkout v0.3.1 && \
- cd /root/MindSpeed-Bridge && git checkout 3655c07cbcc9 && pip install -r requirements.txt && bash tools/install_auto.sh
-
-# Patch Megatron-LM, MindSpeed, MindSpeed-Bridge
-RUN cd /root/MindSpeed && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed.patch && \
- cd /root/Megatron-Bridge && \
- patch -p1 < /root/Relax/docker/npu_patch/megatron-bridge.patch && \
+ cd /root/Megatron-Bridge && git checkout v0.3.1 && cp -r /root/Megatron-Bridge/src/megatron/bridge /root/Megatron-LM/megatron/ && \
+ cd /root/MindSpeed-Ops/ && git checkout 33ac80f7 && pip install -e . --no-build-isolation --no-deps && \
+ cd /root/MindSpeed-Bridge/ && git checkout v0.3.1 && pip install -r requirements.txt && pip install -e . --no-deps
+
+
+COPY . /root/Relax
+# Patch Megatron-LM, MindSpeed, MindSpeed-Bridge, MindSpeed-Ops
+RUN cd /root/Megatron-Bridge && \
+ patch -p1 < /root/Relax/docker/npu_patch/megatron-bridge.patch && \
cd /root/Megatron-LM && \
- patch -p1 < /root/Relax/docker/npu_patch/megatron.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/megatron.patch && \
cd /root/MindSpeed-Bridge && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed-bridge.patch
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed-bridge.patch && \
+ cd /root/MindSpeed-Ops && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed-ops.patch && \
+ cd /root/MindSpeed && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed.patch
-# Copy MindSpeed-Bridge, Megatron-Bridge into Megatron-LM
-RUN cp -r /root/MindSpeed-Bridge/mindspeed_bridge /root/Megatron-LM/ && \
- cp -r /root/Megatron-Bridge/src/megatron/bridge /root/Megatron-LM/megatron/ && \
- cd /root && rm -rf /root/MindSpeed-Bridge && rm -rf /root/Megatron-Bridge
# Install sglang
RUN git clone https://github.com/sgl-project/sglang.git /root/sglang && \
- cd /root/sglang && git checkout v0.5.10 && \
+ cd /root/sglang && git checkout v0.5.15 && \
mv python/pyproject.toml python/pyproject.toml.backup && \
mv python/pyproject_npu.toml python/pyproject.toml && \
pip install -e "python[srt_npu]" --constraint <(echo "torch==2.9.0") && \
- # [NPU] Fix Qwen3.5 inference acc.
- git stash && \
- git fetch origin pull/23815/head:pr-23815 && \
- git checkout pr-23815 && \
- patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch
+ # patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch
+ git add . && git commit -m "install info" && \
+ git fetch && \
+ git cherry-pick ece02ffc9cc32e94382d4f1b553b2c755f83f722 && \
+ git am /root/Relax/docker/npu_patch/sglang-npu.patch
+
# Install sgl-kernle-npu
RUN git clone https://github.com/sgl-project/sgl-kernel-npu /root/sgl-kernel-npu && \
- cd /root/sgl-kernel-npu && git checkout 2026.04.15.rc3 && \
+ cd /root/sgl-kernel-npu && git checkout 2026.7.2 && \
# Adapt tms for colocate train.
- git cherry-pick -n 23519771d347 --no-gpg-sign && \
- patch -p1 < /root/Relax/docker/npu_patch/torch-memory-saver.patch && \
- bash build.sh -a kernels && bash build.sh -a memory-saver && \
+ git am /root/Relax/docker/npu_patch/sgl-kernel-npu.patch && \
+ bash build.sh && \
pip install output/*.whl && \
- cd /root && rm -rf /root/sgl-kernel-npu
+ cd /root
+
+# Install AscendC FLA
+RUN git clone https://github.com/flashserve/flash-linear-attention-npu.git /root/flash-linear-attention-npu && \
+ cd /root/flash-linear-attention-npu && git checkout v26.1.0 && \
+ apt update && apt install gawk && \
+ # 编译命令,注意--soc=${soc_version}需要指定为当前机器的芯片类型{ascend910b/ascend910_93/ascend950}
+ bash build.sh --soc=ascend910_93 --pkg --ops=causal_conv1d,chunk_bwd_dv_local,chunk_bwd_dqkwg,chunk_gated_delta_rule_bwd_dhu,prepare_wy_repr_bwd_da,prepare_wy_repr_bwd_full,chunk_fwd_o,chunk_gated_delta_rule_fwd_h,recurrent_gated_delta_rule,recompute_wu_fwd && \
+ # 安装run包
+ ./build_out/cann-*.run && \
+ source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
+ source /usr/local/Ascend/nnal/atb/set_env.sh && \
+ source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh && \
+ # 一键编译安装脚本,先调用torchnpugen自动接入算子,再运行setup编whl包,最后安装whl包
+ cd torch_custom/fla_npu && bash build.sh
+
+
# Install Relax
+# git clone https://github.com/redai-infra/Relax.git
RUN cd /root/Relax && pip install -e .
+
RUN pip install ray==2.55.1 && pip install protobuf==6.33.6
#Clean cache
RUN pip cache purge && \
rm -rf /tmp/*
+
diff --git a/docker/npu_patch/megatron-bridge.patch b/docker/npu_patch/megatron-bridge.patch
index 0066df63b..d49be66b3 100644
--- a/docker/npu_patch/megatron-bridge.patch
+++ b/docker/npu_patch/megatron-bridge.patch
@@ -1,11 +1,32 @@
diff --git a/src/megatron/bridge/models/conversion/utils.py b/src/megatron/bridge/models/conversion/utils.py
-index 5a66e719..3d411c17 100644
+index 86ddf8661..115624b16 100644
--- a/src/megatron/bridge/models/conversion/utils.py
+++ b/src/megatron/bridge/models/conversion/utils.py
-@@ -203,6 +203,15 @@ def remove_non_pickleables(obj, max_depth: int = 3, current_depth: int = 0):
+@@ -203,6 +203,17 @@ def remove_non_pickleables(obj, max_depth: int = 3, current_depth: int = 0):
+ ): # bound methods
+ return None
+
++ # Convert OmegaConf containers to plain dict/list to avoid in-place
++ # mutation triggering "dictionary changed size during iteration" errors
++ # inside OmegaConf's internal _flags_cache handling.
++ try:
++ from omegaconf import DictConfig, ListConfig, OmegaConf as _OmegaConf
++
++ if isinstance(obj, (DictConfig, ListConfig)):
++ obj = _OmegaConf.to_container(obj, resolve=True)
++ except ImportError:
++ pass
++
+ # Handle dataclass/object with attributes
+ if hasattr(obj, "__dict__"):
+ # Create a copy to avoid modifying the original
+@@ -213,9 +224,18 @@ def remove_non_pickleables(obj, max_depth: int = 3, current_depth: int = 0):
# Recursively clean attribute
cleaned_value = remove_non_pickleables(attr_value, max_depth, current_depth + 1)
+-
+- # Set the cleaned value (or None if it was removed)
+- setattr(cleaned_obj, attr_name, cleaned_value)
+ if hasattr(cleaned_obj, '__setattr__'):
+ try:
+ setattr(cleaned_obj, attr_name, cleaned_value)
@@ -15,6 +36,33 @@ index 5a66e719..3d411c17 100644
+ print(f"Skipping attribute '{attr_name}' due to Union type")
+ continue
+ raise
++ else:
++ # Fallback for objects without __setattr__ override
++ setattr(cleaned_obj, attr_name, cleaned_value)
+
+ return cleaned_obj
+
+diff --git a/src/megatron/bridge/peft/utils.py b/src/megatron/bridge/peft/utils.py
+index 1ca5b18bd..4797e2e42 100644
+--- a/src/megatron/bridge/peft/utils.py
++++ b/src/megatron/bridge/peft/utils.py
+@@ -33,6 +33,7 @@ from megatron.core.transformer.moe.router import TopKRouter
+
+ from megatron.bridge.utils.import_utils import safe_import_from
+
++from relax.utils.device import is_npu_available
+
+ TEColumnParallelLinear, HAVE_TE_COL_LINEAR = safe_import_from(
+ "megatron.core.extensions.transformer_engine", "TEColumnParallelLinear"
+@@ -62,7 +63,10 @@ HAVE_TE = all(
+ )
+ )
+
+-MixedFusedLayerNorm, HAVE_APEX = safe_import_from("apex.normalization.fused_layer_norm", "MixedFusedLayerNorm")
++if is_npu_available:
++ MixedFusedLayerNorm, HAVE_APEX = None, False
++else:
++ MixedFusedLayerNorm, HAVE_APEX = safe_import_from("apex.normalization.fused_layer_norm", "MixedFusedLayerNorm")
- # Set the cleaned value (or None if it was removed)
- setattr(cleaned_obj, attr_name, cleaned_value)
+ TECL = (TEColumnParallelLinear, TELayerNormColumnParallelLinear, TEColumnParallelGroupedLinear)
+ TERL = (TERowParallelLinear, TERowParallelGroupedLinear)
diff --git a/docker/npu_patch/mindspeed-bridge.patch b/docker/npu_patch/mindspeed-bridge.patch
index 327ff7afa..11d7b4924 100644
--- a/docker/npu_patch/mindspeed-bridge.patch
+++ b/docker/npu_patch/mindspeed-bridge.patch
@@ -19,64 +19,98 @@ index 99ccba5..8fa07a1 100644
# Set the cleaned value (or None if it was removed)
setattr(cleaned_obj, attr_name, cleaned_value)
diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
-index 818af29..ef97078 100644
+index 19a3b9d..1417c33 100644
--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
+++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
-@@ -191,7 +191,7 @@ class GatedDeltaNet(MegatronModule):
+@@ -37,12 +37,11 @@ from megatron.core.ssm.gated_delta_net import (
+ )
+
+ try:
+- from causal_conv1d import causal_conv1d
++ import fla_npu
++ from fla.modules.convolution import causal_conv1d
+ except ImportError:
+ causal_conv1d = None
+- causal_conv1d_update = None
+
+-from mindspeed_ops.api.triton.l2norm import l2norm
+ from mindspeed_bridge.models.qwen_vl.modelling_qwen3_vl.chunk_gated_delta_rule import (
+ torch_chunk_gated_delta_rule,
+ )
+@@ -59,6 +58,12 @@ except ImportError:
+
+ from mindspeed_bridge.models.qwen_vl.modelling_qwen3_vl.flash_gated_delta_rule import flash_gated_delta_rule
+
++def naive_l2norm(x: torch.FloatTensor, dim: int = -1, eps: float = 1e-6):
++ """This function is intended to align with the l2norm implementation in the FLA library."""
++ original_dtype = x.dtype
++ inv_norm = torch.rsqrt((x * x).sum(dim=dim, keepdim=True) + eps)
++ # Counteract verl's autocast promotion (bf16 -> fp32) by restoring original dtype
++ return (x * inv_norm).to(original_dtype)
+
+ class GatedDeltaNet(MegatronModule):
+ """Gated Delta Net (GDN) layer class
+@@ -191,8 +196,8 @@ class GatedDeltaNet(MegatronModule):
)
setattr(self.A_log, "tensor_model_parallel", True)
- if HAVE_FLA and self.use_triton_gdn:
+- self.gated_delta_rule = chunk_gated_delta_rule
+ if HAVE_FLA:
- self.gated_delta_rule = chunk_gated_delta_rule
++ self.gated_delta_rule = flash_gated_delta_rule
elif self.use_ascend_gdn:
self.gated_delta_rule = flash_gated_delta_rule
-diff --git a/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py b/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py
-index d495c47..f08ea49 100644
---- a/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py
-+++ b/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py
-@@ -402,22 +402,6 @@ class Qwen35VLModelProvider(GPTModelProvider):
- self.vision_config = Qwen3_5VisionConfig()
- super().__post_init__()
+ else:
+@@ -474,7 +479,7 @@ class GatedDeltaNet(MegatronModule):
+ beta=beta,
+ initial_state=None,
+ output_final_state=False,
+- use_qk_l2norm_in_kernel=True,
++ use_qk_l2norm_in_kernel=False,
+ cu_seqlens=cu_seqlens_q,
+ )
+ nvtx_range_pop(suffix="gated_delta_rule")
+@@ -555,7 +560,7 @@ class GatedDeltaNet(MegatronModule):
-- def finalize(self) -> None:
-- self.validate_parallelism()
-- super().finalize()
--
-- def validate_parallelism(self):
-- """Validate that parallelism settings are compatible with this model's architecture.
--
-- Call this after mutating parallelism attributes (e.g. tensor_model_parallel_size)
-- on an already-constructed provider, since finalize() only runs once before provide().
-- """
-- if self.num_query_groups < self.tensor_model_parallel_size:
-- raise ValueError(
-- f"TP size {self.tensor_model_parallel_size} should be less than or equal to "
-- f"num_query_groups {self.num_query_groups}. Please use a smaller TP size."
-- )
--
- def provide(self, pre_process=None, post_process=None, vp_stage=None) -> Qwen3VLModel:
- """Provide a Qwen3.5 VL dense model instance with vision and language components."""
- language_transformer_config = self
-@@ -595,21 +579,6 @@ class Qwen35VLMoEModelProvider(GPTModelProvider):
- self.vision_config = Qwen3_5MoeVisionConfig()
- super().__post_init__()
+ # Apply L2 norm to query and key
+ if self.use_qk_l2norm:
+- query_key = l2norm(query_key.contiguous())
++ query_key = naive_l2norm(query_key.contiguous())
-- def finalize(self) -> None:
-- self.validate_parallelism()
-- super().finalize()
--
-- def validate_parallelism(self):
-- """Validate that parallelism settings are compatible with this model's architecture.
--
-- Call this after mutating parallelism attributes (e.g. tensor_model_parallel_size)
-- on an already-constructed provider, since finalize() only runs once before provide().
-- """
-- if self.num_query_groups < self.tensor_model_parallel_size:
-- raise ValueError(
-- f"TP size {self.tensor_model_parallel_size} should be less than or equal to "
-- f"num_query_groups {self.num_query_groups}. Please use a smaller TP size."
-- )
+ # Split query and key
+ split_size = self.qk_dim_local_tp // self.key_head_dim // self.cp_size
+diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/model.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/model.py
+index a6f310a..d468fa5 100644
+--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/model.py
++++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/model.py
+@@ -501,7 +501,7 @@ class Qwen3VLModel(MegatronModule):
+ combined_embeddings = split_data_cp_rank(combined_embeddings, cp_size, 0, cp_rank)
+ if packed_seq_params is not None:
+ if attention_mask is None:
+- attention_mask = torch.ones_like(input_ids, dtype=torch.int32, device=input_ids.device)
++ attention_mask = torch.ones_like(input_ids, dtype=torch.bool, device=input_ids.device)
+ input_ids_thd, _ = preprocess_packed_seqs(
+ input_ids,
+ attention_mask,
+@@ -560,7 +560,7 @@ class Qwen3VLModel(MegatronModule):
+ # convert lm_input_ids to THD format so it matches position_ids.
+ if packed_seq_params is not None:
+ if attention_mask is None:
+- attention_mask = torch.ones_like(input_ids, dtype=torch.int32, device=input_ids.device)
++ attention_mask = torch.ones_like(input_ids, dtype=torch.bool, device=input_ids.device)
+ lm_input_ids, _ = preprocess_packed_seqs(
+ input_ids,
+ attention_mask,
+diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py
+index 5d03f85..2488ef7 100644
+--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py
++++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py
+@@ -56,6 +56,8 @@ def get_vision_model_config(hf_config, megatron_config=None):
+ ffn_hidden_size=hf_config.intermediate_size,
+ add_bias_linear=True,
+ add_qkv_bias=True,
++ moe_permute_fusion=False, # ← 新增:vision model 没有 MoE,不需要 permute fusion
++ use_fused_moe_token_permute_and_unpermute=False, # ← 新增:同上
+ )
- def provide(self, pre_process=None, post_process=None, vp_stage=None) -> Qwen3VLModel:
- """Provide a Qwen3.5 VL model instance with vision and language components.
+ # apply text model config to vision model config
diff --git a/docker/npu_patch/mindspeed-ops.patch b/docker/npu_patch/mindspeed-ops.patch
new file mode 100644
index 000000000..9dae51dd4
--- /dev/null
+++ b/docker/npu_patch/mindspeed-ops.patch
@@ -0,0 +1,13 @@
+diff --git a/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py b/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py
+index bf62b0b..1925eca 100644
+--- a/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py
++++ b/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py
+@@ -21,7 +21,7 @@ from mindspeed_ops.api.triton.utils import prepare_chunk_indices, prepare_chunk_
+ )
+ @triton.autotune(
+ configs=get_autotune_config(multibuffer_list=(True, False)),
+- key=['H', 'K', 'V', 'BT', 'BV', 'USE_G', 'IS_VARLEN'],
++ key=['H', 'K', 'V', 'BT', 'BV'],
+ )
+ @triton.jit(do_not_specialize=['T'])
+ def chunk_gated_delta_rule_bwd_kernel_dhu_blockdim64(
diff --git a/docker/npu_patch/mindspeed.patch b/docker/npu_patch/mindspeed.patch
index 9f5f056ae..61c0c21e9 100644
--- a/docker/npu_patch/mindspeed.patch
+++ b/docker/npu_patch/mindspeed.patch
@@ -37,6 +37,20 @@ index c624fd0a..cef668af 100644
return parser
+diff --git a/mindspeed/core/megatron_basic/arguments_basic.py b/mindspeed/core/megatron_basic/arguments_basic.py
+index 87d71f81..aa6c219b 100644
+--- a/mindspeed/core/megatron_basic/arguments_basic.py
++++ b/mindspeed/core/megatron_basic/arguments_basic.py
+@@ -147,7 +147,7 @@ def transformer_config_init_subclass(cls, **kwargs):
+ if callable(value) and not isinstance(value, type):
+ value = field(default_factory=value)
+ elif type(value) in mutable_types:
+- value = field(default_factory=lambda: value)
++ value = field(default_factory=lambda v=value: v)
+ else:
+ value = value
+ setattr(cls, key, value)
+\ No newline at end of file
diff --git a/mindspeed/features_manager/functional/profile.py b/mindspeed/features_manager/functional/profile.py
index 6450b41c..86584015 100644
--- a/mindspeed/features_manager/functional/profile.py
diff --git a/docker/npu_patch/sgl-kernel-npu.patch b/docker/npu_patch/sgl-kernel-npu.patch
new file mode 100644
index 000000000..730580178
--- /dev/null
+++ b/docker/npu_patch/sgl-kernel-npu.patch
@@ -0,0 +1,1707 @@
+From 1732669a033f5fe5aa178c305ea1e4ba379fdca1 Mon Sep 17 00:00:00 2001
+From: wuqiwei
+Date: Wed, 5 Aug 2026 12:32:43 +0000
+Subject: [PATCH 1/3] torch.npu.empty_cache()
+
+---
+ .../torch_memory_saver/python/torch_memory_saver/entrypoint.py | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py b/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
+index b14c561..146c011 100644
+--- a/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
++++ b/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
+@@ -159,6 +159,7 @@ class _TorchMemorySaverImpl:
+ # only be released after the memory region is resumed and empty_cache() is invoked.
+ torch_npu._C._npu_releasePool(torch.npu.current_device(), pool.id)
+ del pool
++ torch.npu.empty_cache()
+ finally:
+ self._binary_wrapper.cdll.tms_set_interesting_region(True)
+
+@@ -176,3 +177,4 @@ def _sanity_checks():
+ raise RuntimeError(
+ "TorchMemorySaver is disabled for the current process because expandable_segments is not supported yet."
+ )
++
+\ No newline at end of file
+--
+2.34.1
+
+
+From 2aca54d20695004a4a8c7222d333fadd78d4d589 Mon Sep 17 00:00:00 2001
+From: cl-vv-h
+Date: Sat, 25 Jul 2026 09:21:13 +0000
+Subject: [PATCH 2/3] feat: add non-greedy MTP sampling kernels
+
+---
+ .../sgl_kernel_npu/sample/__init__.py | 13 +
+ .../sample/chain_speculative_sampling.py | 354 +++++++++++
+ .../sgl_kernel_npu/sample/probability.py | 40 ++
+ .../tree_speculative_sampling_target_only.py | 383 +++++++++++
+ .../test_chain_speculative_sampling.py | 138 ++++
+ .../test_speculative_probability.py | 31 +
+ ...t_tree_speculative_sampling_target_only.py | 595 ++++++++++++++++++
+ 7 files changed, 1554 insertions(+)
+ create mode 100644 python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py
+ create mode 100644 python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py
+ create mode 100644 python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py
+ create mode 100644 tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py
+ create mode 100644 tests/python/sgl_kernel_npu/test_speculative_probability.py
+ create mode 100644 tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py
+
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py
+index e69de29..6e7bbd6 100644
+--- a/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py
+@@ -0,0 +1,13 @@
++from sgl_kernel_npu.sample.chain_speculative_sampling import (
++ chain_speculative_sampling_rejection,
++)
++from sgl_kernel_npu.sample.probability import top_k_top_p_renorm_probs
++from sgl_kernel_npu.sample.tree_speculative_sampling_target_only import (
++ tree_speculative_sampling_target_only,
++)
++
++__all__ = [
++ "chain_speculative_sampling_rejection",
++ "top_k_top_p_renorm_probs",
++ "tree_speculative_sampling_target_only",
++]
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py
+new file mode 100644
+index 0000000..e8340da
+--- /dev/null
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py
+@@ -0,0 +1,354 @@
++import torch
++import triton
++import triton.language as tl
++
++
++@triton.jit
++def _chain_rejection_accept_kernel(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ target_probs,
++ draft_probs,
++ metadata,
++ num_draft_tokens: tl.constexpr,
++ num_speculative_tokens: tl.constexpr,
++ num_draft_prob_rows: tl.constexpr,
++ vocab_size: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ row_offset = req_idx * num_draft_tokens
++
++ cur_prob_row = tl.full((), 0, tl.int64)
++ last_accepted_idx = tl.load(retrive_index + row_offset).to(tl.int64)
++ num_accepted = 0
++ active = tl.full((), 1, tl.int32)
++
++ tl.store(accept_index + req_idx * num_speculative_tokens, last_accepted_idx)
++
++ # Linear Leviathan/Chen verification. Candidate 0 is the root; candidate
++ # step uses probability row step - 1 until a rejection terminates the chain.
++ for step in range(1, num_draft_tokens):
++ if active == 1:
++ draft_token = tl.load(candidates + row_offset + step).to(tl.int64)
++ target_offset = (
++ (row_offset + cur_prob_row) * vocab_size + draft_token
++ )
++ draft_offset = (
++ (req_idx * num_draft_prob_rows + cur_prob_row) * vocab_size
++ + draft_token
++ )
++ target_prob = tl.load(target_probs + target_offset).to(tl.float32)
++ draft_prob = tl.load(draft_probs + draft_offset).to(tl.float32)
++ coin = tl.load(uniform_samples + row_offset + step - 1).to(
++ tl.float32
++ )
++
++ if coin * draft_prob < target_prob:
++ tl.store(predicts + last_accepted_idx, draft_token)
++ num_accepted += 1
++ draft_idx = tl.load(retrive_index + row_offset + step).to(
++ tl.int64
++ )
++ tl.store(
++ accept_index
++ + req_idx * num_speculative_tokens
++ + num_accepted,
++ draft_idx,
++ )
++ last_accepted_idx = draft_idx
++ # Keep this loop-carried value int64 across both branches.
++ # Triton infers the constexpr loop variable `step` as int32.
++ cur_prob_row = tl.full((), step, tl.int64)
++ else:
++ active = 0
++
++ tl.store(accept_token_num + req_idx, num_accepted)
++
++ # metadata = [target row, output slot, all drafts accepted].
++ metadata_offset = req_idx * 3
++ tl.store(metadata + metadata_offset, cur_prob_row)
++ tl.store(metadata + metadata_offset + 1, last_accepted_idx)
++ tl.store(metadata + metadata_offset + 2, active)
++
++
++@triton.jit
++def _chain_rejection_block_sum_kernel(
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens: tl.constexpr,
++ num_draft_prob_rows: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ block_idx = tl.program_id(1)
++ vocab_offsets = block_idx * vocab_block_size + tl.arange(0, vocab_block_size)
++ vocab_mask = vocab_offsets < vocab_size
++
++ metadata_offset = req_idx * 3
++ target_row = tl.load(metadata + metadata_offset).to(tl.int64)
++ all_accepted = tl.load(metadata + metadata_offset + 2).to(tl.int32)
++ target_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target = tl.load(
++ target_probs + target_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++
++ if all_accepted == 1:
++ residual = target
++ else:
++ draft_offset = (
++ req_idx * num_draft_prob_rows + target_row
++ ) * vocab_size
++ draft = tl.load(
++ draft_probs + draft_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target - draft, 0.0)
++
++ tl.store(
++ block_sums + req_idx * num_vocab_blocks + block_idx,
++ tl.sum(residual, axis=0),
++ )
++
++
++@triton.jit
++def _chain_rejection_sample_kernel(
++ predicts,
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens: tl.constexpr,
++ num_draft_prob_rows: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++ pad_num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ metadata_offset = req_idx * 3
++ target_row = tl.load(metadata + metadata_offset).to(tl.int64)
++ output_idx = tl.load(metadata + metadata_offset + 1).to(tl.int64)
++ all_accepted = tl.load(metadata + metadata_offset + 2).to(tl.int32)
++
++ block_offsets = tl.arange(0, pad_num_vocab_blocks)
++ block_mask = block_offsets < num_vocab_blocks
++ sums = tl.load(
++ block_sums + req_idx * num_vocab_blocks + block_offsets,
++ mask=block_mask,
++ other=0.0,
++ ).to(tl.float32)
++ block_cdf = tl.cumsum(sums, axis=0)
++ total = tl.sum(sums, axis=0)
++ coin = tl.load(uniform_samples_for_final_sampling + req_idx).to(tl.float32)
++ target_value = coin * total
++
++ selected_block = tl.sum(
++ ((block_cdf <= target_value) & block_mask).to(tl.int32), axis=0
++ )
++ selected_block = tl.minimum(selected_block, num_vocab_blocks - 1)
++ prefix_sum = tl.sum(
++ tl.where(block_offsets < selected_block, sums, 0.0), axis=0
++ )
++ local_target = tl.maximum(target_value - prefix_sum, 0.0)
++
++ local_offsets = tl.arange(0, vocab_block_size)
++ vocab_offsets = selected_block * vocab_block_size + local_offsets
++ vocab_mask = vocab_offsets < vocab_size
++ target_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target = tl.load(
++ target_probs + target_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ if all_accepted == 1:
++ residual = target
++ else:
++ draft_offset = (
++ req_idx * num_draft_prob_rows + target_row
++ ) * vocab_size
++ draft = tl.load(
++ draft_probs + draft_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target - draft, 0.0)
++
++ local_cdf = tl.cumsum(residual, axis=0)
++ local_index = tl.sum(
++ ((local_cdf <= local_target) & vocab_mask).to(tl.int32), axis=0
++ )
++ last_valid_local = tl.max(
++ tl.where((residual > 0.0) & vocab_mask, local_offsets, -1), axis=0
++ )
++ valid_local_count = tl.minimum(
++ vocab_block_size,
++ vocab_size - selected_block * vocab_block_size,
++ )
++ sampled_local = tl.where(
++ local_index < valid_local_count,
++ local_index,
++ last_valid_local,
++ )
++ sampled_token = tl.where(
++ sampled_local >= 0,
++ selected_block * vocab_block_size + sampled_local,
++ vocab_size - 1,
++ )
++ tl.store(predicts + output_idx, tl.minimum(sampled_token, vocab_size - 1))
++
++
++def chain_speculative_sampling_rejection(
++ predicts: torch.Tensor,
++ accept_index: torch.Tensor,
++ accept_token_num: torch.Tensor,
++ candidates: torch.Tensor,
++ retrive_index: torch.Tensor,
++ retrive_next_token: torch.Tensor,
++ retrive_next_sibling: torch.Tensor,
++ uniform_samples: torch.Tensor,
++ uniform_samples_for_final_sampling: torch.Tensor,
++ target_probs: torch.Tensor,
++ draft_probs: torch.Tensor,
++ threshold_single: float = 1.0,
++ threshold_acc: float = 1.0,
++ deterministic: bool = True,
++) -> None:
++ """NPU kernel implementation of classic chain rejection sampling."""
++ del retrive_next_token, retrive_next_sibling
++ del threshold_single, threshold_acc, deterministic
++
++ if candidates.ndim != 2 or target_probs.ndim != 3:
++ raise ValueError("candidates must be 2-D and target_probs must be 3-D")
++ batch_size, num_draft_tokens = candidates.shape
++ if batch_size == 0:
++ return
++ if num_draft_tokens == 0:
++ raise ValueError("num_draft_tokens must be positive")
++ if target_probs.shape[:2] != (batch_size, num_draft_tokens):
++ raise ValueError(
++ "target_probs shape must be [batch, num_draft_tokens, vocab_size]"
++ )
++ if retrive_index.shape != candidates.shape:
++ raise ValueError("retrive_index shape must match candidates")
++ if accept_index.shape != candidates.shape:
++ raise ValueError(
++ "classic rejection sampling requires a topk=1 linear chain"
++ )
++ if accept_token_num.shape != (batch_size,):
++ raise ValueError("accept_token_num must have shape [batch]")
++ if predicts.ndim != 1:
++ raise ValueError("predicts must be 1-D")
++ if uniform_samples.shape != candidates.shape:
++ raise ValueError("uniform_samples shape must match candidates")
++ if uniform_samples_for_final_sampling.shape != (batch_size,):
++ raise ValueError(
++ "uniform_samples_for_final_sampling must have shape [batch]"
++ )
++ if draft_probs is None or draft_probs.ndim != 3:
++ raise ValueError("draft_probs must be a 3-D tensor")
++ if draft_probs.shape[0] != batch_size:
++ raise ValueError("draft_probs batch size must match candidates")
++ if draft_probs.shape[1] < max(num_draft_tokens - 1, 1):
++ raise ValueError("draft_probs does not contain every proposal row")
++ if draft_probs.shape[-1] != target_probs.shape[-1]:
++ raise ValueError("draft_probs and target_probs vocab sizes must match")
++ if target_probs.dtype != torch.float32 or draft_probs.dtype != torch.float32:
++ raise TypeError("target_probs and draft_probs must be torch.float32")
++ if uniform_samples.dtype != torch.float32:
++ raise TypeError("uniform_samples must be torch.float32")
++ if uniform_samples_for_final_sampling.dtype != torch.float32:
++ raise TypeError("uniform_samples_for_final_sampling must be torch.float32")
++ integer_dtypes = (
++ (predicts, torch.int32, "predicts"),
++ (accept_index, torch.int32, "accept_index"),
++ (accept_token_num, torch.int32, "accept_token_num"),
++ (candidates, torch.int64, "candidates"),
++ (retrive_index, torch.int64, "retrive_index"),
++ )
++ for tensor, expected_dtype, name in integer_dtypes:
++ if tensor.dtype != expected_dtype:
++ raise TypeError(f"{name} must be {expected_dtype}")
++ tensors = (
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ draft_probs,
++ )
++ if any(tensor.device != target_probs.device for tensor in tensors):
++ raise ValueError("all tensors must be on the same NPU device")
++ if any(not tensor.is_contiguous() for tensor in tensors):
++ raise ValueError("all tensors must be contiguous")
++
++ num_speculative_tokens = accept_index.shape[1]
++ num_draft_prob_rows = draft_probs.shape[1]
++ vocab_size = target_probs.shape[-1]
++ vocab_block_size = 2048
++ num_vocab_blocks = triton.cdiv(vocab_size, vocab_block_size)
++ pad_num_vocab_blocks = triton.next_power_of_2(num_vocab_blocks)
++
++ metadata = torch.empty(
++ (batch_size, 3), dtype=torch.int64, device=target_probs.device
++ )
++ block_sums = torch.empty(
++ (batch_size, num_vocab_blocks),
++ dtype=torch.float32,
++ device=target_probs.device,
++ )
++
++ _chain_rejection_accept_kernel[(batch_size,)](
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ target_probs,
++ draft_probs,
++ metadata,
++ num_draft_tokens=num_draft_tokens,
++ num_speculative_tokens=num_speculative_tokens,
++ num_draft_prob_rows=num_draft_prob_rows,
++ vocab_size=vocab_size,
++ )
++ _chain_rejection_block_sum_kernel[(batch_size, num_vocab_blocks)](
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens=num_draft_tokens,
++ num_draft_prob_rows=num_draft_prob_rows,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ )
++ _chain_rejection_sample_kernel[(batch_size,)](
++ predicts,
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens=num_draft_tokens,
++ num_draft_prob_rows=num_draft_prob_rows,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ pad_num_vocab_blocks=pad_num_vocab_blocks,
++ )
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py
+new file mode 100644
+index 0000000..cadddd0
+--- /dev/null
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py
+@@ -0,0 +1,40 @@
++import torch
++
++
++def top_k_top_p_renorm_probs(
++ probs: torch.Tensor,
++ top_ks: torch.Tensor,
++ top_ps: torch.Tensor,
++ need_top_k_sampling: bool,
++ need_top_p_sampling: bool,
++) -> torch.Tensor:
++ """Apply the same sequential top-k then top-p policy used by SGLang GPU."""
++ if not need_top_k_sampling and not need_top_p_sampling:
++ return probs
++
++ vocab_size = probs.shape[-1]
++ sorted_probs, sorted_indices = probs.sort(dim=-1, descending=True)
++
++ if need_top_k_sampling:
++ top_ks = top_ks.to(device=probs.device, dtype=torch.long).clamp(
++ min=1, max=vocab_size
++ )
++ positions = torch.arange(vocab_size, device=probs.device).view(1, -1)
++ sorted_probs.masked_fill_(positions >= top_ks.view(-1, 1), 0.0)
++ sorted_probs.div_(
++ sorted_probs.sum(dim=-1, keepdim=True).clamp_min_(1e-20)
++ )
++
++ if need_top_p_sampling:
++ top_ps = top_ps.to(device=probs.device, dtype=probs.dtype)
++ cumulative_probs = sorted_probs.cumsum(dim=-1)
++ sorted_probs.masked_fill_(
++ cumulative_probs - sorted_probs > top_ps.view(-1, 1), 0.0
++ )
++ sorted_probs.div_(
++ sorted_probs.sum(dim=-1, keepdim=True).clamp_min_(1e-20)
++ )
++
++ return torch.zeros_like(probs).scatter_(
++ dim=-1, index=sorted_indices, src=sorted_probs
++ )
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py
+new file mode 100644
+index 0000000..e34baab
+--- /dev/null
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py
+@@ -0,0 +1,383 @@
++import torch
++import triton
++import triton.language as tl
++
++
++@triton.jit
++def _tree_target_only_accept_kernel(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ target_probs,
++ rejected_probs,
++ metadata,
++ threshold_single,
++ threshold_acc,
++ num_draft_tokens: tl.constexpr,
++ num_speculative_tokens: tl.constexpr,
++ vocab_size: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ row_offset = req_idx * num_draft_tokens
++
++ cur_prob_row = tl.full((), 0, tl.int64)
++ cur_node = tl.full((), 0, tl.int64)
++ last_accepted_idx = tl.load(retrive_index + row_offset).to(tl.int64)
++ coin = tl.load(uniform_samples + row_offset).to(tl.float32)
++ num_accepted = 0
++ path_active = tl.full((), 1, tl.int32)
++
++ tl.store(accept_index + req_idx * num_speculative_tokens, last_accepted_idx)
++
++ # This is the same breadth-at-each-depth traversal used by the CUDA kernel:
++ # descend to the first child, then walk siblings until one is accepted.
++ for _depth in range(1, num_speculative_tokens):
++ accepted_at_depth = tl.full((), 0, tl.int32)
++ prob_acc = tl.full((), 0.0, tl.float32)
++
++ if path_active == 1:
++ cur_node = tl.load(
++ retrive_next_token + row_offset + cur_node
++ ).to(tl.int64)
++ if cur_node == -1:
++ path_active = 0
++
++ # The loop is bounded by the number of tree nodes. It terminates
++ # logically when a child is accepted or the sibling list reaches -1.
++ for _sibling in range(0, num_draft_tokens):
++ if (
++ (path_active == 1)
++ & (accepted_at_depth == 0)
++ & (cur_node != -1)
++ ):
++ draft_token = tl.load(
++ candidates + row_offset + cur_node
++ ).to(tl.int64)
++ draft_idx = tl.load(
++ retrive_index + row_offset + cur_node
++ ).to(tl.int64)
++ prob_offset = (
++ (row_offset + cur_prob_row) * vocab_size + draft_token
++ )
++ target_prob_single = tl.load(
++ target_probs + prob_offset
++ ).to(tl.float32)
++ prob_acc += target_prob_single
++
++ accepted = (coin <= prob_acc / threshold_acc) | (
++ target_prob_single >= threshold_single
++ )
++ if accepted:
++ tl.store(predicts + last_accepted_idx, draft_token)
++ num_accepted += 1
++ tl.store(
++ accept_index
++ + req_idx * num_speculative_tokens
++ + num_accepted,
++ draft_idx,
++ )
++ last_accepted_idx = draft_idx
++ cur_prob_row = cur_node
++ coin = tl.load(
++ uniform_samples + row_offset + cur_node
++ ).to(tl.float32)
++ accepted_at_depth = 1
++ else:
++ # The CUDA target-only kernel stores the rejected sibling's
++ # target probability in draft_probs and later samples from
++ # relu(target_probs - draft_probs).
++ tl.store(rejected_probs + prob_offset, target_prob_single)
++ cur_node = tl.load(
++ retrive_next_sibling + row_offset + cur_node
++ ).to(tl.int64)
++
++ if accepted_at_depth == 0:
++ path_active = 0
++
++ tl.store(accept_token_num + req_idx, num_accepted)
++
++ # metadata = [final target-probability row, final output slot].
++ metadata_offset = req_idx * 2
++ tl.store(metadata + metadata_offset, cur_prob_row)
++ tl.store(metadata + metadata_offset + 1, last_accepted_idx)
++
++
++@triton.jit
++def _tree_target_only_block_sum_kernel(
++ target_probs,
++ rejected_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ block_idx = tl.program_id(1)
++ vocab_offsets = block_idx * vocab_block_size + tl.arange(0, vocab_block_size)
++ vocab_mask = vocab_offsets < vocab_size
++
++ target_row = tl.load(metadata + req_idx * 2).to(tl.int64)
++ probs_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target = tl.load(
++ target_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ rejected = tl.load(
++ rejected_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target - rejected, 0.0)
++ block_sum = tl.sum(residual, axis=0)
++ tl.store(block_sums + req_idx * num_vocab_blocks + block_idx, block_sum)
++
++
++@triton.jit
++def _tree_target_only_sample_kernel(
++ predicts,
++ target_probs,
++ rejected_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++ pad_num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ metadata_offset = req_idx * 2
++ target_row = tl.load(metadata + metadata_offset).to(tl.int64)
++ output_idx = tl.load(metadata + metadata_offset + 1).to(tl.int64)
++
++ block_offsets = tl.arange(0, pad_num_vocab_blocks)
++ block_mask = block_offsets < num_vocab_blocks
++ sums = tl.load(
++ block_sums + req_idx * num_vocab_blocks + block_offsets,
++ mask=block_mask,
++ other=0.0,
++ ).to(tl.float32)
++ block_cdf = tl.cumsum(sums, axis=0)
++ total = tl.sum(sums, axis=0)
++ coin = tl.load(uniform_samples_for_final_sampling + req_idx).to(tl.float32)
++ target = coin * total
++
++ selected_block = tl.sum(
++ ((block_cdf <= target) & block_mask).to(tl.int32), axis=0
++ )
++ selected_block = tl.minimum(selected_block, num_vocab_blocks - 1)
++ prefix_sum = tl.sum(
++ tl.where(block_offsets < selected_block, sums, 0.0), axis=0
++ )
++ local_target = tl.maximum(target - prefix_sum, 0.0)
++
++ local_offsets = tl.arange(0, vocab_block_size)
++ vocab_offsets = selected_block * vocab_block_size + local_offsets
++ vocab_mask = vocab_offsets < vocab_size
++ probs_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target_probs_block = tl.load(
++ target_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ rejected_probs_block = tl.load(
++ rejected_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target_probs_block - rejected_probs_block, 0.0)
++
++ local_cdf = tl.cumsum(residual, axis=0)
++ local_index = tl.sum(
++ ((local_cdf <= local_target) & vocab_mask).to(tl.int32), axis=0
++ )
++ last_valid_local = tl.max(
++ tl.where((residual > 0.0) & vocab_mask, local_offsets, -1), axis=0
++ )
++ valid_local_count = tl.minimum(
++ vocab_block_size,
++ vocab_size - selected_block * vocab_block_size,
++ )
++ sampled_local = tl.where(
++ local_index < valid_local_count,
++ local_index,
++ last_valid_local,
++ )
++ sampled_token = tl.where(
++ sampled_local >= 0,
++ selected_block * vocab_block_size + sampled_local,
++ vocab_size - 1,
++ )
++ sampled_token = tl.minimum(sampled_token, vocab_size - 1)
++ tl.store(predicts + output_idx, sampled_token)
++
++
++def tree_speculative_sampling_target_only(
++ predicts: torch.Tensor,
++ accept_index: torch.Tensor,
++ accept_token_num: torch.Tensor,
++ candidates: torch.Tensor,
++ retrive_index: torch.Tensor,
++ retrive_next_token: torch.Tensor,
++ retrive_next_sibling: torch.Tensor,
++ uniform_samples: torch.Tensor,
++ uniform_samples_for_final_sampling: torch.Tensor,
++ target_probs: torch.Tensor,
++ draft_probs: torch.Tensor,
++ threshold_single: float = 1.0,
++ threshold_acc: float = 1.0,
++ deterministic: bool = True,
++) -> None:
++ """NPU port of GPU target-only tree speculative sampling.
++
++ ``draft_probs`` is scratch storage, matching the GPU API. The function
++ clears it and records rejected sibling probabilities before sampling from
++ ``relu(target_probs - draft_probs)`` on the final selected tree row.
++ """
++ del deterministic
++
++ if candidates.ndim != 2 or target_probs.ndim != 3:
++ raise ValueError("candidates must be 2-D and target_probs must be 3-D")
++
++ batch_size, num_draft_tokens = candidates.shape
++ if batch_size == 0:
++ return
++ if num_draft_tokens == 0:
++ raise ValueError("num_draft_tokens must be positive")
++ if target_probs.shape[:2] != (batch_size, num_draft_tokens):
++ raise ValueError(
++ "target_probs shape must be [batch, num_draft_tokens, vocab_size]"
++ )
++ tree_shapes = (
++ retrive_index.shape,
++ retrive_next_token.shape,
++ retrive_next_sibling.shape,
++ uniform_samples.shape,
++ )
++ if any(shape != candidates.shape for shape in tree_shapes):
++ raise ValueError("all tree-index and uniform tensors must match candidates")
++ if accept_index.ndim != 2 or accept_index.shape[0] != batch_size:
++ raise ValueError("accept_index must be [batch, max_tree_depth]")
++ num_speculative_tokens = accept_index.shape[1]
++ if not 1 <= num_speculative_tokens <= num_draft_tokens:
++ raise ValueError("max_tree_depth must be in [1, num_draft_tokens]")
++ if accept_token_num.shape != (batch_size,):
++ raise ValueError("accept_token_num must have shape [batch]")
++ if predicts.ndim != 1:
++ raise ValueError("predicts must be 1-D")
++ if uniform_samples_for_final_sampling.shape != (batch_size,):
++ raise ValueError(
++ "uniform_samples_for_final_sampling must have shape [batch]"
++ )
++ if draft_probs.shape != target_probs.shape:
++ raise ValueError("draft_probs scratch must match target_probs")
++ if draft_probs.data_ptr() == target_probs.data_ptr():
++ raise ValueError("draft_probs must not alias target_probs")
++ if target_probs.dtype != torch.float32 or draft_probs.dtype != torch.float32:
++ raise TypeError("target_probs and draft_probs must be torch.float32")
++ if uniform_samples.dtype != torch.float32:
++ raise TypeError("uniform_samples must be torch.float32")
++ if uniform_samples_for_final_sampling.dtype != torch.float32:
++ raise TypeError("uniform_samples_for_final_sampling must be torch.float32")
++ integer_dtypes = (
++ (predicts, torch.int32, "predicts"),
++ (accept_index, torch.int32, "accept_index"),
++ (accept_token_num, torch.int32, "accept_token_num"),
++ (candidates, torch.int64, "candidates"),
++ (retrive_index, torch.int64, "retrive_index"),
++ (retrive_next_token, torch.int64, "retrive_next_token"),
++ (retrive_next_sibling, torch.int64, "retrive_next_sibling"),
++ )
++ for tensor, expected_dtype, name in integer_dtypes:
++ if tensor.dtype != expected_dtype:
++ raise TypeError(f"{name} must be {expected_dtype}")
++ tensors = (
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ draft_probs,
++ )
++ if any(tensor.device != target_probs.device for tensor in tensors):
++ raise ValueError("all tensors must be on the same NPU device")
++ if any(not tensor.is_contiguous() for tensor in tensors):
++ raise ValueError("all tensors must be contiguous")
++ if not 0.0 <= threshold_single <= 1.0:
++ raise ValueError("threshold_single must be in [0, 1]")
++ if not 0.0 <= threshold_acc <= 1.0:
++ raise ValueError("threshold_acc must be in [0, 1]")
++
++ threshold_acc = max(float(threshold_acc), 1e-9)
++ vocab_size = target_probs.shape[-1]
++ vocab_block_size = 2048
++ num_vocab_blocks = triton.cdiv(vocab_size, vocab_block_size)
++ pad_num_vocab_blocks = triton.next_power_of_2(num_vocab_blocks)
++
++ # The CUDA call site passes zeros_like(target_probs). Clearing in the NPU
++ # wrapper makes the scratch contract explicit and permits empty_like callers.
++ draft_probs.zero_()
++ metadata = torch.empty(
++ (batch_size, 2), dtype=torch.int64, device=target_probs.device
++ )
++ block_sums = torch.empty(
++ (batch_size, num_vocab_blocks),
++ dtype=torch.float32,
++ device=target_probs.device,
++ )
++
++ _tree_target_only_accept_kernel[(batch_size,)](
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ target_probs,
++ draft_probs,
++ metadata,
++ float(threshold_single),
++ threshold_acc,
++ num_draft_tokens=num_draft_tokens,
++ num_speculative_tokens=num_speculative_tokens,
++ vocab_size=vocab_size,
++ )
++ _tree_target_only_block_sum_kernel[(batch_size, num_vocab_blocks)](
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens=num_draft_tokens,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ )
++ _tree_target_only_sample_kernel[(batch_size,)](
++ predicts,
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens=num_draft_tokens,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ pad_num_vocab_blocks=pad_num_vocab_blocks,
++ )
+diff --git a/tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py b/tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py
+new file mode 100644
+index 0000000..40b04be
+--- /dev/null
++++ b/tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py
+@@ -0,0 +1,138 @@
++import torch
++import torch_npu # noqa: F401
++
++from sgl_kernel_npu.sample import chain_speculative_sampling_rejection
++
++
++def chain_rejection_reference(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ draft_probs,
++):
++ batch_size, num_draft_tokens = candidates.shape
++ for req_idx in range(batch_size):
++ cur_prob_row = 0
++ last_accepted_idx = int(retrive_index[req_idx, 0])
++ accept_index[req_idx, 0] = last_accepted_idx
++ num_accepted = 0
++ all_accepted = True
++
++ for step in range(1, num_draft_tokens):
++ draft_token = int(candidates[req_idx, step])
++ p = float(target_probs[req_idx, cur_prob_row, draft_token])
++ q = float(draft_probs[req_idx, cur_prob_row, draft_token])
++ coin = float(uniform_samples[req_idx, step - 1])
++ if coin * q < p:
++ predicts[last_accepted_idx] = draft_token
++ num_accepted += 1
++ last_accepted_idx = int(retrive_index[req_idx, step])
++ accept_index[req_idx, num_accepted] = last_accepted_idx
++ cur_prob_row = step
++ else:
++ all_accepted = False
++ break
++
++ accept_token_num[req_idx] = num_accepted
++ residual = target_probs[req_idx, cur_prob_row].clone()
++ if not all_accepted:
++ residual.sub_(draft_probs[req_idx, cur_prob_row]).clamp_min_(0.0)
++ target = float(uniform_samples_for_final_sampling[req_idx]) * float(
++ residual.sum()
++ )
++ sampled_token = int((residual.cumsum(0) <= target).sum())
++ if sampled_token == residual.numel():
++ positive = torch.nonzero(residual > 0.0).flatten()
++ sampled_token = (
++ int(positive[-1]) if positive.numel() else residual.numel() - 1
++ )
++ predicts[last_accepted_idx] = sampled_token
++
++
++def test_chain_rejection_matches_gpu_algorithm():
++ batch_size, num_draft_tokens, vocab_size = 2, 4, 11
++ candidates = torch.tensor([[0, 2, 3, 4], [0, 5, 6, 7]])
++ retrive_index = torch.arange(batch_size * num_draft_tokens).view(
++ batch_size, num_draft_tokens
++ )
++ target_probs = torch.softmax(
++ torch.tensor(
++ [
++ [
++ [0.1, 0.2, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ [0.1, 0.2, 0.1, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ [0.1, 0.2, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ [0.1, 0.2, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ ],
++ [
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.2, 0.1, 2.0, 0.1, 0.1, 0.1],
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 2.0, 0.2, 0.1, 0.1, 0.1],
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1],
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1],
++ ],
++ ],
++ dtype=torch.float32,
++ ),
++ dim=-1,
++ )
++ draft_probs = torch.softmax(
++ torch.tensor(
++ [
++ [[0.1] * vocab_size, [0.1] * vocab_size, [0.1] * vocab_size],
++ [[0.1] * vocab_size, [0.1] * vocab_size, [0.1] * vocab_size],
++ ],
++ dtype=torch.float32,
++ ),
++ dim=-1,
++ )
++ draft_probs[1, 0, 5] = 0.9
++ draft_probs[1, 0] /= draft_probs[1, 0].sum()
++ uniforms = torch.tensor([[0.1, 0.1, 0.1, 0.0], [0.99, 0.0, 0.0, 0.0]])
++ final_uniforms = torch.tensor([0.37, 0.61])
++
++ expected_predicts = torch.full(
++ (batch_size * num_draft_tokens,), -1, dtype=torch.int32
++ )
++ expected_accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32
++ )
++ expected_accept_num = torch.zeros(batch_size, dtype=torch.int32)
++ chain_rejection_reference(
++ expected_predicts,
++ expected_accept_index,
++ expected_accept_num,
++ candidates,
++ retrive_index,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ draft_probs,
++ )
++
++ predicts = torch.full_like(expected_predicts, -1, device="npu")
++ accept_index = torch.full_like(expected_accept_index, -1, device="npu")
++ accept_num = torch.zeros_like(expected_accept_num, device="npu")
++ next_token = torch.full_like(candidates, -1, device="npu")
++ next_sibling = torch.full_like(candidates, -1, device="npu")
++ chain_speculative_sampling_rejection(
++ predicts,
++ accept_index,
++ accept_num,
++ candidates.npu(),
++ retrive_index.npu(),
++ next_token,
++ next_sibling,
++ uniforms.npu(),
++ final_uniforms.npu(),
++ target_probs.npu(),
++ draft_probs.npu(),
++ )
++
++ torch.testing.assert_close(predicts.cpu(), expected_predicts)
++ torch.testing.assert_close(accept_index.cpu(), expected_accept_index)
++ torch.testing.assert_close(accept_num.cpu(), expected_accept_num)
+diff --git a/tests/python/sgl_kernel_npu/test_speculative_probability.py b/tests/python/sgl_kernel_npu/test_speculative_probability.py
+new file mode 100644
+index 0000000..563f174
+--- /dev/null
++++ b/tests/python/sgl_kernel_npu/test_speculative_probability.py
+@@ -0,0 +1,31 @@
++import torch
++
++from sgl_kernel_npu.sample.probability import top_k_top_p_renorm_probs
++
++
++def test_top_k_top_p_renorm_matches_sequential_reference():
++ torch.manual_seed(7)
++ probs = torch.softmax(torch.randn(4, 97), dim=-1)
++ top_ks = torch.tensor([1, 7, 31, 97])
++ top_ps = torch.tensor([0.3, 0.75, 0.95, 1.0])
++
++ actual = top_k_top_p_renorm_probs(
++ probs, top_ks, top_ps, True, True
++ )
++
++ sorted_probs, sorted_indices = probs.sort(dim=-1, descending=True)
++ positions = torch.arange(probs.shape[-1]).view(1, -1)
++ sorted_probs[positions >= top_ks.view(-1, 1)] = 0.0
++ sorted_probs /= sorted_probs.sum(dim=-1, keepdim=True)
++ top_k_probs = torch.zeros_like(probs).scatter(
++ -1, sorted_indices, sorted_probs
++ )
++ sorted_probs, sorted_indices = top_k_probs.sort(dim=-1, descending=True)
++ cumulative = sorted_probs.cumsum(dim=-1)
++ sorted_probs[cumulative - sorted_probs > top_ps.view(-1, 1)] = 0.0
++ sorted_probs /= sorted_probs.sum(dim=-1, keepdim=True)
++ expected = torch.zeros_like(probs).scatter(
++ -1, sorted_indices, sorted_probs
++ )
++
++ torch.testing.assert_close(actual, expected, rtol=1e-6, atol=1e-7)
+diff --git a/tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py b/tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py
+new file mode 100644
+index 0000000..3c12d53
+--- /dev/null
++++ b/tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py
+@@ -0,0 +1,595 @@
++import argparse
++import time
++
++import pytest
++import torch
++import torch_npu # noqa: F401
++
++from sgl_kernel_npu.sample import tree_speculative_sampling_target_only
++
++
++def target_only_tree_reference(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ rejected_probs,
++ threshold_single,
++ threshold_acc,
++):
++ """CPU reference translated directly from the GPU CUDA kernel."""
++ batch_size, num_draft_tokens = candidates.shape
++ num_speculative_tokens = accept_index.shape[1]
++ threshold_acc = max(float(threshold_acc), 1e-9)
++ rejected_probs.zero_()
++
++ for req_idx in range(batch_size):
++ cur_prob_row = 0
++ cur_node = 0
++ coin = float(uniform_samples[req_idx, 0])
++ last_accepted_idx = int(retrive_index[req_idx, 0])
++ accept_index[req_idx, 0] = last_accepted_idx
++ num_accepted = 0
++
++ for _ in range(1, num_speculative_tokens):
++ cur_node = int(retrive_next_token[req_idx, cur_node])
++ prob_acc = 0.0
++ while cur_node != -1:
++ draft_idx = int(retrive_index[req_idx, cur_node])
++ draft_token = int(candidates[req_idx, cur_node])
++ target_prob = float(
++ target_probs[req_idx, cur_prob_row, draft_token]
++ )
++ prob_acc += target_prob
++ if (
++ coin <= prob_acc / threshold_acc
++ or target_prob >= threshold_single
++ ):
++ predicts[last_accepted_idx] = draft_token
++ num_accepted += 1
++ accept_index[req_idx, num_accepted] = draft_idx
++ last_accepted_idx = draft_idx
++ cur_prob_row = cur_node
++ coin = float(uniform_samples[req_idx, cur_node])
++ break
++
++ rejected_probs[req_idx, cur_prob_row, draft_token] = target_prob
++ cur_node = int(retrive_next_sibling[req_idx, cur_node])
++
++ if cur_node == -1:
++ break
++
++ accept_token_num[req_idx] = num_accepted
++ residual = (
++ target_probs[req_idx, cur_prob_row]
++ - rejected_probs[req_idx, cur_prob_row]
++ ).clamp_min(0.0)
++ target = float(uniform_samples_for_final_sampling[req_idx]) * float(
++ residual.sum()
++ )
++ sampled_token = int((residual.cumsum(0) <= target).sum())
++ if sampled_token == residual.numel():
++ positive = torch.nonzero(residual > 0.0).flatten()
++ sampled_token = (
++ int(positive[-1]) if positive.numel() else residual.numel() - 1
++ )
++ predicts[last_accepted_idx] = sampled_token
++
++ return predicts, accept_index, accept_token_num, rejected_probs
++
++
++def target_only_chain_reference(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ threshold_single,
++ threshold_acc,
++):
++ batch_size, num_draft_tokens = candidates.shape
++ threshold_acc = max(float(threshold_acc), 1e-9)
++
++ for req_idx in range(batch_size):
++ last_accepted_idx = int(retrive_index[req_idx, 0])
++ accept_index[req_idx, 0] = last_accepted_idx
++ num_accepted = 0
++ rejected_token = -1
++
++ for step in range(1, num_draft_tokens):
++ draft_token = int(candidates[req_idx, step])
++ target_prob = float(target_probs[req_idx, step - 1, draft_token])
++ coin = float(uniform_samples[req_idx, step - 1])
++ if (
++ coin <= target_prob / threshold_acc
++ or target_prob >= threshold_single
++ ):
++ predicts[last_accepted_idx] = draft_token
++ num_accepted += 1
++ last_accepted_idx = int(retrive_index[req_idx, step])
++ accept_index[req_idx, num_accepted] = last_accepted_idx
++ else:
++ rejected_token = draft_token
++ break
++
++ accept_token_num[req_idx] = num_accepted
++ final_probs = target_probs[req_idx, num_accepted].clone().float()
++ if rejected_token >= 0:
++ final_probs[rejected_token] = 0.0
++ final_probs.clamp_min_(0.0)
++ target = float(uniform_samples_for_final_sampling[req_idx]) * float(
++ final_probs.sum()
++ )
++ sampled_token = int((final_probs.cumsum(0) <= target).sum())
++ sampled_token = min(sampled_token, final_probs.numel() - 1)
++ predicts[last_accepted_idx] = sampled_token
++
++ return predicts, accept_index, accept_token_num
++
++
++@pytest.mark.parametrize(
++ "threshold_single,threshold_acc", [(1.0, 1.0), (0.0, 0.0), (0.5, 0.8)]
++)
++def test_general_tree_matches_gpu_algorithm_reference(
++ threshold_single, threshold_acc
++):
++ candidates = torch.tensor(
++ [[0, 1, 2, 3, 4, 5], [7, 8, 9, 10, 11, 12]], dtype=torch.int64
++ )
++ retrive_index = torch.tensor(
++ [[0, 1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11]], dtype=torch.int64
++ )
++ retrive_next_token = torch.tensor(
++ [[1, 2, -1, 4, 5, -1], [4, 2, 3, -1, 5, -1]],
++ dtype=torch.int64,
++ )
++ retrive_next_sibling = torch.tensor(
++ [[-1, 3, -1, -1, -1, -1], [-1, -1, -1, -1, 1, -1]],
++ dtype=torch.int64,
++ )
++ batch_size, num_draft_tokens = candidates.shape
++ vocab_size = 20
++ target_probs = torch.full(
++ (batch_size, num_draft_tokens, vocab_size), 0.01, dtype=torch.float32
++ )
++ target_probs[0, 0, 1] = 0.12
++ target_probs[0, 0, 3] = 0.72
++ target_probs[0, 3, 4] = 0.82
++ target_probs[0, 4, 5] = 0.75
++ target_probs[1, 0, 11] = 0.68
++ target_probs[1, 0, 8] = 0.14
++ target_probs[1, 4, 12] = 0.77
++ target_probs /= target_probs.sum(dim=-1, keepdim=True)
++ uniforms = torch.tensor(
++ [[0.55, 0.2, 0.8, 0.4, 0.3, 0.9], [0.6, 0.2, 0.8, 0.7, 0.3, 0.4]],
++ dtype=torch.float32,
++ )
++ final_uniforms = torch.tensor([0.25, 0.75], dtype=torch.float32)
++
++ ref_predicts = torch.full((12,), -1, dtype=torch.int32)
++ ref_accept_index = torch.full((2, 4), -1, dtype=torch.int32)
++ ref_accept_num = torch.zeros(2, dtype=torch.int32)
++ ref_rejected = torch.empty_like(target_probs)
++ target_only_tree_reference(
++ ref_predicts,
++ ref_accept_index,
++ ref_accept_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ ref_rejected,
++ threshold_single,
++ threshold_acc,
++ )
++
++ npu_predicts = torch.full_like(ref_predicts, -1, device="npu")
++ npu_accept_index = torch.full_like(ref_accept_index, -1, device="npu")
++ npu_accept_num = torch.zeros_like(ref_accept_num, device="npu")
++ npu_rejected = torch.empty_like(target_probs, device="npu")
++ tree_speculative_sampling_target_only(
++ npu_predicts,
++ npu_accept_index,
++ npu_accept_num,
++ candidates.npu(),
++ retrive_index.npu(),
++ retrive_next_token.npu(),
++ retrive_next_sibling.npu(),
++ uniforms.npu(),
++ final_uniforms.npu(),
++ target_probs.npu(),
++ npu_rejected,
++ threshold_single,
++ threshold_acc,
++ True,
++ )
++
++ torch.testing.assert_close(npu_predicts.cpu(), ref_predicts, rtol=0, atol=0)
++ torch.testing.assert_close(
++ npu_accept_index.cpu(), ref_accept_index, rtol=0, atol=0
++ )
++ torch.testing.assert_close(npu_accept_num.cpu(), ref_accept_num, rtol=0, atol=0)
++ torch.testing.assert_close(npu_rejected.cpu(), ref_rejected, rtol=0, atol=0)
++
++
++def target_only_chain_torch(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++):
++ batch_size, num_draft_tokens = candidates.shape
++ device = candidates.device
++ draft_tokens = candidates[:, 1:].long()
++ step_probs = torch.gather(
++ target_probs[:, :-1, :], 2, draft_tokens.unsqueeze(-1)
++ ).squeeze(-1)
++ accept_steps = uniform_samples[:, : num_draft_tokens - 1] <= step_probs
++ reject_count = (~accept_steps).to(torch.int32).cumsum(dim=1)
++ num_correct = (reject_count == 0).to(torch.int32).sum(dim=1)
++
++ accept_token_num.copy_(num_correct)
++ accept_index.fill_(-1)
++ positions = torch.arange(num_draft_tokens, device=device).view(1, -1)
++ valid_accept = positions <= num_correct.view(-1, 1)
++ accept_index.copy_(
++ torch.where(
++ valid_accept,
++ retrive_index.to(torch.int32),
++ torch.full_like(accept_index, -1),
++ )
++ )
++
++ predicts.zero_()
++ parent_positions = torch.arange(num_draft_tokens - 1, device=device).view(1, -1)
++ valid_parent = parent_positions < num_correct.view(-1, 1)
++ parent_indices = retrive_index[:, :-1].reshape(-1).long()
++ parent_values = candidates[:, 1:].to(torch.int32).reshape(-1)
++ predicts[parent_indices] = torch.where(
++ valid_parent.reshape(-1), parent_values, predicts[parent_indices]
++ )
++
++ rows = torch.arange(batch_size, device=device)
++ final_rows = num_correct.long()
++ final_probs = target_probs[rows, final_rows].clone()
++ rejected = num_correct < num_draft_tokens - 1
++ rejected_positions = (num_correct.long() + 1).clamp_max(num_draft_tokens - 1)
++ rejected_tokens = candidates[rows, rejected_positions].long()
++ final_probs[rejected, rejected_tokens[rejected]] = 0.0
++
++ probability_sums = final_probs.sum(dim=-1, keepdim=True)
++ targets = uniform_samples_for_final_sampling.view(-1, 1) * probability_sums
++ final_tokens = (
++ (final_probs.cumsum(dim=-1) <= targets)
++ .to(torch.int32)
++ .sum(dim=-1)
++ .clamp_max(target_probs.shape[-1] - 1)
++ )
++ final_indices = retrive_index[rows, final_rows].long()
++ predicts[final_indices] = final_tokens.to(torch.int32)
++
++
++def make_chain_indices(batch_size, num_draft_tokens, device):
++ retrive_index = torch.arange(
++ batch_size * num_draft_tokens, dtype=torch.int64, device=device
++ ).view(batch_size, num_draft_tokens)
++ retrive_next_token = torch.arange(
++ 1, num_draft_tokens + 1, dtype=torch.int64, device=device
++ ).repeat(batch_size, 1)
++ retrive_next_token[:, -1] = -1
++ retrive_next_sibling = torch.full_like(retrive_next_token, -1)
++ return retrive_index, retrive_next_token, retrive_next_sibling
++
++
++def make_stable_chain_final_uniforms(
++ candidates,
++ uniform_samples,
++ target_probs,
++):
++ """Choose final-sampling coins away from inverse-CDF boundaries."""
++ batch_size, num_draft_tokens = candidates.shape
++ final_uniforms = torch.empty(batch_size, dtype=torch.float32)
++
++ for req_idx in range(batch_size):
++ num_accepted = 0
++ rejected_token = -1
++ for step in range(1, num_draft_tokens):
++ draft_token = int(candidates[req_idx, step])
++ target_prob = float(target_probs[req_idx, step - 1, draft_token])
++ if float(uniform_samples[req_idx, step - 1]) <= target_prob:
++ num_accepted += 1
++ else:
++ rejected_token = draft_token
++ break
++
++ final_probs = target_probs[req_idx, num_accepted].double().clone()
++ if rejected_token >= 0:
++ final_probs[rejected_token] = 0.0
++
++ sampled_token = int(final_probs.argmax())
++ probability_sum = final_probs.sum()
++ cdf_before = final_probs[:sampled_token].sum()
++ cdf_midpoint = cdf_before + final_probs[sampled_token] * 0.5
++ final_uniforms[req_idx] = (cdf_midpoint / probability_sum).float()
++
++ return final_uniforms
++
++
++@pytest.mark.parametrize("batch_size", [1, 4, 17])
++@pytest.mark.parametrize("num_draft_tokens", [2, 5])
++@pytest.mark.parametrize("vocab_size", [20, 32000, 151552])
++def test_target_only_chain_matches_reference(
++ batch_size, num_draft_tokens, vocab_size
++):
++ torch.manual_seed(20260717 + batch_size + num_draft_tokens + vocab_size)
++ candidates = torch.randint(
++ 0, vocab_size, (batch_size, num_draft_tokens), dtype=torch.int64
++ )
++ logits = torch.randn(batch_size, num_draft_tokens, vocab_size)
++ target_probs = torch.softmax(logits, dim=-1).float()
++
++ # Give some draft tokens meaningful acceptance probability.
++ for req_idx in range(batch_size):
++ for step in range(1, num_draft_tokens):
++ token = int(candidates[req_idx, step])
++ target_probs[req_idx, step - 1] *= 0.35
++ target_probs[req_idx, step - 1, token] += 0.65
++ target_probs[req_idx, step - 1] /= target_probs[
++ req_idx, step - 1
++ ].sum()
++
++ uniform_samples = torch.rand(batch_size, num_draft_tokens)
++ # A random coin can land within FP32 reduction error of a CDF boundary for
++ # large vocabularies. Use the midpoint of a high-mass token's interval so
++ # exact token equality tests the algorithm instead of reduction order.
++ final_uniform_samples = make_stable_chain_final_uniforms(
++ candidates,
++ uniform_samples,
++ target_probs,
++ )
++ retrive_index, retrive_next_token, retrive_next_sibling = make_chain_indices(
++ batch_size, num_draft_tokens, "cpu"
++ )
++
++ ref_predicts = torch.full(
++ (batch_size * num_draft_tokens,), -1, dtype=torch.int32
++ )
++ ref_accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32
++ )
++ ref_accept_num = torch.zeros(batch_size, dtype=torch.int32)
++ target_only_chain_reference(
++ ref_predicts,
++ ref_accept_index,
++ ref_accept_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ final_uniform_samples,
++ target_probs,
++ 1.0,
++ 1.0,
++ )
++
++ npu_predicts = torch.full_like(ref_predicts, -1, device="npu")
++ npu_accept_index = torch.full_like(ref_accept_index, -1, device="npu")
++ npu_accept_num = torch.zeros_like(ref_accept_num, device="npu")
++ candidates_npu = candidates.npu()
++ retrive_index_npu = retrive_index.npu()
++ next_token_npu = retrive_next_token.npu()
++ next_sibling_npu = retrive_next_sibling.npu()
++ target_probs_npu = target_probs.npu()
++
++ tree_speculative_sampling_target_only(
++ predicts=npu_predicts,
++ accept_index=npu_accept_index,
++ accept_token_num=npu_accept_num,
++ candidates=candidates_npu,
++ retrive_index=retrive_index_npu,
++ retrive_next_token=next_token_npu,
++ retrive_next_sibling=next_sibling_npu,
++ uniform_samples=uniform_samples.npu(),
++ uniform_samples_for_final_sampling=final_uniform_samples.npu(),
++ target_probs=target_probs_npu,
++ draft_probs=torch.empty_like(target_probs_npu),
++ threshold_single=1.0,
++ threshold_acc=1.0,
++ deterministic=True,
++ )
++
++ torch.testing.assert_close(npu_predicts.cpu(), ref_predicts, rtol=0, atol=0)
++ torch.testing.assert_close(
++ npu_accept_index.cpu(), ref_accept_index, rtol=0, atol=0
++ )
++ torch.testing.assert_close(npu_accept_num.cpu(), ref_accept_num, rtol=0, atol=0)
++
++
++@pytest.mark.parametrize(
++ "threshold_single,threshold_acc",
++ [(1.0, 1.0), (0.0, 0.0), (0.5, 0.8)],
++)
++def test_target_only_thresholds(threshold_single, threshold_acc):
++ batch_size, num_draft_tokens, vocab_size = 2, 4, 32
++ candidates = torch.tensor([[0, 3, 4, 5], [0, 7, 8, 9]], dtype=torch.int64)
++ target_probs = torch.full(
++ (batch_size, num_draft_tokens, vocab_size), 1.0 / vocab_size
++ )
++ for req_idx in range(batch_size):
++ for step in range(1, num_draft_tokens):
++ token = int(candidates[req_idx, step])
++ target_probs[req_idx, step - 1] *= 0.2
++ target_probs[req_idx, step - 1, token] += 0.8
++ target_probs[req_idx, step - 1] /= target_probs[
++ req_idx, step - 1
++ ].sum()
++
++ uniforms = torch.tensor([[0.1, 0.9, 0.2, 0.0], [0.7, 0.2, 0.95, 0.0]])
++ final_uniforms = torch.tensor([0.25, 0.75])
++ retrive_index, next_token, next_sibling = make_chain_indices(
++ batch_size, num_draft_tokens, "cpu"
++ )
++
++ ref_predicts = torch.full((batch_size * num_draft_tokens,), -1, dtype=torch.int32)
++ ref_accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32
++ )
++ ref_accept_num = torch.zeros(batch_size, dtype=torch.int32)
++ target_only_chain_reference(
++ ref_predicts,
++ ref_accept_index,
++ ref_accept_num,
++ candidates,
++ retrive_index,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ threshold_single,
++ threshold_acc,
++ )
++
++ npu_predicts = torch.full_like(ref_predicts, -1, device="npu")
++ npu_accept_index = torch.full_like(ref_accept_index, -1, device="npu")
++ npu_accept_num = torch.zeros_like(ref_accept_num, device="npu")
++ target_probs_npu = target_probs.npu()
++ tree_speculative_sampling_target_only(
++ npu_predicts,
++ npu_accept_index,
++ npu_accept_num,
++ candidates.npu(),
++ retrive_index.npu(),
++ next_token.npu(),
++ next_sibling.npu(),
++ uniforms.npu(),
++ final_uniforms.npu(),
++ target_probs_npu,
++ torch.empty_like(target_probs_npu),
++ threshold_single,
++ threshold_acc,
++ True,
++ )
++
++ torch.testing.assert_close(npu_predicts.cpu(), ref_predicts, rtol=0, atol=0)
++ torch.testing.assert_close(
++ npu_accept_index.cpu(), ref_accept_index, rtol=0, atol=0
++ )
++ torch.testing.assert_close(npu_accept_num.cpu(), ref_accept_num, rtol=0, atol=0)
++
++
++def run_benchmark(batch_size, num_draft_tokens, vocab_size, warmup, iterations):
++ candidates = torch.randint(
++ 0,
++ vocab_size,
++ (batch_size, num_draft_tokens),
++ dtype=torch.int64,
++ device="npu",
++ )
++ target_probs = torch.softmax(
++ torch.randn(
++ batch_size,
++ num_draft_tokens,
++ vocab_size,
++ dtype=torch.float32,
++ device="npu",
++ ),
++ dim=-1,
++ )
++ retrive_index, next_token, next_sibling = make_chain_indices(
++ batch_size, num_draft_tokens, "npu"
++ )
++ uniforms = torch.rand(
++ batch_size, num_draft_tokens, dtype=torch.float32, device="npu"
++ )
++ final_uniforms = torch.rand(batch_size, dtype=torch.float32, device="npu")
++ draft_probs = torch.empty_like(target_probs)
++ predicts = torch.zeros(
++ batch_size * num_draft_tokens, dtype=torch.int32, device="npu"
++ )
++ accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32, device="npu"
++ )
++ accept_num = torch.zeros(batch_size, dtype=torch.int32, device="npu")
++
++ def run_kernel():
++ tree_speculative_sampling_target_only(
++ predicts,
++ accept_index,
++ accept_num,
++ candidates,
++ retrive_index,
++ next_token,
++ next_sibling,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ draft_probs,
++ 1.0,
++ 1.0,
++ True,
++ )
++
++ def run_torch():
++ target_only_chain_torch(
++ predicts,
++ accept_index,
++ accept_num,
++ candidates,
++ retrive_index,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ )
++
++ def benchmark(fn):
++ for _ in range(warmup):
++ fn()
++ torch.npu.synchronize()
++ started = time.perf_counter()
++ for _ in range(iterations):
++ fn()
++ torch.npu.synchronize()
++ return (time.perf_counter() - started) * 1000 / iterations
++
++ kernel_latency_ms = benchmark(run_kernel)
++ torch_latency_ms = benchmark(run_torch)
++ print(
++ f"batch={batch_size} drafts={num_draft_tokens} vocab={vocab_size} "
++ f"kernel_ms={kernel_latency_ms:.4f} torch_ms={torch_latency_ms:.4f} "
++ f"speedup={torch_latency_ms / kernel_latency_ms:.2f}x"
++ )
++
++
++if __name__ == "__main__":
++ parser = argparse.ArgumentParser()
++ parser.add_argument("--perf", action="store_true")
++ parser.add_argument("--batch-size", type=int, default=16)
++ parser.add_argument("--num-draft-tokens", type=int, default=5)
++ parser.add_argument("--vocab-size", type=int, default=151552)
++ parser.add_argument("--warmup", type=int, default=10)
++ parser.add_argument("--iterations", type=int, default=100)
++ args = parser.parse_args()
++ if args.perf:
++ run_benchmark(
++ args.batch_size,
++ args.num_draft_tokens,
++ args.vocab_size,
++ args.warmup,
++ args.iterations,
++ )
++ else:
++ raise SystemExit(pytest.main([__file__]))
+--
+2.34.1
+
+
+From b85bb7a082f3fcf9f8edbbe12b1641f94292245d Mon Sep 17 00:00:00 2001
+From: wuqiwei
+Date: Wed, 5 Aug 2026 13:52:42 +0000
+Subject: [PATCH 3/3] cuda_gragh arg patch
+
+---
+ .../python/torch_memory_saver/entrypoint.py | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py b/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
+index 146c011..7b0508d 100644
+--- a/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
++++ b/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
+@@ -36,6 +36,7 @@ class TorchMemorySaver:
+ capture_error_mode="global",
+ tag: str = _TAG_DEFAULT,
+ enable_cpu_backup: bool = False,
++ **kwargs,
+ ):
+ """Similar to `torch.cuda.graph`, but ensures memory in it to be pauseable."""
+ self._ensure_initialized()
+@@ -46,6 +47,7 @@ class TorchMemorySaver:
+ capture_error_mode=capture_error_mode,
+ tag=tag,
+ enable_cpu_backup=enable_cpu_backup,
++ **kwargs
+ ):
+ yield
+
+@@ -115,12 +117,13 @@ class _TorchMemorySaverImpl:
+ capture_error_mode,
+ tag: str,
+ enable_cpu_backup: bool,
++ **kwargs,
+ ):
+ assert (
+ self._hook_mode == "preload"
+ ), "Only hook_mode=preload supports pauseable CUDA Graph currently"
+ with torch.npu.graph(
+- cuda_graph, pool=pool, stream=stream, capture_error_mode=capture_error_mode
++ cuda_graph, pool=pool, stream=stream, capture_error_mode=capture_error_mode, **kwargs
+ ):
+ with self._with_region_config(tag=tag, enable_cpu_backup=enable_cpu_backup):
+ yield
+@@ -176,5 +179,4 @@ def _sanity_checks():
+ if "expandable_segments:True" in os.environ.get("PYTORCH_CUDA_ALLOC_CONF", ""):
+ raise RuntimeError(
+ "TorchMemorySaver is disabled for the current process because expandable_segments is not supported yet."
+- )
+-
+\ No newline at end of file
++ )
+\ No newline at end of file
+--
+2.34.1
+
diff --git a/docker/npu_patch/sglang-npu.patch b/docker/npu_patch/sglang-npu.patch
index de38657b6..fffe907a7 100644
--- a/docker/npu_patch/sglang-npu.patch
+++ b/docker/npu_patch/sglang-npu.patch
@@ -1,56 +1,421 @@
+From 1bf8b943fa46b025618857cf0efed7ac7b5fb9b5 Mon Sep 17 00:00:00 2001
+From: wuqiwei
+Date: Thu, 23 Jul 2026 08:44:55 +0000
+Subject: [PATCH 1/5] sglang_npu_patch
+
+---
+ python/sglang/srt/layers/quantization/unquant.py | 8 ++++----
+ python/sglang/srt/utils/common.py | 4 +++-
+ 2 files changed, 7 insertions(+), 5 deletions(-)
+
diff --git a/python/sglang/srt/layers/quantization/unquant.py b/python/sglang/srt/layers/quantization/unquant.py
-index 1ade4ed9e4..0161bd398a 100644
+index 82a3d77f05..25d4c21406 100644
--- a/python/sglang/srt/layers/quantization/unquant.py
+++ b/python/sglang/srt/layers/quantization/unquant.py
-@@ -314,12 +314,6 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
+@@ -402,10 +402,10 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
+ layer.w2_weight.data = layer.w2_weight.data.reshape(
layer.num_local_experts, *new_shape_w2
)
-
- if _is_npu:
- for weight_name in ["w13_weight", "w2_weight"]:
- weight = getattr(layer, weight_name)
-- weight.data = weight.data.transpose(1, 2)
-- weight.data = npu_format_cast(weight.data)
--
+- weight.data = npu_format_cast(weight)
++ # if _is_npu:
++ # for weight_name in ["w13_weight", "w2_weight"]:
++ # weight = getattr(layer, weight_name)
++ # weight.data = npu_format_cast(weight)
+
return
- def maybe_restore_flashinfer_trtllm_bf16_weight_shape_for_load(
-@@ -646,7 +640,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
- # gmm1: gate_up_proj
- hidden_states = torch.ops.npu.npu_grouped_matmul(
- x=[hidden_states],
-- weight=[layer.w13_weight],
-+ weight=[layer.w13_weight.transpose(1, 2)],
- bias=w13_bias,
- split_item=2,
- group_list_type=1,
-@@ -670,7 +664,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
- # gmm2: down_proj
- hidden_states = torch.ops.npu.npu_grouped_matmul(
- x=[hidden_states],
-- weight=[layer.w2_weight],
-+ weight=[layer.w2_weight.transpose(1, 2)],
- bias=w2_bias,
- split_item=2,
- group_list_type=1,
diff --git a/python/sglang/srt/utils/common.py b/python/sglang/srt/utils/common.py
-index f68721f3d0..3e85ee7c10 100644
+index 82db90da3b..aabd222d7e 100644
--- a/python/sglang/srt/utils/common.py
+++ b/python/sglang/srt/utils/common.py
-@@ -46,6 +46,7 @@ import types
- import uuid
- import warnings
- from collections import OrderedDict, defaultdict
+@@ -73,6 +73,7 @@ from typing import (
+ TypeVar,
+ Union,
+ )
+from relax.utils.device import is_npu_available
- from contextlib import contextmanager
- from dataclasses import dataclass
- from decimal import Decimal
-@@ -150,7 +151,7 @@ def is_npu() -> bool:
+ from unittest import SkipTest
+ from unittest.case import _ShouldStop
+ from urllib.parse import unquote, urlparse
+@@ -176,7 +177,8 @@ def is_npu() -> bool:
if not hasattr(torch, "npu"):
return False
- if not torch.npu.is_available():
++ # if not torch.npu.is_available():
+ if not is_npu_available:
raise RuntimeError(
"torch_npu detected, but NPU device is not available or visible."
)
+--
+2.34.1
+
+
+From 6ba9e861e0c32c4b299ad5b7b6e7e11c8a441534 Mon Sep 17 00:00:00 2001
+From: David Wang
+Date: Fri, 19 Jun 2026 00:19:40 +0000
+Subject: [PATCH 2/5] fix mamba radix partial page prefix matching
+
+---
+ .../sglang/srt/mem_cache/mamba_radix_cache.py | 9 ++++
+ .../unit/mem_cache/test_mamba_unittest.py | 52 +++++++++++++++++++
+ 2 files changed, 61 insertions(+)
+
+diff --git a/python/sglang/srt/mem_cache/mamba_radix_cache.py b/python/sglang/srt/mem_cache/mamba_radix_cache.py
+index 368781d8d5..0856caae48 100644
+--- a/python/sglang/srt/mem_cache/mamba_radix_cache.py
++++ b/python/sglang/srt/mem_cache/mamba_radix_cache.py
+@@ -1089,6 +1089,10 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ if self.disable or len(key) == 0:
+ return None
+
++ key = key.page_aligned(self.page_size)
++ if len(key) == 0:
++ return None
++
+ return key
+
+ def _match_post_processor(
+@@ -1158,6 +1162,9 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ )
+
+ def _split_node(self, key: RadixKey, child: TreeNode, split_len: int) -> TreeNode:
++ assert (
++ 0 < split_len < len(child.key)
++ ), f"split_len must create non-empty nodes, {split_len=}, {len(child.key)=}"
+ # new_node -> child
+ new_node = TreeNode()
+ new_node.children = {key[split_len:].child_key(self.page_size): child}
+@@ -1166,6 +1173,7 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ new_node.full_lock_ref = child.full_lock_ref
+ new_node.mamba_lock_ref = 0
+ new_node.key = child.key[:split_len]
++ assert len(new_node.key) > 0, f"new_node.key should not be empty"
+ new_node.value = child.value[:split_len].clone()
+
+ # child time should be later than parent's time for mamba tombstone
+@@ -1176,6 +1184,7 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ self.mamba_lru_list.remove_node(child)
+ child.parent = new_node
+ child.key = child.key[split_len:]
++ assert len(child.key) > 0, f"child.key should not be empty"
+ child.value = child.value[split_len:].clone()
+ new_node.parent.children[key.child_key(self.page_size)] = new_node
+ new_node.hash_value, child.hash_value = split_node_hash_value(
+diff --git a/test/registered/unit/mem_cache/test_mamba_unittest.py b/test/registered/unit/mem_cache/test_mamba_unittest.py
+index 9ad577b42e..390a3721c5 100755
+--- a/test/registered/unit/mem_cache/test_mamba_unittest.py
++++ b/test/registered/unit/mem_cache/test_mamba_unittest.py
+@@ -419,6 +419,58 @@ class TestMamba(unittest.TestCase):
+ self.assertEqual(list(second_insert_events[0].token_ids), [5])
+ self.assertEqual(second_insert_events[0].parent_block_hash, split_parent_hash)
+
++ def test_mamba_radix_cache_limited_partial_page_match_does_not_split(self):
++ page_size = 64
++ tree = self._setup_minimal_mamba_radix_cache(page_size)
++ token_ids = array("q", range(page_size))
++
++ tree.insert(
++ InsertParams(
++ key=RadixKey(token_ids, None),
++ value=torch.arange(page_size),
++ mamba_value=torch.tensor([0]),
++ )
++ )
++
++ match = tree.match_prefix(
++ MatchPrefixParams(key=RadixKey(token_ids, None, limit=page_size - 1))
++ )
++
++ self.assertEqual(len(match.device_indices), 0)
++ self.assertEqual(self._non_root_key_lengths(tree), [page_size])
++
++ def _setup_minimal_mamba_radix_cache(self, page_size: int) -> MambaRadixCache:
++ tree = MambaRadixCache.__new__(MambaRadixCache)
++ tree.page_size = page_size
++ tree.mamba_cache_chunk_size = page_size
++ tree.disable = False
++ tree.device = torch.device("cpu")
++ tree.enable_kv_cache_events = False
++ tree.kv_event_queue = []
++ tree.full_evictable_size_ = 0
++ tree.mamba_evictable_size_ = 0
++ tree.full_protected_size_ = 0
++ tree.mamba_protected_size_ = 0
++
++ tree.root_node = TreeNode()
++ tree.root_node.key = RadixKey(array("q"), None)
++ tree.root_node.value = []
++ tree.root_node.hash_value = []
++ tree.root_node.full_lock_ref = 1
++ tree.root_node.mamba_lock_ref = 1
++ tree.full_lru_list = LRUList(mamba=False)
++ tree.mamba_lru_list = LRUList(mamba=True)
++ return tree
++
++ def _non_root_key_lengths(self, tree: MambaRadixCache) -> list[int]:
++ lengths = []
++ stack = list(tree.root_node.children.values())
++ while stack:
++ node = stack.pop()
++ lengths.append(len(node.key))
++ stack.extend(node.children.values())
++ return lengths
++
+ def _setup_tree_and_allocator(self, enable_kv_cache_events=False):
+ """Helper to create a MambaRadixCache with allocator for testing."""
+ server_args = ServerArgs(model_path="dummy", page_size=1)
+--
+2.34.1
+
+
+From 9f5d5ac59b474998956f2ac2e9e494ee2c6529a6 Mon Sep 17 00:00:00 2001
+From: wuqiwei
+Date: Wed, 5 Aug 2026 09:26:57 +0000
+Subject: [PATCH 3/5] [NPU] Enable non-greedy MTP sampling- #32495 #32495
+
+---
+ python/sglang/srt/speculative/eagle_utils.py | 98 +++++++++++++-------
+ 1 file changed, 67 insertions(+), 31 deletions(-)
+
+diff --git a/python/sglang/srt/speculative/eagle_utils.py b/python/sglang/srt/speculative/eagle_utils.py
+index daf9fa82e2..f00f718fd2 100644
+--- a/python/sglang/srt/speculative/eagle_utils.py
++++ b/python/sglang/srt/speculative/eagle_utils.py
+@@ -617,7 +617,7 @@ def eagle_sample(
+
+ # Sample tokens
+ target_predict = None
+- if sampling_info.is_all_greedy or _is_npu or _is_hip or _is_xpu:
++ if sampling_info.is_all_greedy or _is_hip or _is_xpu:
+ target_predict = torch.argmax(next_token_logits, dim=-1)
+ target_predict = target_predict.reshape(bs, verify_input.draft_token_num)
+ predict, accept_index, num_correct_drafts = verify_tree_greedy_func(
+@@ -632,43 +632,80 @@ def eagle_sample(
+ topk=verify_input.tree_topk,
+ )
+ else:
+- from sgl_kernel import (
+- top_k_renorm_prob,
+- top_p_renorm_prob,
+- tree_speculative_sampling_target_only,
+- )
+-
+- from sglang.srt.speculative.reject_sampling import (
+- chain_speculative_sampling_triton,
+- )
+-
+ use_rejection_sampling = (
+ get_global_server_args().speculative_use_rejection_sampling
+ )
+
++ if _is_npu:
++ from sgl_kernel_npu.sample import (
++ chain_speculative_sampling_rejection,
++ top_k_top_p_renorm_probs,
++ tree_speculative_sampling_target_only,
++ )
++
++ sampling_fn = (
++ chain_speculative_sampling_rejection
++ if use_rejection_sampling
++ else tree_speculative_sampling_target_only
++ )
++ else:
++ from sgl_kernel import (
++ top_k_renorm_prob,
++ top_p_renorm_prob,
++ tree_speculative_sampling_target_only,
++ )
++
++ from sglang.srt.speculative.reject_sampling import (
++ chain_speculative_sampling_triton,
++ )
++
++ sampling_fn = (
++ chain_speculative_sampling_triton
++ if use_rejection_sampling
++ else tree_speculative_sampling_target_only
++ )
++
+ # Apply temperature and get target probs
+ expanded_temperature = torch.repeat_interleave(
+ sampling_info.temperatures, verify_input.draft_token_num, dim=0
+ ) # (bs * num_draft_tokens, 1)
+
++ sampling_logits = next_token_logits.float() if _is_npu else next_token_logits
+ target_probs = F.softmax(
+- next_token_logits / expanded_temperature, dim=-1
++ sampling_logits / expanded_temperature, dim=-1
+ ) # (bs * num_draft_tokens, vocab_size)
+ maybe_detect_nan(target_probs, "v2 verify: target_probs after softmax")
+- target_probs = top_k_renorm_prob(
+- target_probs,
+- torch.repeat_interleave(
+- sampling_info.top_ks, verify_input.draft_token_num, dim=0
+- ),
+- ) # (bs * num_draft_tokens, vocab_size)
+- maybe_detect_nan(target_probs, "v2 verify: target_probs after top_k_renorm")
+- target_probs = top_p_renorm_prob(
+- target_probs,
+- torch.repeat_interleave(
+- sampling_info.top_ps, verify_input.draft_token_num, dim=0
+- ),
+- )
+- maybe_detect_nan(target_probs, "v2 verify: target_probs after top_p_renorm")
++
++ if _is_npu:
++ target_probs = top_k_top_p_renorm_probs(
++ target_probs,
++ torch.repeat_interleave(
++ sampling_info.top_ks, verify_input.draft_token_num, dim=0
++ ),
++ torch.repeat_interleave(
++ sampling_info.top_ps, verify_input.draft_token_num, dim=0
++ ),
++ sampling_info.need_top_k_sampling,
++ sampling_info.need_top_p_sampling,
++ )
++ maybe_detect_nan(target_probs, "v2 verify: target_probs after renorm")
++ else:
++ if sampling_info.need_top_k_sampling:
++ target_probs = top_k_renorm_prob(
++ target_probs,
++ torch.repeat_interleave(
++ sampling_info.top_ks, verify_input.draft_token_num, dim=0
++ ),
++ ) # (bs * num_draft_tokens, vocab_size)
++ maybe_detect_nan(target_probs, "v2 verify: target_probs after top_k_renorm")
++ if sampling_info.need_top_p_sampling:
++ target_probs = top_p_renorm_prob(
++ target_probs,
++ torch.repeat_interleave(
++ sampling_info.top_ps, verify_input.draft_token_num, dim=0
++ ),
++ )
++ maybe_detect_nan(target_probs, "v2 verify: target_probs after top_p_renorm")
+ target_probs = target_probs.reshape(bs, verify_input.draft_token_num, -1)
+ draft_probs = (
+ verify_input.draft_probs
+@@ -687,16 +724,15 @@ def eagle_sample(
+ "does not produce one (draft_probs missing or vocab-mismatched)."
+ )
+
++ if _is_npu:
++ target_probs = target_probs.contiguous()
++ draft_probs = draft_probs.float().contiguous()
++
+ # coins for rejection sampling
+ coins = torch.rand_like(candidates, dtype=torch.float32, device=device)
+ # coins for final sampling
+ coins_for_final_sampling = torch.rand((bs,), dtype=torch.float32, device=device)
+
+- sampling_fn = (
+- chain_speculative_sampling_triton
+- if use_rejection_sampling
+- else tree_speculative_sampling_target_only
+- )
+ sampling_fn(
+ predicts=predict, # mutable
+ accept_index=accept_index, # mutable
+--
+2.34.1
+
+
+From 32ffa124368440360bc966cdaca9f44dac5aaf72 Mon Sep 17 00:00:00 2001
+From: wuqiwei
+Date: Thu, 6 Aug 2026 08:10:11 +0000
+Subject: [PATCH 4/5] for tms cuda_gragh
+
+---
+ python/sglang/srt/utils/torch_memory_saver_adapter.py | 9 ++++++---
+ 1 file changed, 6 insertions(+), 3 deletions(-)
+
+diff --git a/python/sglang/srt/utils/torch_memory_saver_adapter.py b/python/sglang/srt/utils/torch_memory_saver_adapter.py
+index ad98e59283..a5b2cdfd32 100644
+--- a/python/sglang/srt/utils/torch_memory_saver_adapter.py
++++ b/python/sglang/srt/utils/torch_memory_saver_adapter.py
+@@ -41,7 +41,7 @@ class TorchMemorySaverAdapter(ABC):
+ def region(self, tag: str, enable_cpu_backup: bool = False):
+ raise NotImplementedError
+
+- def cuda_graph(self, **kwargs):
++ def cuda_graph(self, cuda_graph=None, **kwargs):
+ raise NotImplementedError
+
+ def disable(self):
+@@ -67,7 +67,10 @@ class _TorchMemorySaverAdapterReal(TorchMemorySaverAdapter):
+ def region(self, tag: str, enable_cpu_backup: bool = False):
+ return _memory_saver.region(tag=tag, enable_cpu_backup=enable_cpu_backup)
+
+- def cuda_graph(self, **kwargs):
++ def cuda_graph(self, cuda_graph=None, **kwargs):
++ if cuda_graph is not None:
++ kwargs["cuda_graph"] = cuda_graph
++ # kwargs.pop("auto_dispatch_capture", None) # torch_memory_saver 0.0.8 does not support this arg
+ return _memory_saver.cuda_graph(**kwargs)
+
+ def disable(self):
+@@ -94,7 +97,7 @@ class _TorchMemorySaverAdapterNoop(TorchMemorySaverAdapter):
+ yield
+
+ @contextmanager
+- def cuda_graph(self, **kwargs):
++ def cuda_graph(self, cuda_graph=None, **kwargs):
+ yield
+
+ @contextmanager
+--
+2.34.1
+
+
+From 8c023fdbdf6d0ccf062f88a26a52cb2545b0683a Mon Sep 17 00:00:00 2001
+From: wuqiwei
+Date: Thu, 6 Aug 2026 08:10:48 +0000
+Subject: [PATCH 5/5] disable tms cuda_gragh
+
+---
+ .../npu/graph_runner/npu_cudagraph_backend.py | 18 +++++++++++++++++-
+ 1 file changed, 17 insertions(+), 1 deletion(-)
+
+diff --git a/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py b/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py
+index 919f46619a..c8a185388a 100644
+--- a/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py
++++ b/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py
+@@ -37,6 +37,21 @@ if TYPE_CHECKING:
+ BaseCudaGraphRunner,
+ )
+
++@contextmanager
++def _disable_tms_during_graph_capture():
++ try:
++ from torch_memory_saver import torch_memory_saver
++ _impl = torch_memory_saver._impl
++ except Exception:
++ _impl = None
++
++ if _impl is not None:
++ _impl._binary_wrapper.cdll.tms_set_interesting_region(False)
++ try:
++ yield
++ finally:
++ if _impl is not None:
++ _impl._binary_wrapper.cdll.tms_set_interesting_region(True)
+
+ class NPUCudaGraphBackend(BaseCudaGraphBackend):
+ """One torch.npu.NPUGraph per shape; attention metadata captured
+@@ -117,7 +132,8 @@ class NPUCudaGraphBackend(BaseCudaGraphBackend):
+ stream=self._capture_stream,
+ auto_dispatch_capture=True,
+ ):
+- out = forward_fn()
++ with _disable_tms_during_graph_capture():
++ out = forward_fn()
+
+ self._graphs[shape_key] = graph
+ self._outputs[shape_key] = out
+--
+2.34.1
+
From 57809b43772529b339dc5ba2a96f25594a711a71 Mon Sep 17 00:00:00 2001
From: lixionglong
Date: Fri, 21 Aug 2026 06:26:43 +0000
Subject: [PATCH 02/16] fix(megatron): attach _hf_config for MTP bridge when
pp>1
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# 🐛 Bug Fix
## Fix bridge mapping registry failure with PP > 1
- Set model_bridge._hf_config from bridge.hf_pretrained.config when missing
---
relax/backends/megatron/weight_update/bridge_converter.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/relax/backends/megatron/weight_update/bridge_converter.py b/relax/backends/megatron/weight_update/bridge_converter.py
index 608eb0ce8..6eb2c9a0a 100644
--- a/relax/backends/megatron/weight_update/bridge_converter.py
+++ b/relax/backends/megatron/weight_update/bridge_converter.py
@@ -82,7 +82,10 @@ def init_tasks(self) -> None:
if task.param_weight is not None:
self._bridge_task_map[task.global_param_name] = task
- self._bridge_mapping_registry = bridge._model_bridge.mapping_registry()
+ model_bridge = bridge._model_bridge
+ if not hasattr(model_bridge, "_hf_config"):
+ model_bridge._hf_config = bridge.hf_pretrained.config
+ self._bridge_mapping_registry = model_bridge.mapping_registry()
mapping_registry = self._bridge_mapping_registry
for name, _param in named_params_and_buffers(self._args, self._model):
global_name = strip_param_name_prefix(name)
From 3f51dccd11dc33747715136780fe353d7b0b63f4 Mon Sep 17 00:00:00 2001
From: wuqiwei
Date: Fri, 21 Aug 2026 06:28:27 +0000
Subject: [PATCH 03/16] chore(docker): refresh NPU patch set
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# 🔩 Chore
## Sync NPU patches
- Add sgl-kernel-npu.patch (1847 lines)
- Update sglang-npu.patch (+1070 lines)
- Update mindspeed.patch / mindspeed-bridge.patch / megatron patches
---
docker/npu_patch/megatron-bridge.patch | 56 +-
docker/npu_patch/megatron.patch | 38 +-
docker/npu_patch/mindspeed-bridge.patch | 142 +-
docker/npu_patch/mindspeed-ops.patch | 13 +
docker/npu_patch/mindspeed.patch | 154 ++
docker/npu_patch/sgl-kernel-npu.patch | 1847 +++++++++++++++++++++++
docker/npu_patch/sglang-npu.patch | 1070 ++++++++++++-
7 files changed, 3231 insertions(+), 89 deletions(-)
create mode 100644 docker/npu_patch/mindspeed-ops.patch
create mode 100644 docker/npu_patch/sgl-kernel-npu.patch
diff --git a/docker/npu_patch/megatron-bridge.patch b/docker/npu_patch/megatron-bridge.patch
index 0066df63b..d49be66b3 100644
--- a/docker/npu_patch/megatron-bridge.patch
+++ b/docker/npu_patch/megatron-bridge.patch
@@ -1,11 +1,32 @@
diff --git a/src/megatron/bridge/models/conversion/utils.py b/src/megatron/bridge/models/conversion/utils.py
-index 5a66e719..3d411c17 100644
+index 86ddf8661..115624b16 100644
--- a/src/megatron/bridge/models/conversion/utils.py
+++ b/src/megatron/bridge/models/conversion/utils.py
-@@ -203,6 +203,15 @@ def remove_non_pickleables(obj, max_depth: int = 3, current_depth: int = 0):
+@@ -203,6 +203,17 @@ def remove_non_pickleables(obj, max_depth: int = 3, current_depth: int = 0):
+ ): # bound methods
+ return None
+
++ # Convert OmegaConf containers to plain dict/list to avoid in-place
++ # mutation triggering "dictionary changed size during iteration" errors
++ # inside OmegaConf's internal _flags_cache handling.
++ try:
++ from omegaconf import DictConfig, ListConfig, OmegaConf as _OmegaConf
++
++ if isinstance(obj, (DictConfig, ListConfig)):
++ obj = _OmegaConf.to_container(obj, resolve=True)
++ except ImportError:
++ pass
++
+ # Handle dataclass/object with attributes
+ if hasattr(obj, "__dict__"):
+ # Create a copy to avoid modifying the original
+@@ -213,9 +224,18 @@ def remove_non_pickleables(obj, max_depth: int = 3, current_depth: int = 0):
# Recursively clean attribute
cleaned_value = remove_non_pickleables(attr_value, max_depth, current_depth + 1)
+-
+- # Set the cleaned value (or None if it was removed)
+- setattr(cleaned_obj, attr_name, cleaned_value)
+ if hasattr(cleaned_obj, '__setattr__'):
+ try:
+ setattr(cleaned_obj, attr_name, cleaned_value)
@@ -15,6 +36,33 @@ index 5a66e719..3d411c17 100644
+ print(f"Skipping attribute '{attr_name}' due to Union type")
+ continue
+ raise
++ else:
++ # Fallback for objects without __setattr__ override
++ setattr(cleaned_obj, attr_name, cleaned_value)
+
+ return cleaned_obj
+
+diff --git a/src/megatron/bridge/peft/utils.py b/src/megatron/bridge/peft/utils.py
+index 1ca5b18bd..4797e2e42 100644
+--- a/src/megatron/bridge/peft/utils.py
++++ b/src/megatron/bridge/peft/utils.py
+@@ -33,6 +33,7 @@ from megatron.core.transformer.moe.router import TopKRouter
+
+ from megatron.bridge.utils.import_utils import safe_import_from
+
++from relax.utils.device import is_npu_available
+
+ TEColumnParallelLinear, HAVE_TE_COL_LINEAR = safe_import_from(
+ "megatron.core.extensions.transformer_engine", "TEColumnParallelLinear"
+@@ -62,7 +63,10 @@ HAVE_TE = all(
+ )
+ )
+
+-MixedFusedLayerNorm, HAVE_APEX = safe_import_from("apex.normalization.fused_layer_norm", "MixedFusedLayerNorm")
++if is_npu_available:
++ MixedFusedLayerNorm, HAVE_APEX = None, False
++else:
++ MixedFusedLayerNorm, HAVE_APEX = safe_import_from("apex.normalization.fused_layer_norm", "MixedFusedLayerNorm")
- # Set the cleaned value (or None if it was removed)
- setattr(cleaned_obj, attr_name, cleaned_value)
+ TECL = (TEColumnParallelLinear, TELayerNormColumnParallelLinear, TEColumnParallelGroupedLinear)
+ TERL = (TERowParallelLinear, TERowParallelGroupedLinear)
diff --git a/docker/npu_patch/megatron.patch b/docker/npu_patch/megatron.patch
index 0b76d6360..21de37e10 100644
--- a/docker/npu_patch/megatron.patch
+++ b/docker/npu_patch/megatron.patch
@@ -655,7 +655,7 @@ index 4be974017..0dfdb0928 100644
"""
Maintain the expert bias in float32.
diff --git a/megatron/core/transformer/multi_token_prediction.py b/megatron/core/transformer/multi_token_prediction.py
-index 2edb652bf..58fb89c4e 100755
+index 2edb652bf..7a068271b 100755
--- a/megatron/core/transformer/multi_token_prediction.py
+++ b/megatron/core/transformer/multi_token_prediction.py
@@ -586,6 +586,102 @@ class MTPLossAutoScaler(torch.autograd.Function):
@@ -857,6 +857,42 @@ index 2edb652bf..58fb89c4e 100755
)
if self.config.recompute_method == 'uniform':
+@@ -1053,7 +1192,7 @@ class MultiTokenPredictionBlock(MegatronModule):
+ self._build_layers(pg_collection)
+ assert len(self.layers) > 0, "MultiTokenPredictionBlock must have at least one layer."
+ self.cp_group = pg_collection.cp
+-
++ self._register_mtp_grad_hooks()
+ def _build_layers(self, pg_collection):
+ def build_layer(layer_spec, layer_number):
+ fp8_init_context = get_fp8_context(self.config, is_init=True)
+@@ -1074,6 +1213,26 @@ class MultiTokenPredictionBlock(MegatronModule):
+ ]
+ )
+
++ def _register_mtp_grad_hooks(self):
++ if len(self.layers) == 0:
++ return
++
++ def _make_sync_hook():
++ def _hook(grad):
++ torch.npu.synchronize()
++ return _hook
++
++ sync_keys = [
++ "self_attention.linear_qkv.weight",
++ "eh_proj.weight",
++ ]
++
++ for name, param in self.layers[0].named_parameters():
++ for key in sync_keys:
++ if key in name:
++ param.register_hook(_make_sync_hook())
++ break
++
+ def forward(
+ self,
+ input_ids: Tensor,
diff --git a/megatron/core/transformer/transformer_config.py b/megatron/core/transformer/transformer_config.py
index eaae58590..0f01f6bc0 100644
--- a/megatron/core/transformer/transformer_config.py
diff --git a/docker/npu_patch/mindspeed-bridge.patch b/docker/npu_patch/mindspeed-bridge.patch
index 327ff7afa..f658a5981 100644
--- a/docker/npu_patch/mindspeed-bridge.patch
+++ b/docker/npu_patch/mindspeed-bridge.patch
@@ -19,64 +19,106 @@ index 99ccba5..8fa07a1 100644
# Set the cleaned value (or None if it was removed)
setattr(cleaned_obj, attr_name, cleaned_value)
diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
-index 818af29..ef97078 100644
+index 19a3b9d..3dd5e25 100644
--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
+++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
-@@ -191,7 +191,7 @@ class GatedDeltaNet(MegatronModule):
+@@ -1,6 +1,6 @@
+ # pylint: disable=R0801
+ from typing import List, Optional, Tuple
+-
++import os
+ import torch
+ from torch import nn
+ import torch.nn.functional as F
+@@ -37,12 +37,11 @@ from megatron.core.ssm.gated_delta_net import (
+ )
+
+ try:
+- from causal_conv1d import causal_conv1d
++ import fla_npu
++ from fla.modules.convolution import causal_conv1d
+ except ImportError:
+ causal_conv1d = None
+- causal_conv1d_update = None
+
+-from mindspeed_ops.api.triton.l2norm import l2norm
+ from mindspeed_bridge.models.qwen_vl.modelling_qwen3_vl.chunk_gated_delta_rule import (
+ torch_chunk_gated_delta_rule,
+ )
+@@ -59,6 +58,12 @@ except ImportError:
+
+ from mindspeed_bridge.models.qwen_vl.modelling_qwen3_vl.flash_gated_delta_rule import flash_gated_delta_rule
+
++def naive_l2norm(x: torch.FloatTensor, dim: int = -1, eps: float = 1e-6):
++ """This function is intended to align with the l2norm implementation in the FLA library."""
++ original_dtype = x.dtype
++ inv_norm = torch.rsqrt((x * x).sum(dim=dim, keepdim=True) + eps)
++ # Counteract verl's autocast promotion (bf16 -> fp32) by restoring original dtype
++ return (x * inv_norm).to(original_dtype)
+
+ class GatedDeltaNet(MegatronModule):
+ """Gated Delta Net (GDN) layer class
+@@ -191,9 +196,10 @@ class GatedDeltaNet(MegatronModule):
)
setattr(self.A_log, "tensor_model_parallel", True)
- if HAVE_FLA and self.use_triton_gdn:
-+ if HAVE_FLA:
++ gdn_backend = os.environ.get("MINDSPEED_BRIDGE_GDN_BACKEND", "triton").strip().lower()
++ if gdn_backend == "triton":
self.gated_delta_rule = chunk_gated_delta_rule
- elif self.use_ascend_gdn:
+- elif self.use_ascend_gdn:
++ elif gdn_backend=="ascendc":
self.gated_delta_rule = flash_gated_delta_rule
-diff --git a/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py b/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py
-index d495c47..f08ea49 100644
---- a/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py
-+++ b/mindspeed_bridge/models/qwen_vl/qwen35_vl_provider.py
-@@ -402,22 +402,6 @@ class Qwen35VLModelProvider(GPTModelProvider):
- self.vision_config = Qwen3_5VisionConfig()
- super().__post_init__()
+ else:
+ self.gated_delta_rule = torch_chunk_gated_delta_rule
+@@ -474,7 +480,7 @@ class GatedDeltaNet(MegatronModule):
+ beta=beta,
+ initial_state=None,
+ output_final_state=False,
+- use_qk_l2norm_in_kernel=True,
++ use_qk_l2norm_in_kernel=False,
+ cu_seqlens=cu_seqlens_q,
+ )
+ nvtx_range_pop(suffix="gated_delta_rule")
+@@ -555,7 +561,7 @@ class GatedDeltaNet(MegatronModule):
-- def finalize(self) -> None:
-- self.validate_parallelism()
-- super().finalize()
--
-- def validate_parallelism(self):
-- """Validate that parallelism settings are compatible with this model's architecture.
--
-- Call this after mutating parallelism attributes (e.g. tensor_model_parallel_size)
-- on an already-constructed provider, since finalize() only runs once before provide().
-- """
-- if self.num_query_groups < self.tensor_model_parallel_size:
-- raise ValueError(
-- f"TP size {self.tensor_model_parallel_size} should be less than or equal to "
-- f"num_query_groups {self.num_query_groups}. Please use a smaller TP size."
-- )
--
- def provide(self, pre_process=None, post_process=None, vp_stage=None) -> Qwen3VLModel:
- """Provide a Qwen3.5 VL dense model instance with vision and language components."""
- language_transformer_config = self
-@@ -595,21 +579,6 @@ class Qwen35VLMoEModelProvider(GPTModelProvider):
- self.vision_config = Qwen3_5MoeVisionConfig()
- super().__post_init__()
+ # Apply L2 norm to query and key
+ if self.use_qk_l2norm:
+- query_key = l2norm(query_key.contiguous())
++ query_key = naive_l2norm(query_key.contiguous())
-- def finalize(self) -> None:
-- self.validate_parallelism()
-- super().finalize()
--
-- def validate_parallelism(self):
-- """Validate that parallelism settings are compatible with this model's architecture.
--
-- Call this after mutating parallelism attributes (e.g. tensor_model_parallel_size)
-- on an already-constructed provider, since finalize() only runs once before provide().
-- """
-- if self.num_query_groups < self.tensor_model_parallel_size:
-- raise ValueError(
-- f"TP size {self.tensor_model_parallel_size} should be less than or equal to "
-- f"num_query_groups {self.num_query_groups}. Please use a smaller TP size."
-- )
+ # Split query and key
+ split_size = self.qk_dim_local_tp // self.key_head_dim // self.cp_size
+diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/text_model.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/text_model.py
+index 529eaa3..1df512c 100644
+--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/text_model.py
++++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/text_model.py
+@@ -108,6 +108,7 @@ class Qwen3VLGPTModel(GPTModel):
+ # args for deepstack
+ visual_pos_masks: Optional[torch.Tensor] = None,
+ deepstack_visual_embeds: Optional[list[torch.Tensor]] = None,
++ mtp_kwargs: Optional[dict] = None,
+ ) -> Tensor:
+ """Forward function of the GPT Model This function passes the input tensors
+ through the embedding layer, and then the decoeder and finally into the post
+@@ -195,6 +196,7 @@ class Qwen3VLGPTModel(GPTModel):
+ runtime_gather_output=runtime_gather_output,
+ extra_block_kwargs=extra_block_kwargs,
+ inference_context=inference_context,
++ mtp_kwargs=mtp_kwargs,
+ )
+
+ if _shadow_embedding:
+diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py
+index 5d03f85..2488ef7 100644
+--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py
++++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/transformer_config.py
+@@ -56,6 +56,8 @@ def get_vision_model_config(hf_config, megatron_config=None):
+ ffn_hidden_size=hf_config.intermediate_size,
+ add_bias_linear=True,
+ add_qkv_bias=True,
++ moe_permute_fusion=False, # ← 新增:vision model 没有 MoE,不需要 permute fusion
++ use_fused_moe_token_permute_and_unpermute=False, # ← 新增:同上
+ )
- def provide(self, pre_process=None, post_process=None, vp_stage=None) -> Qwen3VLModel:
- """Provide a Qwen3.5 VL model instance with vision and language components.
+ # apply text model config to vision model config
diff --git a/docker/npu_patch/mindspeed-ops.patch b/docker/npu_patch/mindspeed-ops.patch
new file mode 100644
index 000000000..9dae51dd4
--- /dev/null
+++ b/docker/npu_patch/mindspeed-ops.patch
@@ -0,0 +1,13 @@
+diff --git a/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py b/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py
+index bf62b0b..1925eca 100644
+--- a/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py
++++ b/mindspeed_ops/arch32/triton/gdn/chunk_gated_delta_rule_bwd_dhu.py
+@@ -21,7 +21,7 @@ from mindspeed_ops.api.triton.utils import prepare_chunk_indices, prepare_chunk_
+ )
+ @triton.autotune(
+ configs=get_autotune_config(multibuffer_list=(True, False)),
+- key=['H', 'K', 'V', 'BT', 'BV', 'USE_G', 'IS_VARLEN'],
++ key=['H', 'K', 'V', 'BT', 'BV'],
+ )
+ @triton.jit(do_not_specialize=['T'])
+ def chunk_gated_delta_rule_bwd_kernel_dhu_blockdim64(
diff --git a/docker/npu_patch/mindspeed.patch b/docker/npu_patch/mindspeed.patch
index 9f5f056ae..1597d6bbf 100644
--- a/docker/npu_patch/mindspeed.patch
+++ b/docker/npu_patch/mindspeed.patch
@@ -37,6 +37,22 @@ index c624fd0a..cef668af 100644
return parser
+diff --git a/mindspeed/core/megatron_basic/arguments_basic.py b/mindspeed/core/megatron_basic/arguments_basic.py
+old mode 100644
+new mode 100755
+index 87d71f81..aa6c219b
+--- a/mindspeed/core/megatron_basic/arguments_basic.py
++++ b/mindspeed/core/megatron_basic/arguments_basic.py
+@@ -147,7 +147,7 @@ def transformer_config_init_subclass(cls, **kwargs):
+ if callable(value) and not isinstance(value, type):
+ value = field(default_factory=value)
+ elif type(value) in mutable_types:
+- value = field(default_factory=lambda: value)
++ value = field(default_factory=lambda v=value: v)
+ else:
+ value = value
+ setattr(cls, key, value)
+\ No newline at end of file
diff --git a/mindspeed/features_manager/functional/profile.py b/mindspeed/features_manager/functional/profile.py
index 6450b41c..86584015 100644
--- a/mindspeed/features_manager/functional/profile.py
@@ -93,3 +109,141 @@ index f14b231d..881c7fdb 100644
full_args = get_full_args()
for k, v in args.items():
setattr(full_args, k, v)
+diff --git a/mindspeed/ops/triton/l2norm.py b/mindspeed/ops/triton/l2norm.py
+old mode 100644
+new mode 100755
+index 0050bce3..a9791a2d
+--- a/mindspeed/ops/triton/l2norm.py
++++ b/mindspeed/ops/triton/l2norm.py
+@@ -5,23 +5,13 @@
+ from typing import Optional
+
+ import torch
+-import torch.nn as nn
++from torch import nn
+ import triton
+ import triton.language as tl
+
+-from mindspeed.ops.triton.utils import input_guard, is_amd
++from mindspeed.ops.triton.utils import input_guard
+
+-BT_LIST = [8, 16, 32, 64, 128]
+-NUM_WARPS_AUTOTUNE = [1, 2, 4, 8, 16] if is_amd else [1, 2, 4, 8, 16, 32]
+
+-
+-@triton.autotune(
+- configs=[
+- triton.Config({}, num_warps=num_warps)
+- for num_warps in NUM_WARPS_AUTOTUNE
+- ],
+- key=['D']
+-)
+ @triton.jit
+ def l2norm_fwd_kernel1(
+ x,
+@@ -45,13 +35,13 @@ def l2norm_fwd_kernel1(
+ tl.store(rstd + i_t, b_rstd)
+
+
+-@triton.autotune(
+- configs=[
+- triton.Config({}, num_warps=num_warps)
+- for num_warps in NUM_WARPS_AUTOTUNE
+- ],
+- key=['D']
+-)
++#@triton.autotune(
++ # configs=[
++ # triton.Config({}, num_warps=num_warps)
++ # for num_warps in NUM_WARPS_AUTOTUNE
++ # ],
++ # key=['D']
++#)
+ @triton.jit
+ def l2norm_bwd_kernel1(
+ y,
+@@ -76,14 +66,14 @@ def l2norm_bwd_kernel1(
+ tl.store(dx + cols, b_dx, mask=mask)
+
+
+-@triton.autotune(
+- configs=[
+- triton.Config({'BT': BT}, num_warps=num_warps)
+- for num_warps in [1, 2, 4, 8, 16]
+- for BT in BT_LIST
+- ],
+- key=['D', 'NB']
+-)
++#@triton.autotune(
++# configs=[
++# triton.Config({'BT': BT}, num_warps=num_warps)
++# for num_warps in [1, 2, 4, 8, 16]
++# for BT in BT_LIST
++# ],
++# key=['D', 'NB']
++#)
+ @triton.jit
+ def l2norm_fwd_kernel(
+ x,
+@@ -113,14 +103,14 @@ def l2norm_fwd_kernel(
+ tl.store(p_rstd, b_rstd.to(p_rstd.dtype.element_ty), boundary_check=(0,))
+
+
+-@triton.autotune(
+- configs=[
+- triton.Config({'BT': BT}, num_warps=num_warps)
+- for num_warps in [1, 2, 4, 8, 16]
+- for BT in BT_LIST
+- ],
+- key=['D', 'NB']
+-)
++#@triton.autotune(
++# configs=[
++# triton.Config({'BT': BT}, num_warps=num_warps)
++# for num_warps in [1, 2, 4, 8, 16]
++# for BT in BT_LIST
++# ],
++# key=['D', 'NB']
++#)
+ @triton.jit
+ def l2norm_bwd_kernel(
+ y,
+@@ -188,11 +178,12 @@ def l2norm_fwd(
+ rstd = torch.empty((T,), dtype=torch.float32, device=x.device)
+ if D <= 512:
+ NB = triton.cdiv(T, 2048)
++ BT = 32
+ bt_size = 32
+
+ def grid(meta):
+- new_bt = meta['BT'] * bt_size
+- return (triton.cdiv(T, new_bt), )
++ new_bt = BT * bt_size
++ return (triton.cdiv(T, new_bt),)
+
+ l2norm_fwd_kernel[grid](
+ x=x,
+@@ -203,6 +194,7 @@ def l2norm_fwd(
+ D=D,
+ BD=BD,
+ NB=NB,
++ BT=BT,
+ bt_size=bt_size,
+ )
+ else:
+@@ -238,6 +230,7 @@ def l2norm_bwd(
+
+ if D <= 512:
+ NB = triton.cdiv(T, 2048)
++ BT=32
+ bt_size = 40
+ l2norm_bwd_kernel[(bt_size,)](
+ y=y,
+@@ -249,6 +242,7 @@ def l2norm_bwd(
+ D=D,
+ BD=BD,
+ NB=NB,
++ BT=BT,
+ bt_size=bt_size,
+ )
+ else:
diff --git a/docker/npu_patch/sgl-kernel-npu.patch b/docker/npu_patch/sgl-kernel-npu.patch
new file mode 100644
index 000000000..3ef4d0a96
--- /dev/null
+++ b/docker/npu_patch/sgl-kernel-npu.patch
@@ -0,0 +1,1847 @@
+diff --git a/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py b/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
+index b14c561..7b0508d 100644
+--- a/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
++++ b/contrib/torch_memory_saver/python/torch_memory_saver/entrypoint.py
+@@ -36,6 +36,7 @@ class TorchMemorySaver:
+ capture_error_mode="global",
+ tag: str = _TAG_DEFAULT,
+ enable_cpu_backup: bool = False,
++ **kwargs,
+ ):
+ """Similar to `torch.cuda.graph`, but ensures memory in it to be pauseable."""
+ self._ensure_initialized()
+@@ -46,6 +47,7 @@ class TorchMemorySaver:
+ capture_error_mode=capture_error_mode,
+ tag=tag,
+ enable_cpu_backup=enable_cpu_backup,
++ **kwargs
+ ):
+ yield
+
+@@ -115,12 +117,13 @@ class _TorchMemorySaverImpl:
+ capture_error_mode,
+ tag: str,
+ enable_cpu_backup: bool,
++ **kwargs,
+ ):
+ assert (
+ self._hook_mode == "preload"
+ ), "Only hook_mode=preload supports pauseable CUDA Graph currently"
+ with torch.npu.graph(
+- cuda_graph, pool=pool, stream=stream, capture_error_mode=capture_error_mode
++ cuda_graph, pool=pool, stream=stream, capture_error_mode=capture_error_mode, **kwargs
+ ):
+ with self._with_region_config(tag=tag, enable_cpu_backup=enable_cpu_backup):
+ yield
+@@ -159,6 +162,7 @@ class _TorchMemorySaverImpl:
+ # only be released after the memory region is resumed and empty_cache() is invoked.
+ torch_npu._C._npu_releasePool(torch.npu.current_device(), pool.id)
+ del pool
++ torch.npu.empty_cache()
+ finally:
+ self._binary_wrapper.cdll.tms_set_interesting_region(True)
+
+@@ -175,4 +179,4 @@ def _sanity_checks():
+ if "expandable_segments:True" in os.environ.get("PYTORCH_CUDA_ALLOC_CONF", ""):
+ raise RuntimeError(
+ "TorchMemorySaver is disabled for the current process because expandable_segments is not supported yet."
+- )
++ )
+\ No newline at end of file
+diff --git a/csrc/cache_location_assign/op_host/cache_loc_assign.cpp b/csrc/cache_location_assign/op_host/cache_loc_assign.cpp
+index 5fbecbe..b42f5bc 100644
+--- a/csrc/cache_location_assign/op_host/cache_loc_assign.cpp
++++ b/csrc/cache_location_assign/op_host/cache_loc_assign.cpp
+@@ -19,8 +19,8 @@
+ namespace sglang {
+ namespace npu_kernel {
+
+-at::Tensor getTiling(const at::Tensor &reqPoolIndices, uint64_t rowSize, uint64_t poolSize, uint32_t &blockDim,
+- bool isUpddate)
++at::Tensor getTiling(const at::Tensor &reqPoolIndices, uint64_t rowSize, uint64_t poolSize, uint64_t maxStep,
++ uint32_t &blockDim, bool isUpddate)
+ {
+ auto batchSize = reqPoolIndices.sizes()[0];
+ auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance();
+@@ -39,6 +39,7 @@ at::Tensor getTiling(const at::Tensor &reqPoolIndices, uint64_t rowSize, uint64_
+ tillingData->rowNumNoTail = batchSize / (tillingData->vcoreNum);
+ tillingData->tailNum = batchSize % (tillingData->vcoreNum);
+ tillingData->rowSize = rowSize;
++ tillingData->maxStep = maxStep;
+
+ if (reqPoolIndices.options().dtype() == at::kInt) {
+ tillingData->key = 1;
+@@ -50,13 +51,13 @@ at::Tensor getTiling(const at::Tensor &reqPoolIndices, uint64_t rowSize, uint64_
+ tillingData->reqInxBufferSize = tillingData->reqInxBufferCount * sizeof(int64_t);
+ }
+
+- tillingData->tokenCountAlignInt32 = host_utils::alinInt32Count(MAX_STEP);
++ tillingData->tokenCountAlignInt32 = host_utils::alinInt32Count(maxStep);
+ tillingData->tokenColAlignInt32 = tillingData->tokenCountAlignInt32 * sizeof(int32_t);
+
+ tillingData->offsetCountAlignInt64 = host_utils::alinInt64Count(batchSize);
+ tillingData->offsetColAlignInt64 = tillingData->offsetCountAlignInt64 * sizeof(int64_t);
+
+- tillingData->cacheLocSize = batchSize * MAX_STEP;
++ tillingData->cacheLocSize = batchSize * maxStep;
+ tillingData->cacheLocCountAlignInt32 = host_utils::alinInt32Count(tillingData->cacheLocSize);
+ tillingData->cacheLocAlignInt32 = tillingData->cacheLocCountAlignInt32 * sizeof(int32_t);
+
+@@ -73,7 +74,7 @@ at::Tensor getTiling(const at::Tensor &reqPoolIndices, uint64_t rowSize, uint64_
+ }
+
+ HOST_API void checkParams(const at::Tensor &reqPoolIndices, const at::Tensor &tokenPool, const at::Tensor &startOffset,
+- const at::Tensor &endOffset, const at::Tensor &outCacheLoc)
++ const at::Tensor &endOffset, const at::Tensor &outCacheLoc, int64_t maxStep)
+ {
+ auto reqIdxType = reqPoolIndices.options().dtype();
+ if ((reqIdxType != at::kInt && reqIdxType != at::kLong) || tokenPool.options().dtype() != at::kInt ||
+@@ -83,16 +84,28 @@ HOST_API void checkParams(const at::Tensor &reqPoolIndices, const at::Tensor &to
+ "Only support inputTensor combo1: int64, int32, int64, int64, int32; combo2: "
+ "int32, int32, int64, int64, int32");
+ }
++ if (maxStep < 1 || maxStep > MAX_STEP) {
++ throw std::invalid_argument("max_step must be in [1, " + std::to_string(MAX_STEP) + "], got " +
++ std::to_string(maxStep) +
++ " (the cache_loc_assign kernel handles at most MAX_STEP tokens per row)");
++ }
++ auto batchSize = reqPoolIndices.sizes()[0];
++ if (outCacheLoc.numel() < batchSize * static_cast(maxStep)) {
++ throw std::invalid_argument("out_cache_loc too small: needs at least batchSize * max_step = " +
++ std::to_string(batchSize * maxStep) + " elements, got " +
++ std::to_string(outCacheLoc.numel()));
++ }
+ }
+
+ HOST_API at::Tensor cache_loc_assign(const at::Tensor &reqPoolIndices, const at::Tensor &tokenPool,
+ const at::Tensor &startOffset, const at::Tensor &endOffset,
+- const at::Tensor &outCacheLoc)
++ const at::Tensor &outCacheLoc, int64_t maxStep)
+ {
+- checkParams(reqPoolIndices, tokenPool, startOffset, endOffset, outCacheLoc);
++ checkParams(reqPoolIndices, tokenPool, startOffset, endOffset, outCacheLoc, maxStep);
+ uint32_t blockDim;
+ uint32_t cacheAssignMode = 0;
+- at::Tensor tilingTensor = getTiling(reqPoolIndices, tokenPool.sizes()[1], tokenPool.sizes()[0], blockDim, false);
++ at::Tensor tilingTensor = getTiling(reqPoolIndices, tokenPool.sizes()[1], tokenPool.sizes()[0], maxStep, blockDim,
++ false);
+
+ EXEC_KERNEL_CMD(cache_loc_assign, blockDim, reqPoolIndices, tokenPool, startOffset, endOffset, outCacheLoc,
+ tilingTensor, cacheAssignMode);
+@@ -101,12 +114,13 @@ HOST_API at::Tensor cache_loc_assign(const at::Tensor &reqPoolIndices, const at:
+
+ HOST_API at::Tensor cache_loc_update(const at::Tensor &reqPoolIndices, const at::Tensor &tokenPool,
+ const at::Tensor &startOffset, const at::Tensor &endOffset,
+- const at::Tensor &outCacheLoc)
++ const at::Tensor &outCacheLoc, int64_t maxStep)
+ {
+- checkParams(reqPoolIndices, tokenPool, startOffset, endOffset, outCacheLoc);
++ checkParams(reqPoolIndices, tokenPool, startOffset, endOffset, outCacheLoc, maxStep);
+ uint32_t blockDim;
+ uint32_t cacheAssignMode = 1;
+- at::Tensor tilingTensor = getTiling(reqPoolIndices, tokenPool.sizes()[1], tokenPool.sizes()[0], blockDim, true);
++ at::Tensor tilingTensor = getTiling(reqPoolIndices, tokenPool.sizes()[1], tokenPool.sizes()[0], maxStep, blockDim,
++ true);
+
+ EXEC_KERNEL_CMD(cache_loc_assign, blockDim, reqPoolIndices, tokenPool, startOffset, endOffset, outCacheLoc,
+ tilingTensor, cacheAssignMode);
+diff --git a/csrc/cache_location_assign/op_host/tiling/cache_loc_assign.h b/csrc/cache_location_assign/op_host/tiling/cache_loc_assign.h
+index 2f8e9bd..2d473ee 100644
+--- a/csrc/cache_location_assign/op_host/tiling/cache_loc_assign.h
++++ b/csrc/cache_location_assign/op_host/tiling/cache_loc_assign.h
+@@ -26,6 +26,7 @@ struct AssignCacheTillingData {
+ uint64_t reqInxBufferCount{0};
+ uint64_t reqInxBufferSize{0};
+
++ uint64_t maxStep{0};
+ uint64_t tokenCountAlignInt32{0};
+ uint64_t tokenColAlignInt32{0};
+
+@@ -37,6 +38,10 @@ struct AssignCacheTillingData {
+ uint64_t cacheLocAlignInt32{0};
+ };
+
++// Upper bound of the per-row token count this kernel handles in one call.
++// The host validates `1 <= max_step <= MAX_STEP` and derives every buffer
++// size (token transfer count, out_cache_loc tiling) from the caller-supplied
++// max_step — never from this constant.
+ constexpr uint32_t MAX_STEP = 16;
+
+ #endif // CACHE_LOC_ASSIGN_TILING_H
+diff --git a/csrc/cache_location_assign/op_kernel/cache_loc_assign_kernel.cpp b/csrc/cache_location_assign/op_kernel/cache_loc_assign_kernel.cpp
+index 109f604..c78f303 100644
+--- a/csrc/cache_location_assign/op_kernel/cache_loc_assign_kernel.cpp
++++ b/csrc/cache_location_assign/op_kernel/cache_loc_assign_kernel.cpp
+@@ -21,6 +21,12 @@ constexpr int32_t BUFFER_NUM = 2;
+ constexpr uint32_t ASSIGN_TO_POOL = 0;
+ constexpr uint32_t RETRIEVE_FROM_POOL = 1;
+
++// Per-row contract: the caller passes max_step (tokens moved per row) through
++// the tiling data. The host validates `1 <= max_step <= MAX_STEP` and sizes
++// out_cache_loc as `batchSize * max_step`; every transfer below (token pool
++// copy in CopyIn, token pool write-back, cacheLoc GM read/write) is derived
++// from max_step, so no fixed-size over/under-transfer can occur.
++
+ template
+ class CacheLocAssignKernel
+ {
+@@ -44,6 +50,7 @@ public:
+ }
+ this->rowSize = tempTilingGM->rowSize;
+ this->reqInxBufferCount = tempTilingGM->reqInxBufferCount;
++ this->maxStep = tempTilingGM->maxStep;
+ this->tokenCountAlignInt32 = tempTilingGM->tokenCountAlignInt32;
+ this->offsetCountAlignInt64 = tempTilingGM->offsetCountAlignInt64;
+ this->cacheLocCountAlignInt32 = tempTilingGM->cacheLocCountAlignInt32;
+@@ -151,7 +158,7 @@ private:
+ tokenPoolLocal.SetValue(j, cache);
+ }
+
+- uint32_t tokenBytes = static_cast(MAX_STEP * sizeof(int32_t));
++ uint32_t tokenBytes = static_cast(this->maxStep * sizeof(int32_t));
+ AscendC::DataCopyExtParams copyParams{1, tokenBytes, 0, 0, 0};
+ AscendC::DataCopyPad(tokenPoolGM[reqIdx * this->rowSize + start], tokenPoolLocal, copyParams);
+
+@@ -208,6 +215,7 @@ private:
+ uint64_t tailOffset;
+ uint64_t rowOffset;
+ uint64_t rowSize;
++ uint64_t maxStep;
+ uint64_t cacheLocSize;
+
+ int64_t cacheIdxStart{0};
+diff --git a/csrc/pytorch_extensions.cpp b/csrc/pytorch_extensions.cpp
+index 1cfccb0..8e4ad91 100644
+--- a/csrc/pytorch_extensions.cpp
++++ b/csrc/pytorch_extensions.cpp
+@@ -32,11 +32,11 @@ TORCH_LIBRARY_FRAGMENT(npu, m)
+
+ m.def(
+ "cache_loc_assign(Tensor req_indices, Tensor token_pool, Tensor start_offset, Tensor end_offset, Tensor "
+- "out_cache_loc) -> Tensor");
++ "out_cache_loc, int max_step) -> Tensor");
+
+ m.def(
+ "cache_loc_update(Tensor req_indices, Tensor token_pool, Tensor start_offset, Tensor end_offset, Tensor "
+- "out_cache_loc) -> Tensor");
++ "out_cache_loc, int max_step) -> Tensor");
+
+ m.def(
+ "assign_cache_op(Tensor! out, Tensor src, Tensor dst_start_idx, Tensor dst_end_idx, Tensor src_start_idx, "
+diff --git a/include/sgl_kenel_npu_ops.h b/include/sgl_kenel_npu_ops.h
+index c6a4fba..ad51c12 100644
+--- a/include/sgl_kenel_npu_ops.h
++++ b/include/sgl_kenel_npu_ops.h
+@@ -19,13 +19,15 @@ at::Tensor cache_loc_assign(const at::Tensor &req_indices,
+ const at::Tensor &token_pool,
+ const at::Tensor &start_offset,
+ const at::Tensor &end_offset,
+- const at::Tensor &out_cache_loc);
++ const at::Tensor &out_cache_loc,
++ int64_t max_step);
+
+ at::Tensor cache_loc_update(const at::Tensor &req_indices,
+ const at::Tensor &token_pool,
+ const at::Tensor &start_offset,
+ const at::Tensor &end_offset,
+- const at::Tensor &out_cache_loc);
++ const at::Tensor &out_cache_loc,
++ int64_t max_step);
+
+ bool assign_cache_op(at::Tensor &dst_tensor, const at::Tensor &src_tensor,
+ const at::Tensor &dst_start_idx,
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py
+index e69de29..6e7bbd6 100644
+--- a/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/__init__.py
+@@ -0,0 +1,13 @@
++from sgl_kernel_npu.sample.chain_speculative_sampling import (
++ chain_speculative_sampling_rejection,
++)
++from sgl_kernel_npu.sample.probability import top_k_top_p_renorm_probs
++from sgl_kernel_npu.sample.tree_speculative_sampling_target_only import (
++ tree_speculative_sampling_target_only,
++)
++
++__all__ = [
++ "chain_speculative_sampling_rejection",
++ "top_k_top_p_renorm_probs",
++ "tree_speculative_sampling_target_only",
++]
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py
+new file mode 100644
+index 0000000..e8340da
+--- /dev/null
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/chain_speculative_sampling.py
+@@ -0,0 +1,354 @@
++import torch
++import triton
++import triton.language as tl
++
++
++@triton.jit
++def _chain_rejection_accept_kernel(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ target_probs,
++ draft_probs,
++ metadata,
++ num_draft_tokens: tl.constexpr,
++ num_speculative_tokens: tl.constexpr,
++ num_draft_prob_rows: tl.constexpr,
++ vocab_size: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ row_offset = req_idx * num_draft_tokens
++
++ cur_prob_row = tl.full((), 0, tl.int64)
++ last_accepted_idx = tl.load(retrive_index + row_offset).to(tl.int64)
++ num_accepted = 0
++ active = tl.full((), 1, tl.int32)
++
++ tl.store(accept_index + req_idx * num_speculative_tokens, last_accepted_idx)
++
++ # Linear Leviathan/Chen verification. Candidate 0 is the root; candidate
++ # step uses probability row step - 1 until a rejection terminates the chain.
++ for step in range(1, num_draft_tokens):
++ if active == 1:
++ draft_token = tl.load(candidates + row_offset + step).to(tl.int64)
++ target_offset = (
++ (row_offset + cur_prob_row) * vocab_size + draft_token
++ )
++ draft_offset = (
++ (req_idx * num_draft_prob_rows + cur_prob_row) * vocab_size
++ + draft_token
++ )
++ target_prob = tl.load(target_probs + target_offset).to(tl.float32)
++ draft_prob = tl.load(draft_probs + draft_offset).to(tl.float32)
++ coin = tl.load(uniform_samples + row_offset + step - 1).to(
++ tl.float32
++ )
++
++ if coin * draft_prob < target_prob:
++ tl.store(predicts + last_accepted_idx, draft_token)
++ num_accepted += 1
++ draft_idx = tl.load(retrive_index + row_offset + step).to(
++ tl.int64
++ )
++ tl.store(
++ accept_index
++ + req_idx * num_speculative_tokens
++ + num_accepted,
++ draft_idx,
++ )
++ last_accepted_idx = draft_idx
++ # Keep this loop-carried value int64 across both branches.
++ # Triton infers the constexpr loop variable `step` as int32.
++ cur_prob_row = tl.full((), step, tl.int64)
++ else:
++ active = 0
++
++ tl.store(accept_token_num + req_idx, num_accepted)
++
++ # metadata = [target row, output slot, all drafts accepted].
++ metadata_offset = req_idx * 3
++ tl.store(metadata + metadata_offset, cur_prob_row)
++ tl.store(metadata + metadata_offset + 1, last_accepted_idx)
++ tl.store(metadata + metadata_offset + 2, active)
++
++
++@triton.jit
++def _chain_rejection_block_sum_kernel(
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens: tl.constexpr,
++ num_draft_prob_rows: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ block_idx = tl.program_id(1)
++ vocab_offsets = block_idx * vocab_block_size + tl.arange(0, vocab_block_size)
++ vocab_mask = vocab_offsets < vocab_size
++
++ metadata_offset = req_idx * 3
++ target_row = tl.load(metadata + metadata_offset).to(tl.int64)
++ all_accepted = tl.load(metadata + metadata_offset + 2).to(tl.int32)
++ target_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target = tl.load(
++ target_probs + target_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++
++ if all_accepted == 1:
++ residual = target
++ else:
++ draft_offset = (
++ req_idx * num_draft_prob_rows + target_row
++ ) * vocab_size
++ draft = tl.load(
++ draft_probs + draft_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target - draft, 0.0)
++
++ tl.store(
++ block_sums + req_idx * num_vocab_blocks + block_idx,
++ tl.sum(residual, axis=0),
++ )
++
++
++@triton.jit
++def _chain_rejection_sample_kernel(
++ predicts,
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens: tl.constexpr,
++ num_draft_prob_rows: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++ pad_num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ metadata_offset = req_idx * 3
++ target_row = tl.load(metadata + metadata_offset).to(tl.int64)
++ output_idx = tl.load(metadata + metadata_offset + 1).to(tl.int64)
++ all_accepted = tl.load(metadata + metadata_offset + 2).to(tl.int32)
++
++ block_offsets = tl.arange(0, pad_num_vocab_blocks)
++ block_mask = block_offsets < num_vocab_blocks
++ sums = tl.load(
++ block_sums + req_idx * num_vocab_blocks + block_offsets,
++ mask=block_mask,
++ other=0.0,
++ ).to(tl.float32)
++ block_cdf = tl.cumsum(sums, axis=0)
++ total = tl.sum(sums, axis=0)
++ coin = tl.load(uniform_samples_for_final_sampling + req_idx).to(tl.float32)
++ target_value = coin * total
++
++ selected_block = tl.sum(
++ ((block_cdf <= target_value) & block_mask).to(tl.int32), axis=0
++ )
++ selected_block = tl.minimum(selected_block, num_vocab_blocks - 1)
++ prefix_sum = tl.sum(
++ tl.where(block_offsets < selected_block, sums, 0.0), axis=0
++ )
++ local_target = tl.maximum(target_value - prefix_sum, 0.0)
++
++ local_offsets = tl.arange(0, vocab_block_size)
++ vocab_offsets = selected_block * vocab_block_size + local_offsets
++ vocab_mask = vocab_offsets < vocab_size
++ target_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target = tl.load(
++ target_probs + target_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ if all_accepted == 1:
++ residual = target
++ else:
++ draft_offset = (
++ req_idx * num_draft_prob_rows + target_row
++ ) * vocab_size
++ draft = tl.load(
++ draft_probs + draft_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target - draft, 0.0)
++
++ local_cdf = tl.cumsum(residual, axis=0)
++ local_index = tl.sum(
++ ((local_cdf <= local_target) & vocab_mask).to(tl.int32), axis=0
++ )
++ last_valid_local = tl.max(
++ tl.where((residual > 0.0) & vocab_mask, local_offsets, -1), axis=0
++ )
++ valid_local_count = tl.minimum(
++ vocab_block_size,
++ vocab_size - selected_block * vocab_block_size,
++ )
++ sampled_local = tl.where(
++ local_index < valid_local_count,
++ local_index,
++ last_valid_local,
++ )
++ sampled_token = tl.where(
++ sampled_local >= 0,
++ selected_block * vocab_block_size + sampled_local,
++ vocab_size - 1,
++ )
++ tl.store(predicts + output_idx, tl.minimum(sampled_token, vocab_size - 1))
++
++
++def chain_speculative_sampling_rejection(
++ predicts: torch.Tensor,
++ accept_index: torch.Tensor,
++ accept_token_num: torch.Tensor,
++ candidates: torch.Tensor,
++ retrive_index: torch.Tensor,
++ retrive_next_token: torch.Tensor,
++ retrive_next_sibling: torch.Tensor,
++ uniform_samples: torch.Tensor,
++ uniform_samples_for_final_sampling: torch.Tensor,
++ target_probs: torch.Tensor,
++ draft_probs: torch.Tensor,
++ threshold_single: float = 1.0,
++ threshold_acc: float = 1.0,
++ deterministic: bool = True,
++) -> None:
++ """NPU kernel implementation of classic chain rejection sampling."""
++ del retrive_next_token, retrive_next_sibling
++ del threshold_single, threshold_acc, deterministic
++
++ if candidates.ndim != 2 or target_probs.ndim != 3:
++ raise ValueError("candidates must be 2-D and target_probs must be 3-D")
++ batch_size, num_draft_tokens = candidates.shape
++ if batch_size == 0:
++ return
++ if num_draft_tokens == 0:
++ raise ValueError("num_draft_tokens must be positive")
++ if target_probs.shape[:2] != (batch_size, num_draft_tokens):
++ raise ValueError(
++ "target_probs shape must be [batch, num_draft_tokens, vocab_size]"
++ )
++ if retrive_index.shape != candidates.shape:
++ raise ValueError("retrive_index shape must match candidates")
++ if accept_index.shape != candidates.shape:
++ raise ValueError(
++ "classic rejection sampling requires a topk=1 linear chain"
++ )
++ if accept_token_num.shape != (batch_size,):
++ raise ValueError("accept_token_num must have shape [batch]")
++ if predicts.ndim != 1:
++ raise ValueError("predicts must be 1-D")
++ if uniform_samples.shape != candidates.shape:
++ raise ValueError("uniform_samples shape must match candidates")
++ if uniform_samples_for_final_sampling.shape != (batch_size,):
++ raise ValueError(
++ "uniform_samples_for_final_sampling must have shape [batch]"
++ )
++ if draft_probs is None or draft_probs.ndim != 3:
++ raise ValueError("draft_probs must be a 3-D tensor")
++ if draft_probs.shape[0] != batch_size:
++ raise ValueError("draft_probs batch size must match candidates")
++ if draft_probs.shape[1] < max(num_draft_tokens - 1, 1):
++ raise ValueError("draft_probs does not contain every proposal row")
++ if draft_probs.shape[-1] != target_probs.shape[-1]:
++ raise ValueError("draft_probs and target_probs vocab sizes must match")
++ if target_probs.dtype != torch.float32 or draft_probs.dtype != torch.float32:
++ raise TypeError("target_probs and draft_probs must be torch.float32")
++ if uniform_samples.dtype != torch.float32:
++ raise TypeError("uniform_samples must be torch.float32")
++ if uniform_samples_for_final_sampling.dtype != torch.float32:
++ raise TypeError("uniform_samples_for_final_sampling must be torch.float32")
++ integer_dtypes = (
++ (predicts, torch.int32, "predicts"),
++ (accept_index, torch.int32, "accept_index"),
++ (accept_token_num, torch.int32, "accept_token_num"),
++ (candidates, torch.int64, "candidates"),
++ (retrive_index, torch.int64, "retrive_index"),
++ )
++ for tensor, expected_dtype, name in integer_dtypes:
++ if tensor.dtype != expected_dtype:
++ raise TypeError(f"{name} must be {expected_dtype}")
++ tensors = (
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ draft_probs,
++ )
++ if any(tensor.device != target_probs.device for tensor in tensors):
++ raise ValueError("all tensors must be on the same NPU device")
++ if any(not tensor.is_contiguous() for tensor in tensors):
++ raise ValueError("all tensors must be contiguous")
++
++ num_speculative_tokens = accept_index.shape[1]
++ num_draft_prob_rows = draft_probs.shape[1]
++ vocab_size = target_probs.shape[-1]
++ vocab_block_size = 2048
++ num_vocab_blocks = triton.cdiv(vocab_size, vocab_block_size)
++ pad_num_vocab_blocks = triton.next_power_of_2(num_vocab_blocks)
++
++ metadata = torch.empty(
++ (batch_size, 3), dtype=torch.int64, device=target_probs.device
++ )
++ block_sums = torch.empty(
++ (batch_size, num_vocab_blocks),
++ dtype=torch.float32,
++ device=target_probs.device,
++ )
++
++ _chain_rejection_accept_kernel[(batch_size,)](
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ target_probs,
++ draft_probs,
++ metadata,
++ num_draft_tokens=num_draft_tokens,
++ num_speculative_tokens=num_speculative_tokens,
++ num_draft_prob_rows=num_draft_prob_rows,
++ vocab_size=vocab_size,
++ )
++ _chain_rejection_block_sum_kernel[(batch_size, num_vocab_blocks)](
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens=num_draft_tokens,
++ num_draft_prob_rows=num_draft_prob_rows,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ )
++ _chain_rejection_sample_kernel[(batch_size,)](
++ predicts,
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens=num_draft_tokens,
++ num_draft_prob_rows=num_draft_prob_rows,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ pad_num_vocab_blocks=pad_num_vocab_blocks,
++ )
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py
+new file mode 100644
+index 0000000..cadddd0
+--- /dev/null
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/probability.py
+@@ -0,0 +1,40 @@
++import torch
++
++
++def top_k_top_p_renorm_probs(
++ probs: torch.Tensor,
++ top_ks: torch.Tensor,
++ top_ps: torch.Tensor,
++ need_top_k_sampling: bool,
++ need_top_p_sampling: bool,
++) -> torch.Tensor:
++ """Apply the same sequential top-k then top-p policy used by SGLang GPU."""
++ if not need_top_k_sampling and not need_top_p_sampling:
++ return probs
++
++ vocab_size = probs.shape[-1]
++ sorted_probs, sorted_indices = probs.sort(dim=-1, descending=True)
++
++ if need_top_k_sampling:
++ top_ks = top_ks.to(device=probs.device, dtype=torch.long).clamp(
++ min=1, max=vocab_size
++ )
++ positions = torch.arange(vocab_size, device=probs.device).view(1, -1)
++ sorted_probs.masked_fill_(positions >= top_ks.view(-1, 1), 0.0)
++ sorted_probs.div_(
++ sorted_probs.sum(dim=-1, keepdim=True).clamp_min_(1e-20)
++ )
++
++ if need_top_p_sampling:
++ top_ps = top_ps.to(device=probs.device, dtype=probs.dtype)
++ cumulative_probs = sorted_probs.cumsum(dim=-1)
++ sorted_probs.masked_fill_(
++ cumulative_probs - sorted_probs > top_ps.view(-1, 1), 0.0
++ )
++ sorted_probs.div_(
++ sorted_probs.sum(dim=-1, keepdim=True).clamp_min_(1e-20)
++ )
++
++ return torch.zeros_like(probs).scatter_(
++ dim=-1, index=sorted_indices, src=sorted_probs
++ )
+diff --git a/python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py b/python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py
+new file mode 100644
+index 0000000..e34baab
+--- /dev/null
++++ b/python/sgl_kernel_npu/sgl_kernel_npu/sample/tree_speculative_sampling_target_only.py
+@@ -0,0 +1,383 @@
++import torch
++import triton
++import triton.language as tl
++
++
++@triton.jit
++def _tree_target_only_accept_kernel(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ target_probs,
++ rejected_probs,
++ metadata,
++ threshold_single,
++ threshold_acc,
++ num_draft_tokens: tl.constexpr,
++ num_speculative_tokens: tl.constexpr,
++ vocab_size: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ row_offset = req_idx * num_draft_tokens
++
++ cur_prob_row = tl.full((), 0, tl.int64)
++ cur_node = tl.full((), 0, tl.int64)
++ last_accepted_idx = tl.load(retrive_index + row_offset).to(tl.int64)
++ coin = tl.load(uniform_samples + row_offset).to(tl.float32)
++ num_accepted = 0
++ path_active = tl.full((), 1, tl.int32)
++
++ tl.store(accept_index + req_idx * num_speculative_tokens, last_accepted_idx)
++
++ # This is the same breadth-at-each-depth traversal used by the CUDA kernel:
++ # descend to the first child, then walk siblings until one is accepted.
++ for _depth in range(1, num_speculative_tokens):
++ accepted_at_depth = tl.full((), 0, tl.int32)
++ prob_acc = tl.full((), 0.0, tl.float32)
++
++ if path_active == 1:
++ cur_node = tl.load(
++ retrive_next_token + row_offset + cur_node
++ ).to(tl.int64)
++ if cur_node == -1:
++ path_active = 0
++
++ # The loop is bounded by the number of tree nodes. It terminates
++ # logically when a child is accepted or the sibling list reaches -1.
++ for _sibling in range(0, num_draft_tokens):
++ if (
++ (path_active == 1)
++ & (accepted_at_depth == 0)
++ & (cur_node != -1)
++ ):
++ draft_token = tl.load(
++ candidates + row_offset + cur_node
++ ).to(tl.int64)
++ draft_idx = tl.load(
++ retrive_index + row_offset + cur_node
++ ).to(tl.int64)
++ prob_offset = (
++ (row_offset + cur_prob_row) * vocab_size + draft_token
++ )
++ target_prob_single = tl.load(
++ target_probs + prob_offset
++ ).to(tl.float32)
++ prob_acc += target_prob_single
++
++ accepted = (coin <= prob_acc / threshold_acc) | (
++ target_prob_single >= threshold_single
++ )
++ if accepted:
++ tl.store(predicts + last_accepted_idx, draft_token)
++ num_accepted += 1
++ tl.store(
++ accept_index
++ + req_idx * num_speculative_tokens
++ + num_accepted,
++ draft_idx,
++ )
++ last_accepted_idx = draft_idx
++ cur_prob_row = cur_node
++ coin = tl.load(
++ uniform_samples + row_offset + cur_node
++ ).to(tl.float32)
++ accepted_at_depth = 1
++ else:
++ # The CUDA target-only kernel stores the rejected sibling's
++ # target probability in draft_probs and later samples from
++ # relu(target_probs - draft_probs).
++ tl.store(rejected_probs + prob_offset, target_prob_single)
++ cur_node = tl.load(
++ retrive_next_sibling + row_offset + cur_node
++ ).to(tl.int64)
++
++ if accepted_at_depth == 0:
++ path_active = 0
++
++ tl.store(accept_token_num + req_idx, num_accepted)
++
++ # metadata = [final target-probability row, final output slot].
++ metadata_offset = req_idx * 2
++ tl.store(metadata + metadata_offset, cur_prob_row)
++ tl.store(metadata + metadata_offset + 1, last_accepted_idx)
++
++
++@triton.jit
++def _tree_target_only_block_sum_kernel(
++ target_probs,
++ rejected_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ block_idx = tl.program_id(1)
++ vocab_offsets = block_idx * vocab_block_size + tl.arange(0, vocab_block_size)
++ vocab_mask = vocab_offsets < vocab_size
++
++ target_row = tl.load(metadata + req_idx * 2).to(tl.int64)
++ probs_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target = tl.load(
++ target_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ rejected = tl.load(
++ rejected_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target - rejected, 0.0)
++ block_sum = tl.sum(residual, axis=0)
++ tl.store(block_sums + req_idx * num_vocab_blocks + block_idx, block_sum)
++
++
++@triton.jit
++def _tree_target_only_sample_kernel(
++ predicts,
++ target_probs,
++ rejected_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens: tl.constexpr,
++ vocab_size: tl.constexpr,
++ vocab_block_size: tl.constexpr,
++ num_vocab_blocks: tl.constexpr,
++ pad_num_vocab_blocks: tl.constexpr,
++):
++ req_idx = tl.program_id(0)
++ metadata_offset = req_idx * 2
++ target_row = tl.load(metadata + metadata_offset).to(tl.int64)
++ output_idx = tl.load(metadata + metadata_offset + 1).to(tl.int64)
++
++ block_offsets = tl.arange(0, pad_num_vocab_blocks)
++ block_mask = block_offsets < num_vocab_blocks
++ sums = tl.load(
++ block_sums + req_idx * num_vocab_blocks + block_offsets,
++ mask=block_mask,
++ other=0.0,
++ ).to(tl.float32)
++ block_cdf = tl.cumsum(sums, axis=0)
++ total = tl.sum(sums, axis=0)
++ coin = tl.load(uniform_samples_for_final_sampling + req_idx).to(tl.float32)
++ target = coin * total
++
++ selected_block = tl.sum(
++ ((block_cdf <= target) & block_mask).to(tl.int32), axis=0
++ )
++ selected_block = tl.minimum(selected_block, num_vocab_blocks - 1)
++ prefix_sum = tl.sum(
++ tl.where(block_offsets < selected_block, sums, 0.0), axis=0
++ )
++ local_target = tl.maximum(target - prefix_sum, 0.0)
++
++ local_offsets = tl.arange(0, vocab_block_size)
++ vocab_offsets = selected_block * vocab_block_size + local_offsets
++ vocab_mask = vocab_offsets < vocab_size
++ probs_offset = (req_idx * num_draft_tokens + target_row) * vocab_size
++ target_probs_block = tl.load(
++ target_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ rejected_probs_block = tl.load(
++ rejected_probs + probs_offset + vocab_offsets,
++ mask=vocab_mask,
++ other=0.0,
++ ).to(tl.float32)
++ residual = tl.maximum(target_probs_block - rejected_probs_block, 0.0)
++
++ local_cdf = tl.cumsum(residual, axis=0)
++ local_index = tl.sum(
++ ((local_cdf <= local_target) & vocab_mask).to(tl.int32), axis=0
++ )
++ last_valid_local = tl.max(
++ tl.where((residual > 0.0) & vocab_mask, local_offsets, -1), axis=0
++ )
++ valid_local_count = tl.minimum(
++ vocab_block_size,
++ vocab_size - selected_block * vocab_block_size,
++ )
++ sampled_local = tl.where(
++ local_index < valid_local_count,
++ local_index,
++ last_valid_local,
++ )
++ sampled_token = tl.where(
++ sampled_local >= 0,
++ selected_block * vocab_block_size + sampled_local,
++ vocab_size - 1,
++ )
++ sampled_token = tl.minimum(sampled_token, vocab_size - 1)
++ tl.store(predicts + output_idx, sampled_token)
++
++
++def tree_speculative_sampling_target_only(
++ predicts: torch.Tensor,
++ accept_index: torch.Tensor,
++ accept_token_num: torch.Tensor,
++ candidates: torch.Tensor,
++ retrive_index: torch.Tensor,
++ retrive_next_token: torch.Tensor,
++ retrive_next_sibling: torch.Tensor,
++ uniform_samples: torch.Tensor,
++ uniform_samples_for_final_sampling: torch.Tensor,
++ target_probs: torch.Tensor,
++ draft_probs: torch.Tensor,
++ threshold_single: float = 1.0,
++ threshold_acc: float = 1.0,
++ deterministic: bool = True,
++) -> None:
++ """NPU port of GPU target-only tree speculative sampling.
++
++ ``draft_probs`` is scratch storage, matching the GPU API. The function
++ clears it and records rejected sibling probabilities before sampling from
++ ``relu(target_probs - draft_probs)`` on the final selected tree row.
++ """
++ del deterministic
++
++ if candidates.ndim != 2 or target_probs.ndim != 3:
++ raise ValueError("candidates must be 2-D and target_probs must be 3-D")
++
++ batch_size, num_draft_tokens = candidates.shape
++ if batch_size == 0:
++ return
++ if num_draft_tokens == 0:
++ raise ValueError("num_draft_tokens must be positive")
++ if target_probs.shape[:2] != (batch_size, num_draft_tokens):
++ raise ValueError(
++ "target_probs shape must be [batch, num_draft_tokens, vocab_size]"
++ )
++ tree_shapes = (
++ retrive_index.shape,
++ retrive_next_token.shape,
++ retrive_next_sibling.shape,
++ uniform_samples.shape,
++ )
++ if any(shape != candidates.shape for shape in tree_shapes):
++ raise ValueError("all tree-index and uniform tensors must match candidates")
++ if accept_index.ndim != 2 or accept_index.shape[0] != batch_size:
++ raise ValueError("accept_index must be [batch, max_tree_depth]")
++ num_speculative_tokens = accept_index.shape[1]
++ if not 1 <= num_speculative_tokens <= num_draft_tokens:
++ raise ValueError("max_tree_depth must be in [1, num_draft_tokens]")
++ if accept_token_num.shape != (batch_size,):
++ raise ValueError("accept_token_num must have shape [batch]")
++ if predicts.ndim != 1:
++ raise ValueError("predicts must be 1-D")
++ if uniform_samples_for_final_sampling.shape != (batch_size,):
++ raise ValueError(
++ "uniform_samples_for_final_sampling must have shape [batch]"
++ )
++ if draft_probs.shape != target_probs.shape:
++ raise ValueError("draft_probs scratch must match target_probs")
++ if draft_probs.data_ptr() == target_probs.data_ptr():
++ raise ValueError("draft_probs must not alias target_probs")
++ if target_probs.dtype != torch.float32 or draft_probs.dtype != torch.float32:
++ raise TypeError("target_probs and draft_probs must be torch.float32")
++ if uniform_samples.dtype != torch.float32:
++ raise TypeError("uniform_samples must be torch.float32")
++ if uniform_samples_for_final_sampling.dtype != torch.float32:
++ raise TypeError("uniform_samples_for_final_sampling must be torch.float32")
++ integer_dtypes = (
++ (predicts, torch.int32, "predicts"),
++ (accept_index, torch.int32, "accept_index"),
++ (accept_token_num, torch.int32, "accept_token_num"),
++ (candidates, torch.int64, "candidates"),
++ (retrive_index, torch.int64, "retrive_index"),
++ (retrive_next_token, torch.int64, "retrive_next_token"),
++ (retrive_next_sibling, torch.int64, "retrive_next_sibling"),
++ )
++ for tensor, expected_dtype, name in integer_dtypes:
++ if tensor.dtype != expected_dtype:
++ raise TypeError(f"{name} must be {expected_dtype}")
++ tensors = (
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ draft_probs,
++ )
++ if any(tensor.device != target_probs.device for tensor in tensors):
++ raise ValueError("all tensors must be on the same NPU device")
++ if any(not tensor.is_contiguous() for tensor in tensors):
++ raise ValueError("all tensors must be contiguous")
++ if not 0.0 <= threshold_single <= 1.0:
++ raise ValueError("threshold_single must be in [0, 1]")
++ if not 0.0 <= threshold_acc <= 1.0:
++ raise ValueError("threshold_acc must be in [0, 1]")
++
++ threshold_acc = max(float(threshold_acc), 1e-9)
++ vocab_size = target_probs.shape[-1]
++ vocab_block_size = 2048
++ num_vocab_blocks = triton.cdiv(vocab_size, vocab_block_size)
++ pad_num_vocab_blocks = triton.next_power_of_2(num_vocab_blocks)
++
++ # The CUDA call site passes zeros_like(target_probs). Clearing in the NPU
++ # wrapper makes the scratch contract explicit and permits empty_like callers.
++ draft_probs.zero_()
++ metadata = torch.empty(
++ (batch_size, 2), dtype=torch.int64, device=target_probs.device
++ )
++ block_sums = torch.empty(
++ (batch_size, num_vocab_blocks),
++ dtype=torch.float32,
++ device=target_probs.device,
++ )
++
++ _tree_target_only_accept_kernel[(batch_size,)](
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ target_probs,
++ draft_probs,
++ metadata,
++ float(threshold_single),
++ threshold_acc,
++ num_draft_tokens=num_draft_tokens,
++ num_speculative_tokens=num_speculative_tokens,
++ vocab_size=vocab_size,
++ )
++ _tree_target_only_block_sum_kernel[(batch_size, num_vocab_blocks)](
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ num_draft_tokens=num_draft_tokens,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ )
++ _tree_target_only_sample_kernel[(batch_size,)](
++ predicts,
++ target_probs,
++ draft_probs,
++ metadata,
++ block_sums,
++ uniform_samples_for_final_sampling,
++ num_draft_tokens=num_draft_tokens,
++ vocab_size=vocab_size,
++ vocab_block_size=vocab_block_size,
++ num_vocab_blocks=num_vocab_blocks,
++ pad_num_vocab_blocks=pad_num_vocab_blocks,
++ )
+diff --git a/tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py b/tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py
+new file mode 100644
+index 0000000..40b04be
+--- /dev/null
++++ b/tests/python/sgl_kernel_npu/test_chain_speculative_sampling.py
+@@ -0,0 +1,138 @@
++import torch
++import torch_npu # noqa: F401
++
++from sgl_kernel_npu.sample import chain_speculative_sampling_rejection
++
++
++def chain_rejection_reference(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ draft_probs,
++):
++ batch_size, num_draft_tokens = candidates.shape
++ for req_idx in range(batch_size):
++ cur_prob_row = 0
++ last_accepted_idx = int(retrive_index[req_idx, 0])
++ accept_index[req_idx, 0] = last_accepted_idx
++ num_accepted = 0
++ all_accepted = True
++
++ for step in range(1, num_draft_tokens):
++ draft_token = int(candidates[req_idx, step])
++ p = float(target_probs[req_idx, cur_prob_row, draft_token])
++ q = float(draft_probs[req_idx, cur_prob_row, draft_token])
++ coin = float(uniform_samples[req_idx, step - 1])
++ if coin * q < p:
++ predicts[last_accepted_idx] = draft_token
++ num_accepted += 1
++ last_accepted_idx = int(retrive_index[req_idx, step])
++ accept_index[req_idx, num_accepted] = last_accepted_idx
++ cur_prob_row = step
++ else:
++ all_accepted = False
++ break
++
++ accept_token_num[req_idx] = num_accepted
++ residual = target_probs[req_idx, cur_prob_row].clone()
++ if not all_accepted:
++ residual.sub_(draft_probs[req_idx, cur_prob_row]).clamp_min_(0.0)
++ target = float(uniform_samples_for_final_sampling[req_idx]) * float(
++ residual.sum()
++ )
++ sampled_token = int((residual.cumsum(0) <= target).sum())
++ if sampled_token == residual.numel():
++ positive = torch.nonzero(residual > 0.0).flatten()
++ sampled_token = (
++ int(positive[-1]) if positive.numel() else residual.numel() - 1
++ )
++ predicts[last_accepted_idx] = sampled_token
++
++
++def test_chain_rejection_matches_gpu_algorithm():
++ batch_size, num_draft_tokens, vocab_size = 2, 4, 11
++ candidates = torch.tensor([[0, 2, 3, 4], [0, 5, 6, 7]])
++ retrive_index = torch.arange(batch_size * num_draft_tokens).view(
++ batch_size, num_draft_tokens
++ )
++ target_probs = torch.softmax(
++ torch.tensor(
++ [
++ [
++ [0.1, 0.2, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ [0.1, 0.2, 0.1, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ [0.1, 0.2, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ [0.1, 0.2, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
++ ],
++ [
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.2, 0.1, 2.0, 0.1, 0.1, 0.1],
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 2.0, 0.2, 0.1, 0.1, 0.1],
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1],
++ [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 2.0, 0.1, 0.1, 0.1],
++ ],
++ ],
++ dtype=torch.float32,
++ ),
++ dim=-1,
++ )
++ draft_probs = torch.softmax(
++ torch.tensor(
++ [
++ [[0.1] * vocab_size, [0.1] * vocab_size, [0.1] * vocab_size],
++ [[0.1] * vocab_size, [0.1] * vocab_size, [0.1] * vocab_size],
++ ],
++ dtype=torch.float32,
++ ),
++ dim=-1,
++ )
++ draft_probs[1, 0, 5] = 0.9
++ draft_probs[1, 0] /= draft_probs[1, 0].sum()
++ uniforms = torch.tensor([[0.1, 0.1, 0.1, 0.0], [0.99, 0.0, 0.0, 0.0]])
++ final_uniforms = torch.tensor([0.37, 0.61])
++
++ expected_predicts = torch.full(
++ (batch_size * num_draft_tokens,), -1, dtype=torch.int32
++ )
++ expected_accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32
++ )
++ expected_accept_num = torch.zeros(batch_size, dtype=torch.int32)
++ chain_rejection_reference(
++ expected_predicts,
++ expected_accept_index,
++ expected_accept_num,
++ candidates,
++ retrive_index,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ draft_probs,
++ )
++
++ predicts = torch.full_like(expected_predicts, -1, device="npu")
++ accept_index = torch.full_like(expected_accept_index, -1, device="npu")
++ accept_num = torch.zeros_like(expected_accept_num, device="npu")
++ next_token = torch.full_like(candidates, -1, device="npu")
++ next_sibling = torch.full_like(candidates, -1, device="npu")
++ chain_speculative_sampling_rejection(
++ predicts,
++ accept_index,
++ accept_num,
++ candidates.npu(),
++ retrive_index.npu(),
++ next_token,
++ next_sibling,
++ uniforms.npu(),
++ final_uniforms.npu(),
++ target_probs.npu(),
++ draft_probs.npu(),
++ )
++
++ torch.testing.assert_close(predicts.cpu(), expected_predicts)
++ torch.testing.assert_close(accept_index.cpu(), expected_accept_index)
++ torch.testing.assert_close(accept_num.cpu(), expected_accept_num)
+diff --git a/tests/python/sgl_kernel_npu/test_speculative_probability.py b/tests/python/sgl_kernel_npu/test_speculative_probability.py
+new file mode 100644
+index 0000000..563f174
+--- /dev/null
++++ b/tests/python/sgl_kernel_npu/test_speculative_probability.py
+@@ -0,0 +1,31 @@
++import torch
++
++from sgl_kernel_npu.sample.probability import top_k_top_p_renorm_probs
++
++
++def test_top_k_top_p_renorm_matches_sequential_reference():
++ torch.manual_seed(7)
++ probs = torch.softmax(torch.randn(4, 97), dim=-1)
++ top_ks = torch.tensor([1, 7, 31, 97])
++ top_ps = torch.tensor([0.3, 0.75, 0.95, 1.0])
++
++ actual = top_k_top_p_renorm_probs(
++ probs, top_ks, top_ps, True, True
++ )
++
++ sorted_probs, sorted_indices = probs.sort(dim=-1, descending=True)
++ positions = torch.arange(probs.shape[-1]).view(1, -1)
++ sorted_probs[positions >= top_ks.view(-1, 1)] = 0.0
++ sorted_probs /= sorted_probs.sum(dim=-1, keepdim=True)
++ top_k_probs = torch.zeros_like(probs).scatter(
++ -1, sorted_indices, sorted_probs
++ )
++ sorted_probs, sorted_indices = top_k_probs.sort(dim=-1, descending=True)
++ cumulative = sorted_probs.cumsum(dim=-1)
++ sorted_probs[cumulative - sorted_probs > top_ps.view(-1, 1)] = 0.0
++ sorted_probs /= sorted_probs.sum(dim=-1, keepdim=True)
++ expected = torch.zeros_like(probs).scatter(
++ -1, sorted_indices, sorted_probs
++ )
++
++ torch.testing.assert_close(actual, expected, rtol=1e-6, atol=1e-7)
+diff --git a/tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py b/tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py
+new file mode 100644
+index 0000000..3c12d53
+--- /dev/null
++++ b/tests/python/sgl_kernel_npu/test_tree_speculative_sampling_target_only.py
+@@ -0,0 +1,595 @@
++import argparse
++import time
++
++import pytest
++import torch
++import torch_npu # noqa: F401
++
++from sgl_kernel_npu.sample import tree_speculative_sampling_target_only
++
++
++def target_only_tree_reference(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ rejected_probs,
++ threshold_single,
++ threshold_acc,
++):
++ """CPU reference translated directly from the GPU CUDA kernel."""
++ batch_size, num_draft_tokens = candidates.shape
++ num_speculative_tokens = accept_index.shape[1]
++ threshold_acc = max(float(threshold_acc), 1e-9)
++ rejected_probs.zero_()
++
++ for req_idx in range(batch_size):
++ cur_prob_row = 0
++ cur_node = 0
++ coin = float(uniform_samples[req_idx, 0])
++ last_accepted_idx = int(retrive_index[req_idx, 0])
++ accept_index[req_idx, 0] = last_accepted_idx
++ num_accepted = 0
++
++ for _ in range(1, num_speculative_tokens):
++ cur_node = int(retrive_next_token[req_idx, cur_node])
++ prob_acc = 0.0
++ while cur_node != -1:
++ draft_idx = int(retrive_index[req_idx, cur_node])
++ draft_token = int(candidates[req_idx, cur_node])
++ target_prob = float(
++ target_probs[req_idx, cur_prob_row, draft_token]
++ )
++ prob_acc += target_prob
++ if (
++ coin <= prob_acc / threshold_acc
++ or target_prob >= threshold_single
++ ):
++ predicts[last_accepted_idx] = draft_token
++ num_accepted += 1
++ accept_index[req_idx, num_accepted] = draft_idx
++ last_accepted_idx = draft_idx
++ cur_prob_row = cur_node
++ coin = float(uniform_samples[req_idx, cur_node])
++ break
++
++ rejected_probs[req_idx, cur_prob_row, draft_token] = target_prob
++ cur_node = int(retrive_next_sibling[req_idx, cur_node])
++
++ if cur_node == -1:
++ break
++
++ accept_token_num[req_idx] = num_accepted
++ residual = (
++ target_probs[req_idx, cur_prob_row]
++ - rejected_probs[req_idx, cur_prob_row]
++ ).clamp_min(0.0)
++ target = float(uniform_samples_for_final_sampling[req_idx]) * float(
++ residual.sum()
++ )
++ sampled_token = int((residual.cumsum(0) <= target).sum())
++ if sampled_token == residual.numel():
++ positive = torch.nonzero(residual > 0.0).flatten()
++ sampled_token = (
++ int(positive[-1]) if positive.numel() else residual.numel() - 1
++ )
++ predicts[last_accepted_idx] = sampled_token
++
++ return predicts, accept_index, accept_token_num, rejected_probs
++
++
++def target_only_chain_reference(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++ threshold_single,
++ threshold_acc,
++):
++ batch_size, num_draft_tokens = candidates.shape
++ threshold_acc = max(float(threshold_acc), 1e-9)
++
++ for req_idx in range(batch_size):
++ last_accepted_idx = int(retrive_index[req_idx, 0])
++ accept_index[req_idx, 0] = last_accepted_idx
++ num_accepted = 0
++ rejected_token = -1
++
++ for step in range(1, num_draft_tokens):
++ draft_token = int(candidates[req_idx, step])
++ target_prob = float(target_probs[req_idx, step - 1, draft_token])
++ coin = float(uniform_samples[req_idx, step - 1])
++ if (
++ coin <= target_prob / threshold_acc
++ or target_prob >= threshold_single
++ ):
++ predicts[last_accepted_idx] = draft_token
++ num_accepted += 1
++ last_accepted_idx = int(retrive_index[req_idx, step])
++ accept_index[req_idx, num_accepted] = last_accepted_idx
++ else:
++ rejected_token = draft_token
++ break
++
++ accept_token_num[req_idx] = num_accepted
++ final_probs = target_probs[req_idx, num_accepted].clone().float()
++ if rejected_token >= 0:
++ final_probs[rejected_token] = 0.0
++ final_probs.clamp_min_(0.0)
++ target = float(uniform_samples_for_final_sampling[req_idx]) * float(
++ final_probs.sum()
++ )
++ sampled_token = int((final_probs.cumsum(0) <= target).sum())
++ sampled_token = min(sampled_token, final_probs.numel() - 1)
++ predicts[last_accepted_idx] = sampled_token
++
++ return predicts, accept_index, accept_token_num
++
++
++@pytest.mark.parametrize(
++ "threshold_single,threshold_acc", [(1.0, 1.0), (0.0, 0.0), (0.5, 0.8)]
++)
++def test_general_tree_matches_gpu_algorithm_reference(
++ threshold_single, threshold_acc
++):
++ candidates = torch.tensor(
++ [[0, 1, 2, 3, 4, 5], [7, 8, 9, 10, 11, 12]], dtype=torch.int64
++ )
++ retrive_index = torch.tensor(
++ [[0, 1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11]], dtype=torch.int64
++ )
++ retrive_next_token = torch.tensor(
++ [[1, 2, -1, 4, 5, -1], [4, 2, 3, -1, 5, -1]],
++ dtype=torch.int64,
++ )
++ retrive_next_sibling = torch.tensor(
++ [[-1, 3, -1, -1, -1, -1], [-1, -1, -1, -1, 1, -1]],
++ dtype=torch.int64,
++ )
++ batch_size, num_draft_tokens = candidates.shape
++ vocab_size = 20
++ target_probs = torch.full(
++ (batch_size, num_draft_tokens, vocab_size), 0.01, dtype=torch.float32
++ )
++ target_probs[0, 0, 1] = 0.12
++ target_probs[0, 0, 3] = 0.72
++ target_probs[0, 3, 4] = 0.82
++ target_probs[0, 4, 5] = 0.75
++ target_probs[1, 0, 11] = 0.68
++ target_probs[1, 0, 8] = 0.14
++ target_probs[1, 4, 12] = 0.77
++ target_probs /= target_probs.sum(dim=-1, keepdim=True)
++ uniforms = torch.tensor(
++ [[0.55, 0.2, 0.8, 0.4, 0.3, 0.9], [0.6, 0.2, 0.8, 0.7, 0.3, 0.4]],
++ dtype=torch.float32,
++ )
++ final_uniforms = torch.tensor([0.25, 0.75], dtype=torch.float32)
++
++ ref_predicts = torch.full((12,), -1, dtype=torch.int32)
++ ref_accept_index = torch.full((2, 4), -1, dtype=torch.int32)
++ ref_accept_num = torch.zeros(2, dtype=torch.int32)
++ ref_rejected = torch.empty_like(target_probs)
++ target_only_tree_reference(
++ ref_predicts,
++ ref_accept_index,
++ ref_accept_num,
++ candidates,
++ retrive_index,
++ retrive_next_token,
++ retrive_next_sibling,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ ref_rejected,
++ threshold_single,
++ threshold_acc,
++ )
++
++ npu_predicts = torch.full_like(ref_predicts, -1, device="npu")
++ npu_accept_index = torch.full_like(ref_accept_index, -1, device="npu")
++ npu_accept_num = torch.zeros_like(ref_accept_num, device="npu")
++ npu_rejected = torch.empty_like(target_probs, device="npu")
++ tree_speculative_sampling_target_only(
++ npu_predicts,
++ npu_accept_index,
++ npu_accept_num,
++ candidates.npu(),
++ retrive_index.npu(),
++ retrive_next_token.npu(),
++ retrive_next_sibling.npu(),
++ uniforms.npu(),
++ final_uniforms.npu(),
++ target_probs.npu(),
++ npu_rejected,
++ threshold_single,
++ threshold_acc,
++ True,
++ )
++
++ torch.testing.assert_close(npu_predicts.cpu(), ref_predicts, rtol=0, atol=0)
++ torch.testing.assert_close(
++ npu_accept_index.cpu(), ref_accept_index, rtol=0, atol=0
++ )
++ torch.testing.assert_close(npu_accept_num.cpu(), ref_accept_num, rtol=0, atol=0)
++ torch.testing.assert_close(npu_rejected.cpu(), ref_rejected, rtol=0, atol=0)
++
++
++def target_only_chain_torch(
++ predicts,
++ accept_index,
++ accept_token_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ uniform_samples_for_final_sampling,
++ target_probs,
++):
++ batch_size, num_draft_tokens = candidates.shape
++ device = candidates.device
++ draft_tokens = candidates[:, 1:].long()
++ step_probs = torch.gather(
++ target_probs[:, :-1, :], 2, draft_tokens.unsqueeze(-1)
++ ).squeeze(-1)
++ accept_steps = uniform_samples[:, : num_draft_tokens - 1] <= step_probs
++ reject_count = (~accept_steps).to(torch.int32).cumsum(dim=1)
++ num_correct = (reject_count == 0).to(torch.int32).sum(dim=1)
++
++ accept_token_num.copy_(num_correct)
++ accept_index.fill_(-1)
++ positions = torch.arange(num_draft_tokens, device=device).view(1, -1)
++ valid_accept = positions <= num_correct.view(-1, 1)
++ accept_index.copy_(
++ torch.where(
++ valid_accept,
++ retrive_index.to(torch.int32),
++ torch.full_like(accept_index, -1),
++ )
++ )
++
++ predicts.zero_()
++ parent_positions = torch.arange(num_draft_tokens - 1, device=device).view(1, -1)
++ valid_parent = parent_positions < num_correct.view(-1, 1)
++ parent_indices = retrive_index[:, :-1].reshape(-1).long()
++ parent_values = candidates[:, 1:].to(torch.int32).reshape(-1)
++ predicts[parent_indices] = torch.where(
++ valid_parent.reshape(-1), parent_values, predicts[parent_indices]
++ )
++
++ rows = torch.arange(batch_size, device=device)
++ final_rows = num_correct.long()
++ final_probs = target_probs[rows, final_rows].clone()
++ rejected = num_correct < num_draft_tokens - 1
++ rejected_positions = (num_correct.long() + 1).clamp_max(num_draft_tokens - 1)
++ rejected_tokens = candidates[rows, rejected_positions].long()
++ final_probs[rejected, rejected_tokens[rejected]] = 0.0
++
++ probability_sums = final_probs.sum(dim=-1, keepdim=True)
++ targets = uniform_samples_for_final_sampling.view(-1, 1) * probability_sums
++ final_tokens = (
++ (final_probs.cumsum(dim=-1) <= targets)
++ .to(torch.int32)
++ .sum(dim=-1)
++ .clamp_max(target_probs.shape[-1] - 1)
++ )
++ final_indices = retrive_index[rows, final_rows].long()
++ predicts[final_indices] = final_tokens.to(torch.int32)
++
++
++def make_chain_indices(batch_size, num_draft_tokens, device):
++ retrive_index = torch.arange(
++ batch_size * num_draft_tokens, dtype=torch.int64, device=device
++ ).view(batch_size, num_draft_tokens)
++ retrive_next_token = torch.arange(
++ 1, num_draft_tokens + 1, dtype=torch.int64, device=device
++ ).repeat(batch_size, 1)
++ retrive_next_token[:, -1] = -1
++ retrive_next_sibling = torch.full_like(retrive_next_token, -1)
++ return retrive_index, retrive_next_token, retrive_next_sibling
++
++
++def make_stable_chain_final_uniforms(
++ candidates,
++ uniform_samples,
++ target_probs,
++):
++ """Choose final-sampling coins away from inverse-CDF boundaries."""
++ batch_size, num_draft_tokens = candidates.shape
++ final_uniforms = torch.empty(batch_size, dtype=torch.float32)
++
++ for req_idx in range(batch_size):
++ num_accepted = 0
++ rejected_token = -1
++ for step in range(1, num_draft_tokens):
++ draft_token = int(candidates[req_idx, step])
++ target_prob = float(target_probs[req_idx, step - 1, draft_token])
++ if float(uniform_samples[req_idx, step - 1]) <= target_prob:
++ num_accepted += 1
++ else:
++ rejected_token = draft_token
++ break
++
++ final_probs = target_probs[req_idx, num_accepted].double().clone()
++ if rejected_token >= 0:
++ final_probs[rejected_token] = 0.0
++
++ sampled_token = int(final_probs.argmax())
++ probability_sum = final_probs.sum()
++ cdf_before = final_probs[:sampled_token].sum()
++ cdf_midpoint = cdf_before + final_probs[sampled_token] * 0.5
++ final_uniforms[req_idx] = (cdf_midpoint / probability_sum).float()
++
++ return final_uniforms
++
++
++@pytest.mark.parametrize("batch_size", [1, 4, 17])
++@pytest.mark.parametrize("num_draft_tokens", [2, 5])
++@pytest.mark.parametrize("vocab_size", [20, 32000, 151552])
++def test_target_only_chain_matches_reference(
++ batch_size, num_draft_tokens, vocab_size
++):
++ torch.manual_seed(20260717 + batch_size + num_draft_tokens + vocab_size)
++ candidates = torch.randint(
++ 0, vocab_size, (batch_size, num_draft_tokens), dtype=torch.int64
++ )
++ logits = torch.randn(batch_size, num_draft_tokens, vocab_size)
++ target_probs = torch.softmax(logits, dim=-1).float()
++
++ # Give some draft tokens meaningful acceptance probability.
++ for req_idx in range(batch_size):
++ for step in range(1, num_draft_tokens):
++ token = int(candidates[req_idx, step])
++ target_probs[req_idx, step - 1] *= 0.35
++ target_probs[req_idx, step - 1, token] += 0.65
++ target_probs[req_idx, step - 1] /= target_probs[
++ req_idx, step - 1
++ ].sum()
++
++ uniform_samples = torch.rand(batch_size, num_draft_tokens)
++ # A random coin can land within FP32 reduction error of a CDF boundary for
++ # large vocabularies. Use the midpoint of a high-mass token's interval so
++ # exact token equality tests the algorithm instead of reduction order.
++ final_uniform_samples = make_stable_chain_final_uniforms(
++ candidates,
++ uniform_samples,
++ target_probs,
++ )
++ retrive_index, retrive_next_token, retrive_next_sibling = make_chain_indices(
++ batch_size, num_draft_tokens, "cpu"
++ )
++
++ ref_predicts = torch.full(
++ (batch_size * num_draft_tokens,), -1, dtype=torch.int32
++ )
++ ref_accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32
++ )
++ ref_accept_num = torch.zeros(batch_size, dtype=torch.int32)
++ target_only_chain_reference(
++ ref_predicts,
++ ref_accept_index,
++ ref_accept_num,
++ candidates,
++ retrive_index,
++ uniform_samples,
++ final_uniform_samples,
++ target_probs,
++ 1.0,
++ 1.0,
++ )
++
++ npu_predicts = torch.full_like(ref_predicts, -1, device="npu")
++ npu_accept_index = torch.full_like(ref_accept_index, -1, device="npu")
++ npu_accept_num = torch.zeros_like(ref_accept_num, device="npu")
++ candidates_npu = candidates.npu()
++ retrive_index_npu = retrive_index.npu()
++ next_token_npu = retrive_next_token.npu()
++ next_sibling_npu = retrive_next_sibling.npu()
++ target_probs_npu = target_probs.npu()
++
++ tree_speculative_sampling_target_only(
++ predicts=npu_predicts,
++ accept_index=npu_accept_index,
++ accept_token_num=npu_accept_num,
++ candidates=candidates_npu,
++ retrive_index=retrive_index_npu,
++ retrive_next_token=next_token_npu,
++ retrive_next_sibling=next_sibling_npu,
++ uniform_samples=uniform_samples.npu(),
++ uniform_samples_for_final_sampling=final_uniform_samples.npu(),
++ target_probs=target_probs_npu,
++ draft_probs=torch.empty_like(target_probs_npu),
++ threshold_single=1.0,
++ threshold_acc=1.0,
++ deterministic=True,
++ )
++
++ torch.testing.assert_close(npu_predicts.cpu(), ref_predicts, rtol=0, atol=0)
++ torch.testing.assert_close(
++ npu_accept_index.cpu(), ref_accept_index, rtol=0, atol=0
++ )
++ torch.testing.assert_close(npu_accept_num.cpu(), ref_accept_num, rtol=0, atol=0)
++
++
++@pytest.mark.parametrize(
++ "threshold_single,threshold_acc",
++ [(1.0, 1.0), (0.0, 0.0), (0.5, 0.8)],
++)
++def test_target_only_thresholds(threshold_single, threshold_acc):
++ batch_size, num_draft_tokens, vocab_size = 2, 4, 32
++ candidates = torch.tensor([[0, 3, 4, 5], [0, 7, 8, 9]], dtype=torch.int64)
++ target_probs = torch.full(
++ (batch_size, num_draft_tokens, vocab_size), 1.0 / vocab_size
++ )
++ for req_idx in range(batch_size):
++ for step in range(1, num_draft_tokens):
++ token = int(candidates[req_idx, step])
++ target_probs[req_idx, step - 1] *= 0.2
++ target_probs[req_idx, step - 1, token] += 0.8
++ target_probs[req_idx, step - 1] /= target_probs[
++ req_idx, step - 1
++ ].sum()
++
++ uniforms = torch.tensor([[0.1, 0.9, 0.2, 0.0], [0.7, 0.2, 0.95, 0.0]])
++ final_uniforms = torch.tensor([0.25, 0.75])
++ retrive_index, next_token, next_sibling = make_chain_indices(
++ batch_size, num_draft_tokens, "cpu"
++ )
++
++ ref_predicts = torch.full((batch_size * num_draft_tokens,), -1, dtype=torch.int32)
++ ref_accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32
++ )
++ ref_accept_num = torch.zeros(batch_size, dtype=torch.int32)
++ target_only_chain_reference(
++ ref_predicts,
++ ref_accept_index,
++ ref_accept_num,
++ candidates,
++ retrive_index,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ threshold_single,
++ threshold_acc,
++ )
++
++ npu_predicts = torch.full_like(ref_predicts, -1, device="npu")
++ npu_accept_index = torch.full_like(ref_accept_index, -1, device="npu")
++ npu_accept_num = torch.zeros_like(ref_accept_num, device="npu")
++ target_probs_npu = target_probs.npu()
++ tree_speculative_sampling_target_only(
++ npu_predicts,
++ npu_accept_index,
++ npu_accept_num,
++ candidates.npu(),
++ retrive_index.npu(),
++ next_token.npu(),
++ next_sibling.npu(),
++ uniforms.npu(),
++ final_uniforms.npu(),
++ target_probs_npu,
++ torch.empty_like(target_probs_npu),
++ threshold_single,
++ threshold_acc,
++ True,
++ )
++
++ torch.testing.assert_close(npu_predicts.cpu(), ref_predicts, rtol=0, atol=0)
++ torch.testing.assert_close(
++ npu_accept_index.cpu(), ref_accept_index, rtol=0, atol=0
++ )
++ torch.testing.assert_close(npu_accept_num.cpu(), ref_accept_num, rtol=0, atol=0)
++
++
++def run_benchmark(batch_size, num_draft_tokens, vocab_size, warmup, iterations):
++ candidates = torch.randint(
++ 0,
++ vocab_size,
++ (batch_size, num_draft_tokens),
++ dtype=torch.int64,
++ device="npu",
++ )
++ target_probs = torch.softmax(
++ torch.randn(
++ batch_size,
++ num_draft_tokens,
++ vocab_size,
++ dtype=torch.float32,
++ device="npu",
++ ),
++ dim=-1,
++ )
++ retrive_index, next_token, next_sibling = make_chain_indices(
++ batch_size, num_draft_tokens, "npu"
++ )
++ uniforms = torch.rand(
++ batch_size, num_draft_tokens, dtype=torch.float32, device="npu"
++ )
++ final_uniforms = torch.rand(batch_size, dtype=torch.float32, device="npu")
++ draft_probs = torch.empty_like(target_probs)
++ predicts = torch.zeros(
++ batch_size * num_draft_tokens, dtype=torch.int32, device="npu"
++ )
++ accept_index = torch.full(
++ (batch_size, num_draft_tokens), -1, dtype=torch.int32, device="npu"
++ )
++ accept_num = torch.zeros(batch_size, dtype=torch.int32, device="npu")
++
++ def run_kernel():
++ tree_speculative_sampling_target_only(
++ predicts,
++ accept_index,
++ accept_num,
++ candidates,
++ retrive_index,
++ next_token,
++ next_sibling,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ draft_probs,
++ 1.0,
++ 1.0,
++ True,
++ )
++
++ def run_torch():
++ target_only_chain_torch(
++ predicts,
++ accept_index,
++ accept_num,
++ candidates,
++ retrive_index,
++ uniforms,
++ final_uniforms,
++ target_probs,
++ )
++
++ def benchmark(fn):
++ for _ in range(warmup):
++ fn()
++ torch.npu.synchronize()
++ started = time.perf_counter()
++ for _ in range(iterations):
++ fn()
++ torch.npu.synchronize()
++ return (time.perf_counter() - started) * 1000 / iterations
++
++ kernel_latency_ms = benchmark(run_kernel)
++ torch_latency_ms = benchmark(run_torch)
++ print(
++ f"batch={batch_size} drafts={num_draft_tokens} vocab={vocab_size} "
++ f"kernel_ms={kernel_latency_ms:.4f} torch_ms={torch_latency_ms:.4f} "
++ f"speedup={torch_latency_ms / kernel_latency_ms:.2f}x"
++ )
++
++
++if __name__ == "__main__":
++ parser = argparse.ArgumentParser()
++ parser.add_argument("--perf", action="store_true")
++ parser.add_argument("--batch-size", type=int, default=16)
++ parser.add_argument("--num-draft-tokens", type=int, default=5)
++ parser.add_argument("--vocab-size", type=int, default=151552)
++ parser.add_argument("--warmup", type=int, default=10)
++ parser.add_argument("--iterations", type=int, default=100)
++ args = parser.parse_args()
++ if args.perf:
++ run_benchmark(
++ args.batch_size,
++ args.num_draft_tokens,
++ args.vocab_size,
++ args.warmup,
++ args.iterations,
++ )
++ else:
++ raise SystemExit(pytest.main([__file__]))
diff --git a/docker/npu_patch/sglang-npu.patch b/docker/npu_patch/sglang-npu.patch
index de38657b6..52cbe46e0 100644
--- a/docker/npu_patch/sglang-npu.patch
+++ b/docker/npu_patch/sglang-npu.patch
@@ -1,56 +1,1058 @@
+diff --git a/python/sglang/srt/entrypoints/http_server.py b/python/sglang/srt/entrypoints/http_server.py
+index bd9d7eafa1..1740283de2 100644
+--- a/python/sglang/srt/entrypoints/http_server.py
++++ b/python/sglang/srt/entrypoints/http_server.py
+@@ -598,7 +598,7 @@ async def health_generate(request: Request) -> Response:
+ ):
+ return Response(status_code=200)
+
+- sampling_params = {"max_new_tokens": 1, "temperature": 0.0}
++ sampling_params = {"max_new_tokens": 1, "temperature": 1.0,"top_k":-1,"top_p":1.0}
+ # uuid keeps rids unique across tokenizer workers (a bare time.time() can
+ # collide and crash the shared DetokenizerManager decode_status).
+ rid = f"{HEALTH_CHECK_RID_PREFIX}_{uuid.uuid4().hex}"
+diff --git a/python/sglang/srt/hardware_backend/npu/attention/ascend_gdn_backend.py b/python/sglang/srt/hardware_backend/npu/attention/ascend_gdn_backend.py
+index 7f506e7f1a..204b9382e8 100644
+--- a/python/sglang/srt/hardware_backend/npu/attention/ascend_gdn_backend.py
++++ b/python/sglang/srt/hardware_backend/npu/attention/ascend_gdn_backend.py
+@@ -5,11 +5,6 @@ from sgl_kernel_npu.fla.fused_gdn_gating import (
+ fused_gdn_gating_kernel_without_sigmoid,
+ fused_gdn_gating_npu,
+ )
+-from sgl_kernel_npu.mamba.causal_conv1d import (
+- causal_conv1d_fn_npu,
+- causal_conv1d_update_npu,
+- causal_conv1d_update_v2,
+-)
+
+ from sglang.srt.hardware_backend.npu.attention.ascend_hybrid_linear_attn_backend import (
+ AscendMambaAttnBackendBase,
+@@ -26,8 +21,6 @@ from sglang.srt.model_executor.model_runner import ModelRunner
+ from sglang.srt.speculative.eagle_info import EagleDraftInput, EagleVerifyInput
+
+ fused_gdn_gating = fused_gdn_gating_npu
+-causal_conv1d_fn = causal_conv1d_fn_npu
+-causal_conv1d_update = causal_conv1d_update_npu
+
+
+ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
+@@ -109,6 +102,13 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
+ self._prepare_mamba_track_metadata(forward_batch)
+ self.graph_mode = False
+
++ def _get_conv_weights_t(self, layer: RadixLinearAttention) -> torch.Tensor:
++ w = getattr(layer, "_conv_weights_t", None)
++ if w is None:
++ w = layer.conv_weights.transpose(0, 1).contiguous()
++ layer._conv_weights_t = w
++ return w
++
+ def forward_decode(
+ self,
+ layer: RadixLinearAttention,
+@@ -125,16 +125,17 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
+ cache_indices = self.forward_metadata.mamba_cache_indices
+
+ assert isinstance(mixed_qkv, torch.Tensor)
+- conv_states_tmp = conv_states.transpose(1, 2).clone()
+- mixed_qkv = causal_conv1d_update(
++ mixed_qkv = torch.ops.npu.causal_conv1d(
+ mixed_qkv,
+- conv_states_tmp,
+- layer.conv_weights,
+- layer.bias,
+- layer.activation,
+- conv_state_indices=cache_indices,
++ self._get_conv_weights_t(layer),
++ conv_states=conv_states,
++ bias=layer.bias,
++ query_start_loc=query_start_loc,
++ cache_indices=cache_indices,
++ activation_mode=1,
++ pad_slot_id=-1,
++ run_mode=1,
+ )
+- conv_states[:] = conv_states_tmp.transpose(1, 2)
+
+ query, key, value = torch.split(
+ mixed_qkv,
+@@ -219,44 +220,41 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
+ dtype=torch.int32,
+ device=mixed_qkv.device,
+ )
+- mixed_qkv = causal_conv1d_update_v2(
+- x=mixed_qkv.view(batch_size, draft_token_num, -1).contiguous(),
+- conv_state=conv_states.contiguous(),
+- weight=layer.conv_weights.transpose(0, 1).contiguous(),
++ mixed_qkv = torch.ops.npu.causal_conv1d(
++ mixed_qkv,
++ self._get_conv_weights_t(layer),
++ conv_states=conv_states,
+ bias=layer.bias,
+- activation=layer.activation,
+- conv_state_indices=cache_indices,
++ query_start_loc=query_start_loc,
++ cache_indices=cache_indices,
+ num_accepted_tokens=num_accepted_tokens,
++ activation_mode=1,
+ pad_slot_id=-1,
+- validate_data=False,
+- ).view(seq_len, -1)
++ run_mode=1,
++ )
+ else:
+- mixed_qkv = mixed_qkv.transpose(0, 1)
+ if forward_metadata.has_mamba_track_mask:
+- mixed_qkv_to_track = mixed_qkv[
+- :, forward_metadata.track_conv_indices
+- ].transpose(0, 1)
+- conv_states.transpose(1, 2)[
+- forward_metadata.conv_states_mask_indices
+- ] = mixed_qkv_to_track
++ mixed_qkv_to_track = mixed_qkv[forward_metadata.track_conv_indices]
++ conv_states[forward_metadata.conv_states_mask_indices] = (
++ mixed_qkv_to_track
++ )
+ kernel_size = layer.conv_weights.shape[-1]
+- conv_states_for_prefill = conv_states[:, -(kernel_size - 1) :, :]
+- conv_states_tmp = conv_states_for_prefill.transpose(1, 2).contiguous()
+-
+- mixed_qkv = causal_conv1d_fn(
++ conv_states_for_prefill = conv_states[
++ :, -(kernel_size - 1) :, :
++ ].contiguous()
++ mixed_qkv = torch.ops.npu.causal_conv1d(
+ mixed_qkv,
+- layer.conv_weights,
+- layer.bias,
+- activation=layer.activation,
+- conv_states=conv_states_tmp,
+- has_initial_state=has_initial_states,
+- cache_indices=cache_indices,
++ self._get_conv_weights_t(layer),
++ conv_states=conv_states_for_prefill,
++ bias=layer.bias,
+ query_start_loc=query_start_loc,
+- seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
+- ).transpose(0, 1)[:seq_len]
+- conv_states[:, -(kernel_size - 1) :, :] = conv_states_tmp.transpose(
+- 1, 2
+- ).contiguous()
++ cache_indices=cache_indices,
++ has_initial_state=has_initial_states,
++ activation_mode=1,
++ pad_slot_id=-1,
++ run_mode=0,
++ )
++ conv_states[:, -(kernel_size - 1) :, :] = conv_states_for_prefill
+ if is_target_verify:
+ g, beta = fused_gdn_gating_kernel_without_sigmoid(
+ layer.A_log, a, b, layer.dt_bias
+diff --git a/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py b/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py
+index 919f46619a..c8a185388a 100644
+--- a/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py
++++ b/python/sglang/srt/hardware_backend/npu/graph_runner/npu_cudagraph_backend.py
+@@ -37,6 +37,21 @@ if TYPE_CHECKING:
+ BaseCudaGraphRunner,
+ )
+
++@contextmanager
++def _disable_tms_during_graph_capture():
++ try:
++ from torch_memory_saver import torch_memory_saver
++ _impl = torch_memory_saver._impl
++ except Exception:
++ _impl = None
++
++ if _impl is not None:
++ _impl._binary_wrapper.cdll.tms_set_interesting_region(False)
++ try:
++ yield
++ finally:
++ if _impl is not None:
++ _impl._binary_wrapper.cdll.tms_set_interesting_region(True)
+
+ class NPUCudaGraphBackend(BaseCudaGraphBackend):
+ """One torch.npu.NPUGraph per shape; attention metadata captured
+@@ -117,7 +132,8 @@ class NPUCudaGraphBackend(BaseCudaGraphBackend):
+ stream=self._capture_stream,
+ auto_dispatch_capture=True,
+ ):
+- out = forward_fn()
++ with _disable_tms_during_graph_capture():
++ out = forward_fn()
+
+ self._graphs[shape_key] = graph
+ self._outputs[shape_key] = out
diff --git a/python/sglang/srt/layers/quantization/unquant.py b/python/sglang/srt/layers/quantization/unquant.py
-index 1ade4ed9e4..0161bd398a 100644
+index 82a3d77f05..25d4c21406 100644
--- a/python/sglang/srt/layers/quantization/unquant.py
+++ b/python/sglang/srt/layers/quantization/unquant.py
-@@ -314,12 +314,6 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
+@@ -402,10 +402,10 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
+ layer.w2_weight.data = layer.w2_weight.data.reshape(
layer.num_local_experts, *new_shape_w2
)
-
- if _is_npu:
- for weight_name in ["w13_weight", "w2_weight"]:
- weight = getattr(layer, weight_name)
-- weight.data = weight.data.transpose(1, 2)
-- weight.data = npu_format_cast(weight.data)
--
+- weight.data = npu_format_cast(weight)
++ # if _is_npu:
++ # for weight_name in ["w13_weight", "w2_weight"]:
++ # weight = getattr(layer, weight_name)
++ # weight.data = npu_format_cast(weight)
+
return
- def maybe_restore_flashinfer_trtllm_bf16_weight_shape_for_load(
-@@ -646,7 +640,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
- # gmm1: gate_up_proj
- hidden_states = torch.ops.npu.npu_grouped_matmul(
- x=[hidden_states],
-- weight=[layer.w13_weight],
-+ weight=[layer.w13_weight.transpose(1, 2)],
- bias=w13_bias,
- split_item=2,
- group_list_type=1,
-@@ -670,7 +664,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
- # gmm2: down_proj
- hidden_states = torch.ops.npu.npu_grouped_matmul(
- x=[hidden_states],
-- weight=[layer.w2_weight],
-+ weight=[layer.w2_weight.transpose(1, 2)],
- bias=w2_bias,
- split_item=2,
- group_list_type=1,
+diff --git a/python/sglang/srt/layers/sampler.py b/python/sglang/srt/layers/sampler.py
+index e83e4157fa..19ed3f244b 100644
+--- a/python/sglang/srt/layers/sampler.py
++++ b/python/sglang/srt/layers/sampler.py
+@@ -81,6 +81,170 @@ class Sampler(nn.Module):
+ # In RL on-policy mode, we use log_softmax to compute logprobs to match the trainer.
+ self.use_log_softmax_logprob = self.rl_on_policy_target is not None
+ self.use_ascend_backend = get_flags().sampling_backend == "ascend"
++ # Generate uniform noise on a side stream while the model is running, and
++ # apply the exponential-race transform at consumption time on the main
++ # stream, exactly matching stock aten::exponential_ semantics (NPU
++ # op-plugin composite): x = min(1 - u, 1 - eps/2), q = -log(x).
++ # uniform_() decomposes to DSARandomUniform plus an async
++ # D2D copy, i.e. pure DSA-engine work with no AIV transform kernels, so
++ # the side stream cannot contend with the model's vector-core kernels
++ # even when it overlaps the next forward (v1 ran the full exponential_()
++ # chain here and its AIV tail contended with forward).
++ # This is opt-in because it changes the random-number sequence, although it
++ # preserves the categorical sampling distribution.
++ self.enable_async_exponential = is_npu() and get_bool_env_var(
++ "SGLANG_NPU_ASYNC_EXPONENTIAL"
++ )
++ self._async_exponential_stream = None
++ self._async_exponential_event = None
++ self._async_exponential_u = None
++ self._async_exponential_pending = False
++ self._async_exp_min_bound = None
++
++ def can_prepare_async_exponential(
++ self, sampling_info: SamplingBatchInfo
++ ) -> bool:
++ """Return whether this batch can use precomputed uniform noise."""
++ return (
++ self.enable_async_exponential
++ and not sampling_info.is_all_greedy
++ and sampling_info.sampling_seed is None
++ and not sampling_info.need_top_p_sampling
++ and not sampling_info.need_top_k_sampling
++ and not sampling_info.need_min_p_sampling
++ )
++
++ @torch.no_grad()
++ def prepare_async_exponential(
++ self,
++ batch_size: int,
++ vocab_size: int,
++ sampling_info: SamplingBatchInfo,
++ device: torch.device,
++ ) -> bool:
++ """Enqueue U(0,1) noise before model forward on a dedicated NPU stream.
++
++ Reusing the buffer is safe because the side stream first waits for all
++ previously enqueued work on the current stream. The sampling path later
++ inserts a device-side event wait; it never synchronizes the CPU.
++ """
++ if not self.can_prepare_async_exponential(sampling_info):
++ return False
++
++ # A delayed sampler may still own the previous buffer. Do not overwrite it.
++ if self._async_exponential_pending:
++ logger.warning(
++ "Skip async exponential preparation because the previous batch "
++ "has not consumed its random buffer"
++ )
++ return False
++
++ if self._async_exponential_stream is None:
++ self._async_exponential_stream = torch.npu.Stream()
++ self._async_exponential_event = torch.npu.Event()
++ logger.info(
++ "Enabled asynchronous NPU exponential-race sampling "
++ "(uniform noise on side stream, stock-exponential argmax consumption)"
++ )
++
++ current_stream = torch.npu.current_stream()
++ self._async_exponential_stream.wait_stream(current_stream)
++
++ expected_shape = (batch_size, vocab_size)
++ with torch.npu.stream(self._async_exponential_stream):
++ u = self._async_exponential_u
++ if (
++ u is None
++ or tuple(u.shape) != expected_shape
++ or u.dtype != torch.float32
++ or u.device.type != torch.device(device).type
++ ):
++ # SGLang converts next-token logits to FP32 before sampling; the
++ # noise grid must match CANN's fp32 uniform to preserve the
++ # incumbent sampling distribution.
++ u = torch.empty(
++ expected_shape,
++ dtype=torch.float32,
++ device=device,
++ )
++ self._async_exponential_u = u
++ u.uniform_()
++ self._async_exponential_event.record()
++
++ self._async_exponential_pending = True
++ return True
++
++ def _sample_with_async_exponential(
++ self, probs: torch.Tensor
++ ) -> Optional[torch.Tensor]:
++ """Consume precomputed U(0,1) noise using the exponential-race identity.
++
++ The full stock exponential_ transform (complement + clamp + -log) runs
++ here on the main stream (serial work that the sampling path owns
++ anyway), keeping the side stream free of AIV kernels so it cannot
++ slow down the model forward.
++ """
++ if not self._async_exponential_pending:
++ return None
++
++ u = self._async_exponential_u
++ self._async_exponential_pending = False
++ if (
++ u is None
++ or u.shape != probs.shape
++ or u.dtype != probs.dtype
++ or u.device != probs.device
++ ):
++ logger.warning(
++ "Async uniform buffer does not match probs; falling back to "
++ "torch.multinomial (u=%s/%s/%s, probs=%s/%s/%s)",
++ None if u is None else tuple(u.shape),
++ None if u is None else u.dtype,
++ None if u is None else u.device,
++ tuple(probs.shape),
++ probs.dtype,
++ probs.device,
++ )
++ return None
++
++ current_stream = torch.npu.current_stream()
++ current_stream.wait_event(self._async_exponential_event)
++ u.record_stream(current_stream)
++
++ # Do not modify probs in place: the standard backend reuses it for logprobs.
++ # argmax over probs / q with q ~ Exp(1): v1's production consumption
++ # form, kept because the stock argmax kernel is healthy while argmin at
++ # this shape is a slow legacy kernel (0.38ms vs ~0.13ms at bs=128).
++ # q is produced by transforming u in place with the exact stock
++ # aten::exponential_ values (NPU op-plugin composite, fp32 path):
++ # x = min(1 - u, 1 - eps/2); q = -log(x), eps = finfo(dtype).eps
++ # The guard is a single torch.minimum against a cached 0-dim bound
++ # tensor: min(x, 1-eps/2) is bitwise-identical to stock's
++ # ge+masked_fill_ pair (x <= 1 always), and unlike clamp_max_ it stays
++ # on the template-grade aclnnMinimum kernel -- clamp_max_ routes to
++ # the legacy-family ClipByValueV2 op with two scalar->device uploads
++ # per call, which costs more than the unary-template kernels used by
++ # the rest of the chain. The cap keeps x below 1 so q can never be 0:
++ # u == 0 maps to q ~= 5.96e-8 and scores huge, exactly as stock (v2.3
++ # mapped it to +inf/+0.0 instead). For fp32 q stays within
++ # [~5.96e-8, ~16.6], finite and strictly positive, so the race has no
++ # inf/NaN edge.
++ # All transform ops run here on the main stream; the side stream stays
++ # pure DSA uniform with zero AIV kernels.
++ bound = self._async_exp_min_bound
++ if bound is None or bound.dtype != u.dtype or bound.device != u.device:
++ bound = torch.full(
++ (),
++ 1.0 - torch.finfo(u.dtype).eps / 2.0,
++ dtype=u.dtype,
++ device=u.device,
++ )
++ self._async_exp_min_bound = bound
++ u.neg_().add_(1.0)
++ torch.minimum(u, bound, out=u)
++ u.log_().neg_()
++ sampled_index = torch.div(probs, u).argmax(dim=-1)
++ return sampled_index.view(-1).to(torch.int32)
+
+ def _preprocess_logits(
+ self, logits: torch.Tensor, sampling_info: SamplingBatchInfo
+@@ -115,6 +279,9 @@ class Sampler(nn.Module):
+ to get the unique seed for each position.
+ """
+ logits = logits_output.next_token_logits
++ # In the plain probability path, keep the softmax output and apply log
++ # only to the values requested by the caller.
++ logprobs_are_probs = False
+
+ # Preprocess logits (custom processors and NaN handling)
+ logits = self._preprocess_logits(logits, sampling_info)
+@@ -180,9 +347,13 @@ class Sampler(nn.Module):
+ # Standard path: do softmax and sample from probs.
+ logits.div_(sampling_info.temperatures)
+
+- # In-place op to save memory
+- logits[:] = torch.softmax(logits, dim=-1)
+- probs = logits
++ # Do not write the softmax output back into logits: the
++ # write-back costs a full-matrix TensorMove pass (~0.2 ms on
++ # NPU at bs=128 x vocab=248320 fp32). logits (now x/T) is not
++ # read again on this path and is overwritten by the next
++ # forward anyway. Trade-off: one extra live [batch, vocab]
++ # fp32 tensor during sampling. (post_sample v1 的 A1 改动)
++ probs = torch.softmax(logits, dim=-1)
+
+ batch_next_token_ids = self._sample_from_probs(
+ probs, sampling_info, positions, simple_sampling_case
+@@ -191,8 +362,9 @@ class Sampler(nn.Module):
+ logprobs = (
+ logprobs_via_logsoftmax_kernel
+ if logprobs_via_logsoftmax_kernel is not None
+- else torch.log(probs)
++ else probs
+ )
++ logprobs_are_probs = logprobs_via_logsoftmax_kernel is None
+ del probs
+
+ # Attach logprobs to logits_output (in-place modification)
+@@ -206,6 +378,7 @@ class Sampler(nn.Module):
+ token_ids_logprobs,
+ sampling_info,
+ batch_next_token_ids,
++ logprobs_are_probs,
+ )
+
+ self._sync_token_ids_across_tp(batch_next_token_ids, sampling_info)
+@@ -225,11 +398,13 @@ class Sampler(nn.Module):
+ Handles both simple (direct multinomial) and complex (top-k/top-p/min-p) cases.
+ """
+ if simple_sampling_case:
+- batch_next_token_ids = sampling_from_probs_torch(
+- probs,
+- sampling_seed=sampling_info.sampling_seed,
+- positions=positions,
+- )
++ batch_next_token_ids = self._sample_with_async_exponential(probs)
++ if batch_next_token_ids is None:
++ batch_next_token_ids = sampling_from_probs_torch(
++ probs,
++ sampling_seed=sampling_info.sampling_seed,
++ positions=positions,
++ )
+ else:
+ backend = get_flags().sampling_backend
+ if backend == "flashinfer":
+@@ -302,7 +477,11 @@ class Sampler(nn.Module):
+ probabilities, sampling_info.sampling_seed, positions
+ ).view(-1)
+ else:
+- batch_next_token_ids = torch.multinomial(probs, num_samples=1).view(-1)
++ batch_next_token_ids = self._sample_with_async_exponential(probs)
++ if batch_next_token_ids is None:
++ batch_next_token_ids = torch.multinomial(
++ probs, num_samples=1
++ ).view(-1)
+ return batch_next_token_ids.to(torch.int32)
+ else:
+ assert (
+@@ -353,9 +532,14 @@ class Sampler(nn.Module):
+ token_ids_logprobs: List[List[int]],
+ sampling_info: SamplingBatchInfo,
+ batch_next_token_ids: torch.Tensor,
++ logprobs_are_probs: bool,
+ ):
+- # clamp to avoid -inf values
+- logprobs.clamp_(min=torch.finfo(logprobs.dtype).min)
++ # Clamp the extracted values instead of the full [batch, vocab]
++ # matrix. clamp(min=const) is elementwise and monotone
++ # non-decreasing, so it commutes with topk/gather: clamping the
++ # small outputs gives identical results (-inf -> finfo.min) while
++ # skipping a full-matrix read+write pass (~0.4 ms/step on NPU).
++ clamp_min = torch.finfo(logprobs.dtype).min
+
+ # Attach logprobs to logits_output (in-place modification)
+ if any(x > 0 for x in top_logprobs_nums):
+@@ -363,6 +547,20 @@ class Sampler(nn.Module):
+ logits_output.next_token_top_logprobs_val,
+ logits_output.next_token_top_logprobs_idx,
+ ) = get_top_logprobs(logprobs, top_logprobs_nums, no_copy_to_cpu=True)
++ # Same extraction as get_top_logprobs, but clamp the
++ # [batch, max_k] topk result in a single kernel before
++ # slicing per request.
++ max_k = max(top_logprobs_nums)
++ top_vals, top_idx = logprobs.topk(max_k, dim=-1)
++ if logprobs_are_probs:
++ top_vals.log_()
++ top_vals.clamp_(min=clamp_min)
++ logits_output.next_token_top_logprobs_val = [
++ top_vals[i][:k] for i, k in enumerate(top_logprobs_nums)
++ ]
++ logits_output.next_token_top_logprobs_idx = [
++ top_idx[i][:k] for i, k in enumerate(top_logprobs_nums)
++ ]
+
+ if any(x is not None for x in token_ids_logprobs):
+ (
+@@ -372,10 +570,19 @@ class Sampler(nn.Module):
+ logprobs, token_ids_logprobs, no_copy_to_cpu=True
+ )
+
+- logits_output.next_token_logprobs = logprobs[
+- torch.arange(len(batch_next_token_ids), device=sampling_info.device),
+- batch_next_token_ids,
+- ]
++ for row in logits_output.token_ids_logprobs_val:
++ if torch.is_tensor(row):
++ if logprobs_are_probs:
++ row.log_()
++ row.clamp_(min=clamp_min)
++
++ # Gather one value per row directly; this removes the temporary arange
++ # and the 2-D advanced-index operation from the hot path.
++ token_indices = batch_next_token_ids.to(dtype=torch.long).view(-1, 1)
++ next_token_logprobs = torch.gather(logprobs, dim=1, index=token_indices).view(-1)
++ if logprobs_are_probs:
++ next_token_logprobs.log_()
++ logits_output.next_token_logprobs = next_token_logprobs.clamp_(min=clamp_min)
+
+ def _sync_token_ids_across_tp(
+ self, batch_next_token_ids: torch.Tensor, sampling_info: SamplingBatchInfo
+diff --git a/python/sglang/srt/managers/tp_worker.py b/python/sglang/srt/managers/tp_worker.py
+index 3db49daf82..fdc5f40d81 100644
+--- a/python/sglang/srt/managers/tp_worker.py
++++ b/python/sglang/srt/managers/tp_worker.py
+@@ -486,6 +486,35 @@ class TpModelWorker(BaseTpWorker):
+ can_run_cuda_graph=can_run_cuda_graph,
+ )
+
++ def _prepare_async_exponential(
++ self,
++ forward_batch: ForwardBatch,
++ is_verify: bool,
++ ) -> None:
++ """Start simple-sampling RNG before forward so it can overlap on NPU."""
++ if (
++ is_verify
++ or self.enable_spec
++ or forward_batch.is_prefill_only
++ or not forward_batch.forward_mode.is_decode()
++ or forward_batch.sampling_info is None
++ or forward_batch.sampling_info.grammars is not None
++ ):
++ return
++
++ sampler = self.model_runner.sampler
++ prepare = getattr(sampler, "prepare_async_exponential", None)
++ if prepare is None:
++ return
++
++ prepare(
++ batch_size=forward_batch.batch_size,
++ vocab_size=self.model_runner.model_config.vocab_size,
++ sampling_info=forward_batch.sampling_info,
++ device=forward_batch.input_ids.device,
++ )
++
++
+ def forward_batch_generation(
+ self,
+ batch: Optional[ScheduleBatch],
+@@ -511,6 +540,7 @@ class TpModelWorker(BaseTpWorker):
+ return self._forward_batch_generation_dllm(forward_batch)
+
+ if self.pp_group.is_last_rank:
++ self._prepare_async_exponential(forward_batch, is_verify)
+ out = self.model_runner.forward(
+ forward_batch,
+ pp_proxy_tensors=pp_proxy_tensors,
+diff --git a/python/sglang/srt/mem_cache/mamba_radix_cache.py b/python/sglang/srt/mem_cache/mamba_radix_cache.py
+index 368781d8d5..0856caae48 100644
+--- a/python/sglang/srt/mem_cache/mamba_radix_cache.py
++++ b/python/sglang/srt/mem_cache/mamba_radix_cache.py
+@@ -1089,6 +1089,10 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ if self.disable or len(key) == 0:
+ return None
+
++ key = key.page_aligned(self.page_size)
++ if len(key) == 0:
++ return None
++
+ return key
+
+ def _match_post_processor(
+@@ -1158,6 +1162,9 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ )
+
+ def _split_node(self, key: RadixKey, child: TreeNode, split_len: int) -> TreeNode:
++ assert (
++ 0 < split_len < len(child.key)
++ ), f"split_len must create non-empty nodes, {split_len=}, {len(child.key)=}"
+ # new_node -> child
+ new_node = TreeNode()
+ new_node.children = {key[split_len:].child_key(self.page_size): child}
+@@ -1166,6 +1173,7 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ new_node.full_lock_ref = child.full_lock_ref
+ new_node.mamba_lock_ref = 0
+ new_node.key = child.key[:split_len]
++ assert len(new_node.key) > 0, f"new_node.key should not be empty"
+ new_node.value = child.value[:split_len].clone()
+
+ # child time should be later than parent's time for mamba tombstone
+@@ -1176,6 +1184,7 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
+ self.mamba_lru_list.remove_node(child)
+ child.parent = new_node
+ child.key = child.key[split_len:]
++ assert len(child.key) > 0, f"child.key should not be empty"
+ child.value = child.value[split_len:].clone()
+ new_node.parent.children[key.child_key(self.page_size)] = new_node
+ new_node.hash_value, child.hash_value = split_node_hash_value(
+diff --git a/python/sglang/srt/model_executor/forward_batch_info.py b/python/sglang/srt/model_executor/forward_batch_info.py
+index 031ab4fe87..93e14ff483 100644
+--- a/python/sglang/srt/model_executor/forward_batch_info.py
++++ b/python/sglang/srt/model_executor/forward_batch_info.py
+@@ -49,7 +49,7 @@ from sglang.srt.model_executor.forward_batch_deepseek_mha_mixin import (
+ ForwardBatchDeepSeekMHAMixin,
+ )
+ from sglang.srt.model_executor.triton_ops.position import compute_position_triton
+-from sglang.srt.runtime_context import get_parallel
++from sglang.srt.runtime_context import get_parallel, get_server_args
+ from sglang.srt.server_args import get_global_server_args
+ from sglang.srt.utils import (
+ is_cuda,
+@@ -1053,11 +1053,32 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
+ return mrope_positions
+
+ def _compute_mrope_positions(self, model_runner: ModelRunner, batch: ScheduleBatch):
++ mm_inputs = batch.multimodal_inputs
++ rl_on_policy_target = get_server_args().rl_on_policy_target
++
++ if (
++ self.spec_info is None
++ and batch.dllm_config is None
++ and (
++ rl_on_policy_target is not None
++ or all(mm_input is None for mm_input in mm_inputs)
++ )
++ ):
++ # Regular text generation does not need to rebuild mRoPE on the
++ # host. init_new has already produced the same flattened token
++ # positions on model_runner.device: clamp_position() for decode,
++ # or compute_position() for extend/mixed. Text mRoPE has identical
++ # temporal/height/width coordinates, so materialize the three rows
++ # directly and avoid the per-request host factories, cat, and H2D.
++ self.mrope_positions = (
++ self.positions.to(dtype=torch.int64).unsqueeze(0).repeat(3, 1)
++ )
++ return
+ # batch_size * [3 * seq_len]
+ batch_size = self.seq_lens_cpu.shape[0]
+ mrope_positions_list = [[]] * batch_size
+ for batch_idx in range(batch_size):
+- mm_input = batch.multimodal_inputs[batch_idx]
++ mm_input = mm_inputs[batch_idx]
+ if self.forward_mode.is_decode():
+ # 3 * N
+ if (
+diff --git a/python/sglang/srt/models/qwen2_moe.py b/python/sglang/srt/models/qwen2_moe.py
+index d5aac381dc..e710d83b05 100644
+--- a/python/sglang/srt/models/qwen2_moe.py
++++ b/python/sglang/srt/models/qwen2_moe.py
+@@ -37,6 +37,7 @@ from sglang.srt.distributed import (
+ moe_tensor_model_parallel_all_reduce,
+ tensor_model_parallel_all_reduce,
+ )
++from sglang.srt.environ import envs
+ from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
+ from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
+ from sglang.srt.eplb.expert_location_dispatch import ExpertLocationDispatchInfo
+@@ -93,7 +94,10 @@ from sglang.srt.model_executor.cuda_graph_config import (
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
+ from sglang.srt.model_executor.runner import get_is_capture_mode
+ from sglang.srt.model_loader.weight_utils import default_weight_loader
+-from sglang.srt.runtime_context import get_flags, get_parallel
++from sglang.srt.runtime_context import (
++ get_flags,
++ get_parallel,
++)
+ from sglang.srt.server_args import get_global_server_args
+ from sglang.srt.utils import (
+ add_prefix,
+@@ -106,14 +110,6 @@ from sglang.srt.utils import (
+ make_layers,
+ use_intel_amx_backend,
+ )
+-
+-if is_npu():
+- from sglang.srt.hardware_backend.npu.cmo import (
+- shared_expert_on_independent_stream,
+- wait_share_stream,
+- )
+-
+-from sglang.srt.environ import envs
+ from sglang.srt.utils.hf_transformers_utils import get_rope_config
+
+ _SGLANG_EXPERIMENTAL_LORA_OPTI = envs.SGLANG_EXPERIMENTAL_LORA_OPTI.get()
+@@ -446,9 +442,12 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
+ # router_logits: (num_tokens, n_experts)
+ router_logits, _ = self.gate(hidden_states)
+ if enable_dual_stream:
+- shared_output = shared_expert_on_independent_stream(
+- hidden_states.clone(), self._forward_shared_experts
+- )
++ current_stream = torch.npu.current_stream()
++ self.alt_stream.wait_stream(current_stream)
++ with torch.npu.stream(self.alt_stream):
++ shared_output = self._forward_shared_experts(hidden_states)
++ shared_output.record_stream(self.alt_stream)
++ shared_event = self.alt_stream.record_event()
+ else:
+ shared_output = self._forward_shared_experts(hidden_states)
+ topk_output = self.topk(
+@@ -469,8 +468,8 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
+ hidden_states=hidden_states,
+ topk_output=topk_output,
+ )
+- if enable_dual_stream:
+- wait_share_stream()
++ if hidden_states.shape[0] > 0 and enable_dual_stream:
++ torch.npu.current_stream().wait_event(shared_event)
+
+ if shared_output is not None:
+ final_hidden_states.add_(shared_output)
+diff --git a/python/sglang/srt/models/qwen3_5.py b/python/sglang/srt/models/qwen3_5.py
+index fed78cf888..7b6505fa10 100644
+--- a/python/sglang/srt/models/qwen3_5.py
++++ b/python/sglang/srt/models/qwen3_5.py
+@@ -35,6 +35,7 @@ from sglang.srt.configs.qwen3_5 import (
+
+ # Distributed
+ from sglang.srt.distributed import get_pp_group
++from sglang.srt.environ import envs
+ from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
+ from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
+
+@@ -608,7 +609,11 @@ class Qwen3_5LinearDecoderLayer(nn.Module):
+ quant_config=quant_config,
+ alt_stream=(
+ alt_stream
+- if (_is_cuda or _disable_shared_experts_fusion())
++ if (
++ _is_cuda
++ or _disable_shared_experts_fusion()
++ or envs.SGLANG_NPU_USE_MULTI_STREAM.get()
++ )
+ else None
+ ),
+ prefix=add_prefix("mlp", prefix.replace(".linear_attn", "")),
+@@ -824,7 +829,11 @@ class Qwen3_5AttentionDecoderLayer(nn.Module):
+ quant_config=quant_config,
+ alt_stream=(
+ alt_stream
+- if (_is_cuda or _disable_shared_experts_fusion())
++ if (
++ _is_cuda
++ or _disable_shared_experts_fusion()
++ or envs.SGLANG_NPU_USE_MULTI_STREAM.get()
++ )
+ else None
+ ),
+ prefix=add_prefix("mlp", prefix.replace(".self_attn", "")),
+@@ -1207,6 +1216,11 @@ class Qwen3_5ForCausalLM(nn.Module):
+ self._maybe_autodisable_shared_experts_fusion(config, quant_config)
+
+ alt_stream = torch.cuda.Stream() if _is_cuda or _hip_use_alt_stream else None
++ alt_stream = (
++ torch.cuda.Stream()
++ if _is_cuda or _hip_use_alt_stream or envs.SGLANG_NPU_USE_MULTI_STREAM.get()
++ else None
++ )
+
+ # Embedding layer
+ if self.pp_group.is_first_rank:
+diff --git a/python/sglang/srt/speculative/eagle_utils.py b/python/sglang/srt/speculative/eagle_utils.py
+index daf9fa82e2..f00f718fd2 100644
+--- a/python/sglang/srt/speculative/eagle_utils.py
++++ b/python/sglang/srt/speculative/eagle_utils.py
+@@ -617,7 +617,7 @@ def eagle_sample(
+
+ # Sample tokens
+ target_predict = None
+- if sampling_info.is_all_greedy or _is_npu or _is_hip or _is_xpu:
++ if sampling_info.is_all_greedy or _is_hip or _is_xpu:
+ target_predict = torch.argmax(next_token_logits, dim=-1)
+ target_predict = target_predict.reshape(bs, verify_input.draft_token_num)
+ predict, accept_index, num_correct_drafts = verify_tree_greedy_func(
+@@ -632,43 +632,80 @@ def eagle_sample(
+ topk=verify_input.tree_topk,
+ )
+ else:
+- from sgl_kernel import (
+- top_k_renorm_prob,
+- top_p_renorm_prob,
+- tree_speculative_sampling_target_only,
+- )
+-
+- from sglang.srt.speculative.reject_sampling import (
+- chain_speculative_sampling_triton,
+- )
+-
+ use_rejection_sampling = (
+ get_global_server_args().speculative_use_rejection_sampling
+ )
+
++ if _is_npu:
++ from sgl_kernel_npu.sample import (
++ chain_speculative_sampling_rejection,
++ top_k_top_p_renorm_probs,
++ tree_speculative_sampling_target_only,
++ )
++
++ sampling_fn = (
++ chain_speculative_sampling_rejection
++ if use_rejection_sampling
++ else tree_speculative_sampling_target_only
++ )
++ else:
++ from sgl_kernel import (
++ top_k_renorm_prob,
++ top_p_renorm_prob,
++ tree_speculative_sampling_target_only,
++ )
++
++ from sglang.srt.speculative.reject_sampling import (
++ chain_speculative_sampling_triton,
++ )
++
++ sampling_fn = (
++ chain_speculative_sampling_triton
++ if use_rejection_sampling
++ else tree_speculative_sampling_target_only
++ )
++
+ # Apply temperature and get target probs
+ expanded_temperature = torch.repeat_interleave(
+ sampling_info.temperatures, verify_input.draft_token_num, dim=0
+ ) # (bs * num_draft_tokens, 1)
+
++ sampling_logits = next_token_logits.float() if _is_npu else next_token_logits
+ target_probs = F.softmax(
+- next_token_logits / expanded_temperature, dim=-1
++ sampling_logits / expanded_temperature, dim=-1
+ ) # (bs * num_draft_tokens, vocab_size)
+ maybe_detect_nan(target_probs, "v2 verify: target_probs after softmax")
+- target_probs = top_k_renorm_prob(
+- target_probs,
+- torch.repeat_interleave(
+- sampling_info.top_ks, verify_input.draft_token_num, dim=0
+- ),
+- ) # (bs * num_draft_tokens, vocab_size)
+- maybe_detect_nan(target_probs, "v2 verify: target_probs after top_k_renorm")
+- target_probs = top_p_renorm_prob(
+- target_probs,
+- torch.repeat_interleave(
+- sampling_info.top_ps, verify_input.draft_token_num, dim=0
+- ),
+- )
+- maybe_detect_nan(target_probs, "v2 verify: target_probs after top_p_renorm")
++
++ if _is_npu:
++ target_probs = top_k_top_p_renorm_probs(
++ target_probs,
++ torch.repeat_interleave(
++ sampling_info.top_ks, verify_input.draft_token_num, dim=0
++ ),
++ torch.repeat_interleave(
++ sampling_info.top_ps, verify_input.draft_token_num, dim=0
++ ),
++ sampling_info.need_top_k_sampling,
++ sampling_info.need_top_p_sampling,
++ )
++ maybe_detect_nan(target_probs, "v2 verify: target_probs after renorm")
++ else:
++ if sampling_info.need_top_k_sampling:
++ target_probs = top_k_renorm_prob(
++ target_probs,
++ torch.repeat_interleave(
++ sampling_info.top_ks, verify_input.draft_token_num, dim=0
++ ),
++ ) # (bs * num_draft_tokens, vocab_size)
++ maybe_detect_nan(target_probs, "v2 verify: target_probs after top_k_renorm")
++ if sampling_info.need_top_p_sampling:
++ target_probs = top_p_renorm_prob(
++ target_probs,
++ torch.repeat_interleave(
++ sampling_info.top_ps, verify_input.draft_token_num, dim=0
++ ),
++ )
++ maybe_detect_nan(target_probs, "v2 verify: target_probs after top_p_renorm")
+ target_probs = target_probs.reshape(bs, verify_input.draft_token_num, -1)
+ draft_probs = (
+ verify_input.draft_probs
+@@ -687,16 +724,15 @@ def eagle_sample(
+ "does not produce one (draft_probs missing or vocab-mismatched)."
+ )
+
++ if _is_npu:
++ target_probs = target_probs.contiguous()
++ draft_probs = draft_probs.float().contiguous()
++
+ # coins for rejection sampling
+ coins = torch.rand_like(candidates, dtype=torch.float32, device=device)
+ # coins for final sampling
+ coins_for_final_sampling = torch.rand((bs,), dtype=torch.float32, device=device)
+
+- sampling_fn = (
+- chain_speculative_sampling_triton
+- if use_rejection_sampling
+- else tree_speculative_sampling_target_only
+- )
+ sampling_fn(
+ predicts=predict, # mutable
+ accept_index=accept_index, # mutable
+diff --git a/python/sglang/srt/speculative/triton_ops/cache_locs.py b/python/sglang/srt/speculative/triton_ops/cache_locs.py
+index 663c238722..522fd5ee44 100644
+--- a/python/sglang/srt/speculative/triton_ops/cache_locs.py
++++ b/python/sglang/srt/speculative/triton_ops/cache_locs.py
+@@ -363,6 +363,14 @@ def assign_extend_cache_locs_func(
+ return out_cache_loc
+
+ elif _is_npu:
++ '''
++ sgl-kernel-npu's cache_loc_assign / cache_loc_update operate under an explicit contract:
++ each row processes max_step tokens, and the tiling dimension of out_cache_loc is
++ cacheLocSize = batchSize * max_step, which is validated on the host side.
++ The host enforces 1 <= max_step <= MAX_STEP (16) and checks the size of out_cache_loc.
++ Therefore, here we allocate exactly batch_size * draft_token_num and pass draft_token_num
++ as max_step; no padding to 16 is needed.
++ '''
+ out_cache_loc = torch.empty(
+ (batch_size * draft_token_num,),
+ dtype=torch.int32,
+@@ -374,6 +382,7 @@ def assign_extend_cache_locs_func(
+ start_offset,
+ end_offset,
+ out_cache_loc,
++ draft_token_num,
+ )
+
+ return out_cache_loc
diff --git a/python/sglang/srt/utils/common.py b/python/sglang/srt/utils/common.py
-index f68721f3d0..3e85ee7c10 100644
+index 82db90da3b..aabd222d7e 100644
--- a/python/sglang/srt/utils/common.py
+++ b/python/sglang/srt/utils/common.py
-@@ -46,6 +46,7 @@ import types
- import uuid
- import warnings
- from collections import OrderedDict, defaultdict
+@@ -73,6 +73,7 @@ from typing import (
+ TypeVar,
+ Union,
+ )
+from relax.utils.device import is_npu_available
- from contextlib import contextmanager
- from dataclasses import dataclass
- from decimal import Decimal
-@@ -150,7 +151,7 @@ def is_npu() -> bool:
+ from unittest import SkipTest
+ from unittest.case import _ShouldStop
+ from urllib.parse import unquote, urlparse
+@@ -176,7 +177,8 @@ def is_npu() -> bool:
if not hasattr(torch, "npu"):
return False
- if not torch.npu.is_available():
++ # if not torch.npu.is_available():
+ if not is_npu_available:
raise RuntimeError(
"torch_npu detected, but NPU device is not available or visible."
)
+diff --git a/python/sglang/srt/utils/torch_memory_saver_adapter.py b/python/sglang/srt/utils/torch_memory_saver_adapter.py
+index ad98e59283..a5b2cdfd32 100644
+--- a/python/sglang/srt/utils/torch_memory_saver_adapter.py
++++ b/python/sglang/srt/utils/torch_memory_saver_adapter.py
+@@ -41,7 +41,7 @@ class TorchMemorySaverAdapter(ABC):
+ def region(self, tag: str, enable_cpu_backup: bool = False):
+ raise NotImplementedError
+
+- def cuda_graph(self, **kwargs):
++ def cuda_graph(self, cuda_graph=None, **kwargs):
+ raise NotImplementedError
+
+ def disable(self):
+@@ -67,7 +67,10 @@ class _TorchMemorySaverAdapterReal(TorchMemorySaverAdapter):
+ def region(self, tag: str, enable_cpu_backup: bool = False):
+ return _memory_saver.region(tag=tag, enable_cpu_backup=enable_cpu_backup)
+
+- def cuda_graph(self, **kwargs):
++ def cuda_graph(self, cuda_graph=None, **kwargs):
++ if cuda_graph is not None:
++ kwargs["cuda_graph"] = cuda_graph
++ # kwargs.pop("auto_dispatch_capture", None) # torch_memory_saver 0.0.8 does not support this arg
+ return _memory_saver.cuda_graph(**kwargs)
+
+ def disable(self):
+@@ -94,7 +97,7 @@ class _TorchMemorySaverAdapterNoop(TorchMemorySaverAdapter):
+ yield
+
+ @contextmanager
+- def cuda_graph(self, **kwargs):
++ def cuda_graph(self, cuda_graph=None, **kwargs):
+ yield
+
+ @contextmanager
+diff --git a/test/registered/unit/mem_cache/test_mamba_unittest.py b/test/registered/unit/mem_cache/test_mamba_unittest.py
+index 9ad577b42e..390a3721c5 100755
+--- a/test/registered/unit/mem_cache/test_mamba_unittest.py
++++ b/test/registered/unit/mem_cache/test_mamba_unittest.py
+@@ -419,6 +419,58 @@ class TestMamba(unittest.TestCase):
+ self.assertEqual(list(second_insert_events[0].token_ids), [5])
+ self.assertEqual(second_insert_events[0].parent_block_hash, split_parent_hash)
+
++ def test_mamba_radix_cache_limited_partial_page_match_does_not_split(self):
++ page_size = 64
++ tree = self._setup_minimal_mamba_radix_cache(page_size)
++ token_ids = array("q", range(page_size))
++
++ tree.insert(
++ InsertParams(
++ key=RadixKey(token_ids, None),
++ value=torch.arange(page_size),
++ mamba_value=torch.tensor([0]),
++ )
++ )
++
++ match = tree.match_prefix(
++ MatchPrefixParams(key=RadixKey(token_ids, None, limit=page_size - 1))
++ )
++
++ self.assertEqual(len(match.device_indices), 0)
++ self.assertEqual(self._non_root_key_lengths(tree), [page_size])
++
++ def _setup_minimal_mamba_radix_cache(self, page_size: int) -> MambaRadixCache:
++ tree = MambaRadixCache.__new__(MambaRadixCache)
++ tree.page_size = page_size
++ tree.mamba_cache_chunk_size = page_size
++ tree.disable = False
++ tree.device = torch.device("cpu")
++ tree.enable_kv_cache_events = False
++ tree.kv_event_queue = []
++ tree.full_evictable_size_ = 0
++ tree.mamba_evictable_size_ = 0
++ tree.full_protected_size_ = 0
++ tree.mamba_protected_size_ = 0
++
++ tree.root_node = TreeNode()
++ tree.root_node.key = RadixKey(array("q"), None)
++ tree.root_node.value = []
++ tree.root_node.hash_value = []
++ tree.root_node.full_lock_ref = 1
++ tree.root_node.mamba_lock_ref = 1
++ tree.full_lru_list = LRUList(mamba=False)
++ tree.mamba_lru_list = LRUList(mamba=True)
++ return tree
++
++ def _non_root_key_lengths(self, tree: MambaRadixCache) -> list[int]:
++ lengths = []
++ stack = list(tree.root_node.children.values())
++ while stack:
++ node = stack.pop()
++ lengths.append(len(node.key))
++ stack.extend(node.children.values())
++ return lengths
++
+ def _setup_tree_and_allocator(self, enable_kv_cache_events=False):
+ """Helper to create a MambaRadixCache with allocator for testing."""
+ server_args = ServerArgs(model_path="dummy", page_size=1)
From 25a42cc4d9156fd8618d0d042453554781002d8f Mon Sep 17 00:00:00 2001
From: wuqiwei
Date: Fri, 21 Aug 2026 06:29:08 +0000
Subject: [PATCH 04/16] build(docker): upgrade NPU image to CANN 9.0.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# 🔩 Chore
## Upgrade NPU Dockerfile dependencies
- Bump base image CANN 8.5.1 → 9.0.0
- Upgrade torch_npu to v26.0.1 (PyTorch 2.9.0)
- Upgrade triton-ascend 3.2.0 → 3.2.1
---
docker/Dockerfile.npu | 104 ++++++++++++++++++++++++++++--------------
1 file changed, 69 insertions(+), 35 deletions(-)
diff --git a/docker/Dockerfile.npu b/docker/Dockerfile.npu
index 89ed93f0c..98184d709 100644
--- a/docker/Dockerfile.npu
+++ b/docker/Dockerfile.npu
@@ -2,7 +2,7 @@
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
-FROM quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11
+FROM quay.io/ascend/cann:9.0.0-a3-ubuntu22.04-py3.11
ARG HTTP_PROXY
ARG HTTPS_PROXY
@@ -43,86 +43,120 @@ RUN ARCH=$(uname -m) && \
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/devlib/linux/x86_64/:$LD_LIBRARY_PATH; \
fi && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
- source /usr/local/Ascend/nnal/atb/set_env.sh
+ source /usr/local/Ascend/nnal/atb/set_env.sh && \
+ source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh
+
+
# Setting pip & git config. Global config (set once, persists across subsequent RUN layers)
ARG PIP_INDEX_URL="https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
RUN pip config set global.index-url ${PIP_INDEX_URL} && \
git config --global http.sslverify false && \
+ git config --global https.sslverify false && \
git config --global http.postBuffer 2147483648 && \
git config --global user.email "temp@example.com" && \
git config --global user.name "temp"
-WORKDIR /root
-COPY . /root/Relax
+# install torch
RUN pip install --upgrade pip packaging setuptools==80.10.2 && \
pip install torch==2.9.0 && \
pip install numpy==1.26.0
- # build torch_npu
+
+# build torch_npu
RUN pip install pyyaml && \
git clone https://gitcode.com/Ascend/pytorch.git /root/pytorch && \
cd /root/pytorch && \
- git checkout v2.9.0-7.3.0 && \
+ git checkout v26.0.1-pytorch2.9.0 && \
git cherry-pick -n f495de675bce38a2fa21edbf067b73d2a5f26733 && \
bash ci/build.sh --python=3.11 && \
- pip install dist/torch_npu-2.9.0*.whl
-RUN cd /root && rm -rf /root/pytorch && \
- pip install triton-ascend==3.2.0 && \
+ pip install dist/torch_npu*.whl
+
+# install triton/TQ
+RUN pip install triton-ascend==3.2.1 --extra-index-url=https://triton-ascend.osinfra.cn/pypi/simple && \
pip install tensordict==0.10.0 pyvers==0.1.0 --no-deps && \
pip install "transferqueue @ git+https://github.com/redai-infra/TransferQueue.git@58054a33834aadbcf76aacd6b1e32e25c030f2c9" --no-deps
- # Clone Megatron-LM, MindSpeed, MindSpeed-Bridge, Megatron-Bridge and install
+# Clone Megatron-LM, MindSpeed, MindSpeed-Bridge, Megatron-Bridge and install
RUN git clone https://gitcode.com/ascend/MindSpeed.git /root/MindSpeed && \
git clone https://github.com/NVIDIA/Megatron-LM.git /root/Megatron-LM && \
+ git clone https://gitcode.com/ascend/MindSpeed-Ops.git /root/MindSpeed-Ops && \
git clone https://gitcode.com/ascend/MindSpeed-Bridge.git /root/MindSpeed-Bridge && \
git clone https://github.com/NVIDIA-NeMo/Megatron-Bridge.git /root/Megatron-Bridge
-RUN cd /root/MindSpeed && git checkout core_r0.16.0 && pip install -r requirements.txt && pip install -e . && \
+RUN cd /root/MindSpeed && git checkout core_r0.16.0 && pip install -r requirements.txt && pip install -e . && git checkout e4772499 && \
cd /root/Megatron-LM && git checkout core_v0.16.1 && pip install -e . --no-build-isolation && \
cd /root/Megatron-Bridge && git checkout v0.3.1 && \
- cd /root/MindSpeed-Bridge && git checkout 3655c07cbcc9 && pip install -r requirements.txt && bash tools/install_auto.sh
+ cd /root/MindSpeed-Ops/ && git checkout 33ac80f7 && pip install -e . --no-build-isolation --no-deps && \
+ cd /root/MindSpeed-Bridge/ && git checkout v0.3.1 && pip install -r requirements.txt && pip install -e . --no-deps
+
-# Patch Megatron-LM, MindSpeed, MindSpeed-Bridge
-RUN cd /root/MindSpeed && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed.patch && \
- cd /root/Megatron-Bridge && \
- patch -p1 < /root/Relax/docker/npu_patch/megatron-bridge.patch && \
+COPY . /root/Relax
+# Patch Megatron-LM, MindSpeed, MindSpeed-Bridge, MindSpeed-Ops
+RUN cd /root/Megatron-Bridge && \
+ patch -p1 < /root/Relax/docker/npu_patch/megatron-bridge.patch && \
+ git add . && git commit -m "base line" && \
cd /root/Megatron-LM && \
- patch -p1 < /root/Relax/docker/npu_patch/megatron.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/megatron.patch && \
+ git add . && git commit -m "base line" && \
cd /root/MindSpeed-Bridge && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed-bridge.patch
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed-bridge.patch && \
+ git add . && git commit -m "base line" && \
+ cd /root/MindSpeed-Ops && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed-ops.patch && \
+ git add . && git commit -m "base line" && \
+ cd /root/MindSpeed && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed.patch && \
+ git add . && git commit -m "base line"
-# Copy MindSpeed-Bridge, Megatron-Bridge into Megatron-LM
-RUN cp -r /root/MindSpeed-Bridge/mindspeed_bridge /root/Megatron-LM/ && \
- cp -r /root/Megatron-Bridge/src/megatron/bridge /root/Megatron-LM/megatron/ && \
- cd /root && rm -rf /root/MindSpeed-Bridge && rm -rf /root/Megatron-Bridge
# Install sglang
RUN git clone https://github.com/sgl-project/sglang.git /root/sglang && \
- cd /root/sglang && git checkout v0.5.10 && \
+ cd /root/sglang && git checkout v0.5.15 && \
mv python/pyproject.toml python/pyproject.toml.backup && \
mv python/pyproject_npu.toml python/pyproject.toml && \
pip install -e "python[srt_npu]" --constraint <(echo "torch==2.9.0") && \
- # [NPU] Fix Qwen3.5 inference acc.
- git stash && \
- git fetch origin pull/23815/head:pr-23815 && \
- git checkout pr-23815 && \
- patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch
+ # patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch
+ git add . && git commit -m "install info" && \
+ git fetch && \
+ git cherry-pick ece02ffc9cc32e94382d4f1b553b2c755f83f722 && \
+ patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch && \
+ git add . && git commit -m "sglang-npu.patch"
+
# Install sgl-kernle-npu
RUN git clone https://github.com/sgl-project/sgl-kernel-npu /root/sgl-kernel-npu && \
- cd /root/sgl-kernel-npu && git checkout 2026.04.15.rc3 && \
+ cd /root/sgl-kernel-npu && git checkout 2026.7.2 && \
# Adapt tms for colocate train.
- git cherry-pick -n 23519771d347 --no-gpg-sign && \
- patch -p1 < /root/Relax/docker/npu_patch/torch-memory-saver.patch && \
- bash build.sh -a kernels && bash build.sh -a memory-saver && \
+ patch -p1 < /root/Relax/docker/npu_patch/sgl-kernel-npu.patch && \
+ git add . && git commit -m "sgl-kernel-npu.patch" && \
+ bash build.sh && \
pip install output/*.whl && \
- cd /root && rm -rf /root/sgl-kernel-npu
+ cd /root
+
+# Install AscendC FLA
+RUN git clone https://github.com/flashserve/flash-linear-attention-npu.git /root/flash-linear-attention-npu && \
+ cd /root/flash-linear-attention-npu && git checkout v26.1.0 && \
+ apt update && apt install gawk && \
+ # 编译命令,注意--soc=${soc_version}需要指定为当前机器的芯片类型{ascend910b/ascend910_93/ascend950}
+ bash build.sh --soc=ascend910_93 --pkg --ops=causal_conv1d,chunk_bwd_dv_local,chunk_bwd_dqkwg,chunk_gated_delta_rule_bwd_dhu,prepare_wy_repr_bwd_da,prepare_wy_repr_bwd_full,chunk_fwd_o,chunk_gated_delta_rule_fwd_h,recurrent_gated_delta_rule,recompute_wu_fwd && \
+ # 安装run包
+ ./build_out/cann-*.run && \
+ source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
+ source /usr/local/Ascend/nnal/atb/set_env.sh && \
+ source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh && \
+ # 一键编译安装脚本,先调用torchnpugen自动接入算子,再运行setup编whl包,最后安装whl包
+ cd torch_custom/fla_npu && bash build.sh
+
+
# Install Relax
-RUN cd /root/Relax && pip install -e .
+# git clone https://github.com/redai-infra/Relax.git
+RUN cd /root/Relax && \
+ pip install -e .
+
RUN pip install ray==2.55.1 && pip install protobuf==6.33.6
#Clean cache
RUN pip cache purge && \
rm -rf /tmp/*
+
From 700b291ee17476ac6916b88c69f8272f6f346f8f Mon Sep 17 00:00:00 2001
From: Tgz27 <617796318@qq.com>
Date: Mon, 24 Aug 2026 16:36:59 +0800
Subject: [PATCH 05/16] feat(npu): add Qwen3.5 MTP SFT/training scripts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
feat(npu): add Qwen3.5 MTP training scripts for 0821
# ⭐ Feature
## Add Qwen3.5 MTP training launch scripts
- run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh: Qwen3.5-35B-A3B MTP SFT on pokemon-gpt4o-captions, 8xNPU single-node, ray-submit launch
- run_qwen35-35B-A3B-16xnpu-colocate-thd.sh: Qwen3.5-35B-A3B colocate THD training on 16xNPU
- run_qwen35_9B_mtp_8xnpu_thd.sh: Qwen3.5-9B MTP THD training on 8xNPU
---
# 🐛 Bug Fix
## Fix EXP_DIR silently overridden by MODEL_DIR default
- EXP_DIR now uses its own default with MODEL_DIR following EXP_DIR, matching the 9B THD script pattern
---
# 🔩 Chore
## Align script naming and comments
- Rename scripts to the 8xnpu naming convention and drop duplicated .sh suffix
- Remove commented-out --qkv-format bshd / --micro-batch-size 1 lines
- Sync Usage comments with actual script names
---
.../run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh | 160 ++++++++++++++
.../run_qwen35-35B-A3B-16xnpu-colocate-thd.sh | 167 ++++++++++++++
.../text/run_qwen35_9B_mtp_8xnpu_thd.sh | 206 ++++++++++++++++++
3 files changed, 533 insertions(+)
create mode 100644 scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
create mode 100644 scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
create mode 100644 scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
diff --git a/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh b/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
new file mode 100644
index 000000000..e5415ad0c
--- /dev/null
+++ b/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
@@ -0,0 +1,160 @@
+#!/bin/bash
+
+# Copyright (c) 2026 Relax Authors. All Rights Reserved.
+#
+# Qwen3.5-35B-A3B MTP SFT on pokemon-gpt4o-captions, 8xNPU single-node, ray-submit launch.
+#
+# Usage:
+# bash scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
+
+set -ex
+set -o pipefail
+
+now=$(date "+%Y-%m-%d-%H:%M:%S")
+echo 当前时间:
+
+export ASCEND_COREDUMP_SIGNAL=none
+export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
+export HCCL_HOST_SOCKET_PORT_RANGE=63000-63150
+export HCCL_NPU_SOCKET_PORT_RANGE=64000-64150
+export MASTER_ADDR=$(hostname -I | awk '{print $1}')
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+# Auto-source local environment when not launched via an external entrypoint
+if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
+ source "${SCRIPT_DIR}/../../entrypoint/local-npu.sh"
+fi
+source "${MODEL_CONFIG_DIR}/qwen35-35B-A3B.sh"
+
+PROJECT_NAME="${PROJECT_NAME:=Relax/sft/pokemon}"
+EXP_NAME=qwen3.5-35B-A3B-mtp-sft-pokemon-gpu8
+EXP_DIR="${EXP_DIR:-/mnt/tidalfs-hwwh01/dataset/yuanhang/models}"
+MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
+DATA_DIR="${DATA_DIR:=/mnt/tidalfs-hwwh01/dataset/yuanhang/datasets}"
+TRAIN_FILES=(
+ "'${DATA_DIR}/sft/data/pokemon-gpt4o-captions/pokemon_gpt4o_en.parquet'"
+ "'${DATA_DIR}/sft/data/pokemon-gpt4o-captions/pokemon_gpt4o_zh.parquet'"
+)
+PROMPT_DATA="[$(IFS=,; echo "${TRAIN_FILES[*]}")]"
+SAVE_DIR="${SAVE_DIR:=${EXP_DIR}/checkpoint/checkpoints/qwen3.5-35B-A3B-mtp-pokemon-sft-0821}"
+
+CKPT_ARGS=(
+ --hf-checkpoint ${EXP_DIR}/Qwen3.5-35B-A3B
+ --ref-load ${EXP_DIR}/Qwen3.5-35B-A3B
+ --megatron-to-hf-mode bridge
+ --save ${SAVE_DIR}/sft/${EXP_NAME}
+ # --load ${SAVE_DIR}/sft/${EXP_NAME}
+ --save-interval 100
+ --num-epoch 10
+)
+
+SFT_ARGS=(
+ --loss-type sft
+ --prompt-data "${PROMPT_DATA}"
+ --input-key conversations
+ --multimodal-keys '{"image":"images"}'
+ --conversation-key-map '{"from":"role","value":"content","human":"user","gpt":"assistant"}'
+ --global-batch-size 64
+ --use-dynamic-batch-size
+ --max-tokens-per-gpu 20480
+ --balance-data
+ --per-rank-fetch
+ --sft-prefetch-num-workers 16
+ --sft-prefetch-buffer-size 512
+)
+
+MTP_ARGS=(
+ --mtp-num-layers 1
+ --enable-mtp-training
+ --mtp-loss-scaling-factor 0.2
+ # --ci-test
+)
+
+EVAL_ARGS=(
+ --eval-size 0.1
+ --eval-interval 20
+)
+
+PREDICT_ARGS=(
+ # --sft-predict-interval 10
+ # --eval-temperature 0.0
+ # --eval-max-response-len 512
+ # --rollout-num-gpus-per-engine 2
+ # --sglang-mem-fraction-static 0.6
+)
+
+PERF_ARGS=(
+ --tensor-model-parallel-size 4
+ --sequence-parallel
+ --pipeline-model-parallel-size 1
+ --context-parallel-size 1
+ --expert-model-parallel-size 8
+ --expert-tensor-parallel-size 1
+
+ --recompute-granularity full
+ --recompute-method uniform
+ --recompute-num-layers 1
+
+ --optimizer-cpu-offload
+ --overlap-cpu-optimizer-d2h-h2d
+ --use-precision-aware-optimizer
+ --no-gradient-accumulation-fusion
+
+ # --moe-flex-dispatcher-backend deepep
+ # --moe-token-dispatcher-type flex
+ --cross-entropy-loss-fusion
+ --no-rope-fusion
+ --sft-chunked-logits
+ --sft-logits-chunk-size ${SFT_LOGITS_CHUNK_SIZE:-2048}
+
+ --colocate
+)
+
+OPTIMIZER_ARGS=(
+ --optimizer adam
+ --lr 1e-5
+ --lr-decay-style cosine
+ --min-lr 1e-6
+ --weight-decay 0.1
+ --adam-beta1 0.9
+ --adam-beta2 0.98
+ --clip-grad 1.0
+)
+
+WANDB_ARGS=(
+ --use-clearml
+ --use-metrics-service
+ --use-tensorboard
+ --tb-project-name ${PROJECT_NAME}
+ --tb-experiment-name ${EXP_NAME}-${now}
+)
+
+MISC_ARGS=(
+ --attention-dropout 0.0
+ --hidden-dropout 0.0
+ --accumulate-allreduce-grads-in-fp32
+ --attention-softmax-in-fp32
+ --attention-backend flash
+ --use-health-check
+ --use-flash-attn
+)
+
+mkdir -p log
+
+ray job submit ${RAY_NO_WAIT:+--no-wait} --address="http://${MASTER_ADDR}:8265" \
+ ${WORKING_DIR:+--working-dir "${WORKING_DIR}"} \
+ --runtime-env-json="${RUNTIME_ENV_JSON}" \
+ -- python3 -m relax.entrypoints.train \
+ --resource '{"sft": [1, 0], "actor": [1, 8]}' \
+ --sft-max-in-flight-steps 4 \
+ --num-data-storage-units 8 \
+ "${MODEL_ARGS[@]}" \
+ "${CKPT_ARGS[@]}" \
+ "${SFT_ARGS[@]}" \
+ "${MTP_ARGS[@]}" \
+ "${EVAL_ARGS[@]}" \
+ "${PREDICT_ARGS[@]}" \
+ "${OPTIMIZER_ARGS[@]}" \
+ "${WANDB_ARGS[@]}" \
+ "${PERF_ARGS[@]}" \
+ "${MISC_ARGS[@]}" 2>&1 | tee log/qwen3.5-35B-A3B-mtp-sft-pokemon-npu8-${now}.log
diff --git a/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh b/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
new file mode 100644
index 000000000..c0dc81952
--- /dev/null
+++ b/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
@@ -0,0 +1,167 @@
+#!/bin/bash
+
+# Copyright (c) 2026 Relax Authors. All Rights Reserved.
+#
+# Qwen3.5-35B-A3B 16xNPU colocate training script.
+#
+# Usage:
+# bash scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
+
+set -ex
+set -o pipefail
+unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
+
+ulimit -n 65535
+
+export HCCL_SOCKET_IFNAME="${HCCL_SOCKET_IFNAME:-enp23s0f3}"
+export GLOO_SOCKET_IFNAME="${GLOO_SOCKET_IFNAME:-enp23s0f3}"
+export TP_SOCKET_IFNAME="${TP_SOCKET_IFNAME:-enp23s0f3}"
+export HCCL_CONNECT_TIMEOUT=1200
+export RAY_DEDUP_LOGS=0
+export PYTHONBUFFERED=1
+
+now=$(date "+%Y-%m-%d-%H:%M:%S")
+echo "当前时间: $now"
+export ASCEND_COREDUMP_SIGNAL=none
+export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
+export HCCL_HOST_SOCKET_PORT_RANGE=63000-63150
+export HCCL_NPU_SOCKET_PORT_RANGE=64000-64150
+export TMS_HOOK_MODE="preload"
+export HYDRA_FULL_ERROR=1
+
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+# Auto-source local environment when not launched via an external entrypoint
+if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
+ source "${SCRIPT_DIR}/../../entrypoint/local-npu.sh"
+fi
+source "${MODEL_CONFIG_DIR}/qwen35-35B-A3B.sh"
+EXP_DIR="${EXP_DIR:-/mnt/tidalfs-hwwh01/dataset/yuanhang/models}"
+PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math}"
+NUM_ROLLOUT="${NUM_ROLLOUT:=3000}"
+
+CKPT_ARGS=(
+ --hf-checkpoint ${EXP_DIR}/Qwen3.5-35B-A3B
+ --ref-load ${EXP_DIR}/Qwen3.5-35B-A3B
+ --megatron-to-hf-mode bridge
+ # --load ${EXP_DIR}/Qwen3.5-35B-A3B-save-0821
+ --save ${EXP_DIR}/Qwen3.5-35B-A3B-save-0821
+ --save-interval 100
+ --max-actor-ckpt-to-keep 0
+)
+
+PROMPT_SET=${EXP_DIR}/dapo-math-17k/dapo-math-17k.jsonl
+
+ROLLOUT_ARGS=(
+ --prompt-data ${PROMPT_SET}
+ --input-key prompt
+ --label-key label
+ --apply-chat-template
+ --rollout-shuffle
+ --rm-type dapo
+ --reward-key score
+ --num-rollout ${NUM_ROLLOUT}
+ --rollout-batch-size 16
+ --n-samples-per-prompt 8
+ --rollout-max-response-len 8192
+ --rollout-temperature 1
+ --global-batch-size 128
+ --use-fault-tolerance
+)
+
+EVAL_ARGS=(
+ --log-passrate
+ --eval-interval 20
+ --skip-eval-before-train
+ --eval-prompt-data aime ${EXP_DIR}/aime-2024/aime-2024.jsonl
+ --n-samples-per-eval-prompt 8
+ --eval-max-response-len 8192
+ #--eval-top-p 0.7
+)
+
+PERF_ARGS=(
+ --tensor-model-parallel-size 4
+ --sequence-parallel
+ --pipeline-model-parallel-size 2
+ --context-parallel-size 1
+ --expert-model-parallel-size 8
+ --expert-tensor-parallel-size 1
+ --recompute-granularity full
+ --recompute-method uniform
+ --recompute-num-layers 1
+ --use-dynamic-batch-size
+ # Packing is not supported for GDN currently
+ --max-tokens-per-gpu 10240
+ --no-rope-fusion
+ --no-gradient-accumulation-fusion
+)
+
+GRPO_ARGS=(
+ --advantage-estimator grpo
+ --use-kl-loss
+ --kl-loss-coef 0.00
+ --kl-loss-type low_var_kl
+ --entropy-coef 0.00
+ --eps-clip 0.2
+ --eps-clip-high 0.28
+ --use-tis
+)
+
+OPTIMIZER_ARGS=(
+ --optimizer adam
+ --lr 1e-6
+ --lr-decay-style constant
+ --weight-decay 0.1
+ --adam-beta1 0.9
+ --adam-beta2 0.98
+ --optimizer-cpu-offload
+ --overlap-cpu-optimizer-d2h-h2d
+ --use-precision-aware-optimizer
+ --use-distributed-optimizer
+)
+
+SGLANG_ARGS=(
+ --rollout-num-gpus-per-engine 8
+ --sglang-mem-fraction-static 0.6
+ --sglang-max-running-requests 256
+ --sglang-cuda-graph-bs 4 8 16 32 64 128 192 256
+ --sglang-device npu
+ --sglang-disable-radix-cache
+ --sglang-chunked-prefill-size 8192
+ --sglang-max-prefill-tokens 8192
+ --sglang-enable-dp-attention
+ --sglang-enable-dp-lm-head
+ --sglang-attention-backend ascend
+)
+
+MISC_ARGS=(
+ # default dropout in megatron is 0.1
+ --attention-dropout 0.0
+ --hidden-dropout 0.0
+ # should be good for model performance
+ --accumulate-allreduce-grads-in-fp32
+ --attention-softmax-in-fp32
+ # need to comment this when using model with MLA
+ --attention-backend flash
+ --use-flash-attn
+)
+
+mkdir -p log
+ ray job submit ${RAY_NO_WAIT:+--no-wait} --address="http://${MASTER_ADDR}:8265" \
+ ${WORKING_DIR:+--working-dir "${WORKING_DIR}"} \
+ --runtime-env-json="${RUNTIME_ENV_JSON}" \
+ -- python3 -m relax.entrypoints.train \
+ --resource '{"actor": [1, 16], "rollout": [1, 16]}'\
+ --max-staleness 0 \
+ --colocate \
+ --num-gpus-per-node 16 \
+ --use-health-check \
+ "${MODEL_ARGS[@]}" \
+ "${CKPT_ARGS[@]}" \
+ "${ROLLOUT_ARGS[@]}" \
+ "${OPTIMIZER_ARGS[@]}" \
+ "${GRPO_ARGS[@]}" \
+ "${PERF_ARGS[@]}" \
+ "${EVAL_ARGS[@]}" \
+ "${SGLANG_ARGS[@]}" \
+ "${MISC_ARGS[@]}" 2>&1 | tee log/qwen35-35B-MATH-npu16-colocate-${now}.log
diff --git a/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh b/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
new file mode 100644
index 000000000..5e7f7b94f
--- /dev/null
+++ b/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
@@ -0,0 +1,206 @@
+#!/bin/bash
+
+# Copyright (c) 2026 Relax Authors. All Rights Reserved.
+#
+# Qwen3.5-9B 4xNPU colocate (sync) GRPO + MTP joint-training script.
+#
+# Phase-1 RL MTP: trains the native MTP head jointly with the policy via an
+# auxiliary loss (slime-style). Rollout keeps `enable_draft_weights_cpu_backup=True`
+# so SGLang inference uses the base model only — no speculative decoding here.
+#
+# Requires the HF checkpoint to contain MTP weights (`num_nextn_predict_layers>=1`).
+#
+# Differences from the GPU MTP script:
+# - Removes --cross-entropy-loss-fusion / --cross-entropy-fusion-impl te
+# (TransformerEngine fused CE kernel is CUDA-only, not available on NPU).
+# - Uses NPU-specific SGLang args (--sglang-device npu, ascend attention backend).
+# - Uses NPU-specific perf args (--qkv-format bshd, --no-rope-fusion, etc.).
+#
+# Usage:
+# bash scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
+
+set -ex
+set -o pipefail
+unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
+now=$(date "+%Y-%m-%d-%H:%M:%S")
+echo "当前时间: $now"
+export ASCEND_COREDUMP_SIGNAL=none
+export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
+export HCCL_HOST_SOCKET_PORT_RANGE=63000-63050
+export HCCL_NPU_SOCKET_PORT_RANGE=64000-64050
+export TMS_HOOK_MODE="preload"
+export HYDRA_FULL_ERROR=1
+
+
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+# Auto-source local environment when not launched via an external entrypoint
+if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
+ source "${SCRIPT_DIR}/../../entrypoint/local-npu.sh"
+fi
+source "${MODEL_CONFIG_DIR}/qwen35-9B.sh"
+# Support setting env from outside
+EXP_DIR="${EXP_DIR:-/mnt/tidalfs-hwwh01/dataset/yuanhang/models}"
+MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
+DATA_DIR="${DATA_DIR:-${EXP_DIR}}"
+PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math-mtp}"
+NUM_ROLLOUT="${NUM_ROLLOUT:=200}"
+
+
+CKPT_ARGS=(
+ --hf-checkpoint ${MODEL_DIR}/Qwen3.5-9B/
+ --ref-load ${MODEL_DIR}/Qwen3.5-9B/
+ --megatron-to-hf-mode bridge
+ # --load ${EXP_DIR}/Qwen3.5-9B-mtp-save-0821
+ --save ${EXP_DIR}/Qwen3.5-9B-mtp-save-0821
+ --save-interval 100
+ --max-actor-ckpt-to-keep 1
+)
+
+PROMPT_SET=${DATA_DIR}/dapo-math-17k/dapo-math-17k.jsonl
+ROLLOUT_ARGS=(
+ --prompt-data ${PROMPT_SET}
+ --input-key prompt
+ --label-key label
+ --apply-chat-template
+ --rollout-shuffle
+ --rm-type dapo
+ --reward-key score
+ --num-rollout ${NUM_ROLLOUT}
+ --rollout-batch-size 32
+ --n-samples-per-prompt 8
+ --rollout-max-response-len 8192
+ --rollout-temperature 1
+ --global-batch-size 256
+ --balance-data
+ --use-fault-tolerance
+)
+
+EVAL_ARGS=(
+ --log-passrate
+ --skip-eval-before-train
+ --eval-interval 50
+ --eval-prompt-data aime ${EXP_DIR}/aime-2024/aime-2024.jsonl
+ --n-samples-per-eval-prompt 8
+ --eval-max-response-len 8192
+ --eval-top-p 1
+)
+
+PERF_ARGS=(
+ --tensor-model-parallel-size 4
+ --sequence-parallel
+ --pipeline-model-parallel-size 1
+ --context-parallel-size 1
+ --expert-model-parallel-size 1
+ --expert-tensor-parallel-size 1
+
+ --recompute-granularity full
+ --recompute-method uniform
+ --recompute-num-layers 1
+
+ --use-dynamic-batch-size
+ --qkv-format thd
+ --max-tokens-per-gpu 10240
+
+ --no-rope-fusion
+ --no-gradient-accumulation-fusion
+)
+
+GRPO_ARGS=(
+ --advantage-estimator grpo
+ --use-kl-loss
+ --kl-loss-coef 0.00
+ --kl-loss-type low_var_kl
+ --entropy-coef 0.00
+ --eps-clip 0.2
+ --eps-clip-high 0.28
+ --use-tis
+
+ --custom-tis-function-path relax.backends.megatron.loss.icepop_function
+)
+
+MTP_ARGS=(
+ --mtp-num-layers ${MTP_NUM_LAYERS:-1}
+ --enable-mtp-training
+ --mtp-loss-scaling-factor ${MTP_LOSS_SCALING_FACTOR:-0.1}
+ # NOTE: --cross-entropy-loss-fusion / --cross-entropy-fusion-impl te are
+ # intentionally omitted — the TE fused CE kernel is CUDA-only. Megatron
+ # will fall back to the non-fused cross-entropy path.
+)
+
+OPTIMIZER_ARGS=(
+ --optimizer adam
+ --lr 1e-6
+ --lr-decay-style constant
+ --weight-decay 0.1
+ --adam-beta1 0.9
+ --adam-beta2 0.98
+ --optimizer-cpu-offload
+ --overlap-cpu-optimizer-d2h-h2d
+ --use-precision-aware-optimizer
+)
+
+SGLANG_ARGS=(
+ --rollout-num-gpus-per-engine 4
+ --sglang-mem-fraction-static 0.6
+ --sglang-cuda-graph-bs 1 2 4 8 16 24 32 48 64
+ --sglang-max-running-requests 64
+ --sglang-device npu
+ --sglang-chunked-prefill-size 8192
+ --sglang-max-prefill-tokens 8192
+ --sglang-enable-dp-attention
+ --sglang-enable-dp-lm-head
+ --sglang-attention-backend ascend
+ --sglang-max-mamba-cache-size 352
+ --sglang-mamba-ssm-dtype bfloat16
+ --sglang-mamba-scheduler-strategy extra_buffer
+ --sglang-speculative-algorithm NEXTN
+ --sglang-speculative-num-steps 2
+ --sglang-speculative-eagle-topk 1
+ --sglang-speculative-num-draft-tokens 3
+
+)
+WANDB_ARGS=(
+ --use-tensorboard
+ --use-metrics-service
+ --tb-project-name ${PROJECT_NAME}
+ --tb-experiment-name qwen35-9B-mtp-GRPO-4x-sync-${now}
+ # --use-wandb
+ # --wandb-project slime-dev
+ # --wandb-group qwen3-4B-test
+ # --wandb-key ${WANDB_KEY}
+)
+
+MISC_ARGS=(
+ # default dropout in megatron is 0.1
+ --attention-dropout 0.0
+ --hidden-dropout 0.0
+ # should be good for model performance
+ --accumulate-allreduce-grads-in-fp32
+ --attention-softmax-in-fp32
+ # need to comment this when using model with MLA
+ --attention-backend flash
+ --use-flash-attn
+)
+
+mkdir -p log
+ray job submit ${RAY_NO_WAIT:+--no-wait} --address="http://127.0.0.1:8265" \
+ ${WORKING_DIR:+--working-dir "${WORKING_DIR}"} \
+ --runtime-env-json="${RUNTIME_ENV_JSON}" \
+ -- python3 -m relax.entrypoints.train \
+ --resource '{"actor": [1, 8], "rollout": [1, 8]}' \
+ --max-staleness 0 \
+ --num-data-storage-units 1 \
+ --colocate \
+ --use-health-check \
+ "${MODEL_ARGS[@]}" \
+ "${CKPT_ARGS[@]}" \
+ "${ROLLOUT_ARGS[@]}" \
+ "${OPTIMIZER_ARGS[@]}" \
+ "${GRPO_ARGS[@]}" \
+ "${MTP_ARGS[@]}" \
+ "${WANDB_ARGS[@]}" \
+ "${PERF_ARGS[@]}" \
+ "${EVAL_ARGS[@]}" \
+ "${SGLANG_ARGS[@]}" \
+ "${MISC_ARGS[@]}" 2>&1 | tee log/qwen35-9B-MATH-npu16-colocate-mtp-${now}.log
From bb7dda73430802ec2310330cc91cc9feb9d7f31e Mon Sep 17 00:00:00 2001
From: lixionglong
Date: Mon, 24 Aug 2026 17:36:15 +0800
Subject: [PATCH 06/16] Revert "fix(megatron): attach _hf_config for MTP bridge
when pp>1"
This reverts commit 57809b43772529b339dc5ba2a96f25594a711a71.
---
relax/backends/megatron/weight_update/bridge_converter.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/relax/backends/megatron/weight_update/bridge_converter.py b/relax/backends/megatron/weight_update/bridge_converter.py
index 6eb2c9a0a..608eb0ce8 100644
--- a/relax/backends/megatron/weight_update/bridge_converter.py
+++ b/relax/backends/megatron/weight_update/bridge_converter.py
@@ -82,10 +82,7 @@ def init_tasks(self) -> None:
if task.param_weight is not None:
self._bridge_task_map[task.global_param_name] = task
- model_bridge = bridge._model_bridge
- if not hasattr(model_bridge, "_hf_config"):
- model_bridge._hf_config = bridge.hf_pretrained.config
- self._bridge_mapping_registry = model_bridge.mapping_registry()
+ self._bridge_mapping_registry = bridge._model_bridge.mapping_registry()
mapping_registry = self._bridge_mapping_registry
for name, _param in named_params_and_buffers(self._args, self._model):
global_name = strip_param_name_prefix(name)
From 41606123e6df390a9feca9c52e991e74454dc86d Mon Sep 17 00:00:00 2001
From: lixionglong
Date: Mon, 24 Aug 2026 17:52:40 +0800
Subject: [PATCH 07/16] fix: update qwen35 mtp mapping in
mindspeed-bridge.patch for relax bridge_converter.py
---
docker/npu_patch/mindspeed-bridge.patch | 192 ++++++++++++++++++++++++
1 file changed, 192 insertions(+)
diff --git a/docker/npu_patch/mindspeed-bridge.patch b/docker/npu_patch/mindspeed-bridge.patch
index f658a5981..a0e6fcfd3 100644
--- a/docker/npu_patch/mindspeed-bridge.patch
+++ b/docker/npu_patch/mindspeed-bridge.patch
@@ -122,3 +122,195 @@ index 5d03f85..2488ef7 100644
)
# apply text model config to vision model config
+diff --git a/mindspeed_bridge/models/qwen_vl/qwen35_vl_bridge.py b/mindspeed_bridge/models/qwen_vl/qwen35_vl_bridge.py
+index ebbd2a3..9accb3f 100644
+--- a/mindspeed_bridge/models/qwen_vl/qwen35_vl_bridge.py
++++ b/mindspeed_bridge/models/qwen_vl/qwen35_vl_bridge.py
+@@ -458,62 +458,53 @@ class Qwen35VLMoEBridge(MegatronModelBridge):
+ # Megatron VL prefix: language_model.mtp.*
+ # HF prefix: mtp.* (top-level, not under model.language_model.)
+ # =================================================================
+- if not hasattr(self, "_hf_config"):
+- logger.warning("No HF config found, skipping MTP mappings.")
+- return MegatronMappingRegistry(*mapping_list)
+-
+- hf_config = self._hf_config
+- num_mtp_layers = getattr(hf_config.text_config, "mtp_num_hidden_layers", None)
+-
+- if num_mtp_layers is not None:
+- for mtp_layer in range(num_mtp_layers):
+- mtp_param_mappings = {
+- f"language_model.mtp.layers.{mtp_layer}.eh_proj.weight": "mtp.fc.weight",
+- f"language_model.mtp.layers.{mtp_layer}.enorm.weight": "mtp.pre_fc_norm_embedding.weight",
+- f"language_model.mtp.layers.{mtp_layer}.hnorm.weight": "mtp.pre_fc_norm_hidden.weight",
+- f"language_model.mtp.layers.{mtp_layer}.final_layernorm.weight": "mtp.norm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.router.weight": f"mtp.layers.{mtp_layer}.mlp.gate.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.pre_mlp_layernorm.weight": f"mtp.layers.{mtp_layer}.post_attention_layernorm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.linear_qkv.layer_norm_weight": f"mtp.layers.{mtp_layer}.input_layernorm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.q_layernorm.weight": f"mtp.layers.{mtp_layer}.self_attn.q_norm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.k_layernorm.weight": f"mtp.layers.{mtp_layer}.self_attn.k_norm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.linear_proj.weight": f"mtp.layers.{mtp_layer}.self_attn.o_proj.weight",
+- }
+- for megatron_param, hf_param in mtp_param_mappings.items():
+- mapping_list.append(AutoMapping(megatron_param=megatron_param, hf_param=hf_param))
+-
+- mapping_list.extend(
+- [
+- QKVMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.linear_qkv.weight",
+- q=f"mtp.layers.{mtp_layer}.self_attn.q_proj.weight",
+- k=f"mtp.layers.{mtp_layer}.self_attn.k_proj.weight",
+- v=f"mtp.layers.{mtp_layer}.self_attn.v_proj.weight",
+- ),
+- GatedMLPMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.experts.linear_fc1.weight*",
+- gate=f"mtp.layers.{mtp_layer}.mlp.experts.*.gate_proj.weight",
+- up=f"mtp.layers.{mtp_layer}.mlp.experts.*.up_proj.weight",
+- ),
+- AutoMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.experts.linear_fc2.weight*",
+- hf_param=f"mtp.layers.{mtp_layer}.mlp.experts.*.down_proj.weight",
+- ),
+- GatedMLPMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.shared_experts.linear_fc1.weight",
+- gate=f"mtp.layers.{mtp_layer}.mlp.shared_expert.gate_proj.weight",
+- up=f"mtp.layers.{mtp_layer}.mlp.shared_expert.up_proj.weight",
+- ),
+- AutoMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.shared_experts.linear_fc2.weight",
+- hf_param=f"mtp.layers.{mtp_layer}.mlp.shared_expert.down_proj.weight",
+- ),
+- ReplicatedMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.shared_experts.gate_weight",
+- hf_param=f"mtp.layers.{mtp_layer}.mlp.shared_expert_gate.weight",
+- ),
+- ]
+- )
++ mtp_param_mappings = {
++ "language_model.mtp.layers.0.eh_proj.weight": "mtp.fc.weight",
++ "language_model.mtp.layers.0.enorm.weight": "mtp.pre_fc_norm_embedding.weight",
++ "language_model.mtp.layers.0.hnorm.weight": "mtp.pre_fc_norm_hidden.weight",
++ "language_model.mtp.layers.0.final_layernorm.weight": "mtp.norm.weight",
++ "language_model.mtp.layers.0.transformer_layer.mlp.router.weight": "mtp.layers.0.mlp.gate.weight",
++ "language_model.mtp.layers.0.transformer_layer.pre_mlp_layernorm.weight": "mtp.layers.0.post_attention_layernorm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.linear_qkv.layer_norm_weight": "mtp.layers.0.input_layernorm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.q_layernorm.weight": "mtp.layers.0.self_attn.q_norm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.k_layernorm.weight": "mtp.layers.0.self_attn.k_norm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.linear_proj.weight": "mtp.layers.0.self_attn.o_proj.weight",
++ }
++ for megatron_param, hf_param in mtp_param_mappings.items():
++ mapping_list.append(AutoMapping(megatron_param=megatron_param, hf_param=hf_param))
++
++ mapping_list.extend(
++ [
++ QKVMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.self_attention.linear_qkv.weight",
++ q="mtp.layers.0.self_attn.q_proj.weight",
++ k="mtp.layers.0.self_attn.k_proj.weight",
++ v="mtp.layers.0.self_attn.v_proj.weight",
++ ),
++ GatedMLPMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.mlp.experts.linear_fc1.weight*",
++ gate="mtp.layers.0.mlp.experts.*.gate_proj.weight",
++ up="mtp.layers.0.mlp.experts.*.up_proj.weight",
++ ),
++ AutoMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.mlp.experts.linear_fc2.weight*",
++ hf_param="mtp.layers.0.mlp.experts.*.down_proj.weight",
++ ),
++ GatedMLPMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.mlp.shared_experts.linear_fc1.weight",
++ gate="mtp.layers.0.mlp.shared_expert.gate_proj.weight",
++ up="mtp.layers.0.mlp.shared_expert.up_proj.weight",
++ ),
++ AutoMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.mlp.shared_experts.linear_fc2.weight",
++ hf_param="mtp.layers.0.mlp.shared_expert.down_proj.weight",
++ ),
++ ReplicatedMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.mlp.shared_experts.gate_weight",
++ hf_param="mtp.layers.0.mlp.shared_expert_gate.weight",
++ ),
++ ]
++ )
+
+ return MegatronMappingRegistry(*mapping_list)
+
+@@ -769,46 +760,37 @@ class Qwen35VLBridge(MegatronModelBridge):
+ # Megatron VL prefix: language_model.mtp.*
+ # HF prefix: mtp.* (top-level, not under model.language_model.)
+ # =================================================================
+- if not hasattr(self, "_hf_config"):
+- logger.warning("No HF config found, skipping MTP mappings.")
+- return MegatronMappingRegistry(*mapping_list)
+-
+- hf_config = self._hf_config
+- num_mtp_layers = getattr(hf_config.text_config, "mtp_num_hidden_layers", None)
+-
+- if num_mtp_layers is not None:
+- for mtp_layer in range(num_mtp_layers):
+- mtp_param_mappings = {
+- f"language_model.mtp.layers.{mtp_layer}.eh_proj.weight": "mtp.fc.weight",
+- f"language_model.mtp.layers.{mtp_layer}.enorm.weight": "mtp.pre_fc_norm_embedding.weight",
+- f"language_model.mtp.layers.{mtp_layer}.hnorm.weight": "mtp.pre_fc_norm_hidden.weight",
+- f"language_model.mtp.layers.{mtp_layer}.final_layernorm.weight": "mtp.norm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.linear_fc1.layer_norm_weight": f"mtp.layers.{mtp_layer}.post_attention_layernorm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.linear_fc2.weight": f"mtp.layers.{mtp_layer}.mlp.down_proj.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.pre_mlp_layernorm.weight": f"mtp.layers.{mtp_layer}.post_attention_layernorm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.linear_qkv.layer_norm_weight": f"mtp.layers.{mtp_layer}.input_layernorm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.q_layernorm.weight": f"mtp.layers.{mtp_layer}.self_attn.q_norm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.k_layernorm.weight": f"mtp.layers.{mtp_layer}.self_attn.k_norm.weight",
+- f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.linear_proj.weight": f"mtp.layers.{mtp_layer}.self_attn.o_proj.weight",
+- }
+- for megatron_param, hf_param in mtp_param_mappings.items():
+- mapping_list.append(AutoMapping(megatron_param=megatron_param, hf_param=hf_param))
+-
+- mapping_list.extend(
+- [
+- QKVMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.self_attention.linear_qkv.weight",
+- q=f"mtp.layers.{mtp_layer}.self_attn.q_proj.weight",
+- k=f"mtp.layers.{mtp_layer}.self_attn.k_proj.weight",
+- v=f"mtp.layers.{mtp_layer}.self_attn.v_proj.weight",
+- ),
+- GatedMLPMapping(
+- megatron_param=f"language_model.mtp.layers.{mtp_layer}.transformer_layer.mlp.linear_fc1.weight",
+- gate=f"mtp.layers.{mtp_layer}.mlp.gate_proj.weight",
+- up=f"mtp.layers.{mtp_layer}.mlp.up_proj.weight",
+- ),
+- ]
+- )
++ mtp_param_mappings = {
++ "language_model.mtp.layers.0.eh_proj.weight": "mtp.fc.weight",
++ "language_model.mtp.layers.0.enorm.weight": "mtp.pre_fc_norm_embedding.weight",
++ "language_model.mtp.layers.0.hnorm.weight": "mtp.pre_fc_norm_hidden.weight",
++ "language_model.mtp.layers.0.final_layernorm.weight": "mtp.norm.weight",
++ "language_model.mtp.layers.0.transformer_layer.mlp.linear_fc1.layer_norm_weight": "mtp.layers.0.post_attention_layernorm.weight",
++ "language_model.mtp.layers.0.transformer_layer.mlp.linear_fc2.weight": "mtp.layers.0.mlp.down_proj.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.linear_qkv.layer_norm_weight": "mtp.layers.0.input_layernorm.weight",
++ "language_model.mtp.layers.0.transformer_layer.pre_mlp_layernorm.weight": "mtp.layers.0.post_attention_layernorm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.q_layernorm.weight": "mtp.layers.0.self_attn.q_norm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.k_layernorm.weight": "mtp.layers.0.self_attn.k_norm.weight",
++ "language_model.mtp.layers.0.transformer_layer.self_attention.linear_proj.weight": "mtp.layers.0.self_attn.o_proj.weight",
++ }
++ for megatron_param, hf_param in mtp_param_mappings.items():
++ mapping_list.append(AutoMapping(megatron_param=megatron_param, hf_param=hf_param))
++
++ mapping_list.extend(
++ [
++ QKVMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.self_attention.linear_qkv.weight",
++ q="mtp.layers.0.self_attn.q_proj.weight",
++ k="mtp.layers.0.self_attn.k_proj.weight",
++ v="mtp.layers.0.self_attn.v_proj.weight",
++ ),
++ GatedMLPMapping(
++ megatron_param="language_model.mtp.layers.0.transformer_layer.mlp.linear_fc1.weight",
++ gate="mtp.layers.0.mlp.gate_proj.weight",
++ up="mtp.layers.0.mlp.up_proj.weight",
++ ),
++ ]
++ )
+
+ return MegatronMappingRegistry(*mapping_list)
+
From 905857ade8e20bf5a4512cca476545ac5639d3cb Mon Sep 17 00:00:00 2001
From: dabuliu123 <270334047@qq.com>
Date: Wed, 26 Aug 2026 16:35:08 +0800
Subject: [PATCH 08/16] fix(npu): rename Qwen3.5-9B CP script to 16xnpu
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
feat(npu): add Qwen3.5-9B CP training script
# ⭐ Feature
## Add Qwen3.5-9B CP colocate training script
- Add run-qwen35-9B-8xnpu-cp.sh for DAPO math training with TP4/CP4 on 16 NPUs
- Set MINDSPEED_BRIDGE_GDN_USE_TORCH_CONV=1 to use torch conv fallback for GDN
## Support GDN causal conv on NPU via fla_npu
- Replace causal_conv1d with fla_npu in mindspeed-bridge.patch GDN layer
- Add MINDSPEED_BRIDGE_GDN_USE_TORCH_CONV env switch to fall back to torch conv
---
# 🐛 Bug Fix
## Fix repatch ordering before Megatron init
- Move repatch(args) before init(args) in MegatronTrainRayActor so bridge patches apply during initialization
fix(npu): rename Qwen3.5-9B CP script to 16xnpu
# 🐛 Bug Fix
## Fix Qwen3.5-9B CP script naming for 16 NPUs
- Rename run-qwen35-9B-8xnpu-cp.sh to run-qwen35-9B-16xnpu-cp.sh to match the actual 16-NPU setup
- Update header comment to 16xNPU and log filename to qwen35-9B-GRPO-npu16
docs(npu): add feature support table
# 📝 Documentation
## Add feature support table to NPU training guide
- Add 特性支持 table covering Qwen3.5-9B CP, Qwen3.5-9B MTP, and Qwen3.5-35B-A3B SFT MTP with reference scripts
- Update 下一步 checklist from feature support to performance optimization
- Fix MTP row minimum card count to 4卡 to match the script's 8 NPUs
---
docker/npu-training.md | 10 +-
docker/npu_patch/mindspeed-bridge.patch | 26 ++-
relax/backends/megatron/actor.py | 2 +-
.../training/text/run-qwen35-9B-16xnpu-cp.sh | 194 ++++++++++++++++++
4 files changed, 220 insertions(+), 12 deletions(-)
create mode 100755 scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
diff --git a/docker/npu-training.md b/docker/npu-training.md
index d35892be1..f8909f6e4 100644
--- a/docker/npu-training.md
+++ b/docker/npu-training.md
@@ -12,6 +12,14 @@
| Qwen3.5-9B | DAPO | √ | √ | 910C 2卡 | `scripts/training/text/run-qwen35-9B-4xnpu-colocate.sh` |
| Qwen3.5-35B-A3B | DAPO | √ | √ | 910C 8卡 | `scripts/training/text/run-qwen35-35B-A3B-16xnpu-colocate.sh` |
+## 特性支持
+
+| 模型 | 训练场景 | 多模态 | MTP | CP | 训练所需最小卡数 | 参考脚本 |
+| --------------- | -------- | ------ | --- | --- | ---------------- | -------------------------------------------------------------- |
+| Qwen3.5-9B | DAPO | - | - | √ | 910C 8卡 | `scripts/training/text/run-qwen35-9B-16xnpu-cp.sh` |
+| Qwen3.5-9B | DAPO | - | √ | - | 910C 4卡 | `scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh` |
+| Qwen3.5-35B-A3B | SFT | √ | √ | - | 910C 4卡 | `scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh` |
+
## 环境准备
### 前置准备
@@ -120,4 +128,4 @@ bash scripts/training/text/run-qwen3-4B-8xgpu-async-npu.sh
## 下一步
-- [ ] 特性支持:多模态、CP长序列、MTP 等
+- [ ] 性能优化:Qwen3.5-35B-A3B 等
diff --git a/docker/npu_patch/mindspeed-bridge.patch b/docker/npu_patch/mindspeed-bridge.patch
index a0e6fcfd3..6d574e221 100644
--- a/docker/npu_patch/mindspeed-bridge.patch
+++ b/docker/npu_patch/mindspeed-bridge.patch
@@ -19,7 +19,7 @@ index 99ccba5..8fa07a1 100644
# Set the cleaned value (or None if it was removed)
setattr(cleaned_obj, attr_name, cleaned_value)
diff --git a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
-index 19a3b9d..3dd5e25 100644
+index 19a3b9d..00a3a80 100644
--- a/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
+++ b/mindspeed_bridge/models/qwen_vl/modelling_qwen3_vl/gated_delta_net.py
@@ -1,6 +1,6 @@
@@ -30,22 +30,28 @@ index 19a3b9d..3dd5e25 100644
import torch
from torch import nn
import torch.nn.functional as F
-@@ -37,12 +37,11 @@ from megatron.core.ssm.gated_delta_net import (
+@@ -36,13 +36,15 @@ from megatron.core.ssm.gated_delta_net import (
+ _split_tensor_factory,
)
- try:
+-try:
- from causal_conv1d import causal_conv1d
-+ import fla_npu
-+ from fla.modules.convolution import causal_conv1d
- except ImportError:
+-except ImportError:
++if os.environ.get("MINDSPEED_BRIDGE_GDN_USE_TORCH_CONV", "0") == "1":
causal_conv1d = None
- causal_conv1d_update = None
++else:
++ try:
++ import fla_npu
++ from fla.modules.convolution import causal_conv1d
++ except ImportError:
++ causal_conv1d = None
-from mindspeed_ops.api.triton.l2norm import l2norm
from mindspeed_bridge.models.qwen_vl.modelling_qwen3_vl.chunk_gated_delta_rule import (
torch_chunk_gated_delta_rule,
)
-@@ -59,6 +58,12 @@ except ImportError:
+@@ -59,6 +61,12 @@ except ImportError:
from mindspeed_bridge.models.qwen_vl.modelling_qwen3_vl.flash_gated_delta_rule import flash_gated_delta_rule
@@ -58,7 +64,7 @@ index 19a3b9d..3dd5e25 100644
class GatedDeltaNet(MegatronModule):
"""Gated Delta Net (GDN) layer class
-@@ -191,9 +196,10 @@ class GatedDeltaNet(MegatronModule):
+@@ -191,9 +199,10 @@ class GatedDeltaNet(MegatronModule):
)
setattr(self.A_log, "tensor_model_parallel", True)
@@ -71,7 +77,7 @@ index 19a3b9d..3dd5e25 100644
self.gated_delta_rule = flash_gated_delta_rule
else:
self.gated_delta_rule = torch_chunk_gated_delta_rule
-@@ -474,7 +480,7 @@ class GatedDeltaNet(MegatronModule):
+@@ -474,7 +483,7 @@ class GatedDeltaNet(MegatronModule):
beta=beta,
initial_state=None,
output_final_state=False,
@@ -80,7 +86,7 @@ index 19a3b9d..3dd5e25 100644
cu_seqlens=cu_seqlens_q,
)
nvtx_range_pop(suffix="gated_delta_rule")
-@@ -555,7 +561,7 @@ class GatedDeltaNet(MegatronModule):
+@@ -555,7 +564,7 @@ class GatedDeltaNet(MegatronModule):
# Apply L2 norm to query and key
if self.use_qk_l2norm:
diff --git a/relax/backends/megatron/actor.py b/relax/backends/megatron/actor.py
index 75143efa6..72d230001 100644
--- a/relax/backends/megatron/actor.py
+++ b/relax/backends/megatron/actor.py
@@ -184,9 +184,9 @@ def _init(
self.genrm_manager = None
- init(args)
if repatch is not None:
repatch(args)
+ init(args)
tq.init(args.tq_config)
self.data_system_client = tq.get_client()
if is_megatron_main_rank():
diff --git a/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh b/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
new file mode 100755
index 000000000..4315ee447
--- /dev/null
+++ b/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
@@ -0,0 +1,194 @@
+#!/bin/bash
+
+# Copyright (c) 2026 Relax Authors. All Rights Reserved.
+#
+# Qwen3.5-9B 16xNPU colocate (sync) training script for DAPO math dataset.
+#
+
+set -ex
+set -o pipefail
+unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
+now=$(date "+%Y-%m-%d-%H:%M:%S")
+echo "当前时间: $now"
+export ASCEND_COREDUMP_SIGNAL=none
+export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
+export HCCL_HOST_SOCKET_PORT_RANGE=63000-63050
+export HCCL_NPU_SOCKET_PORT_RANGE=64000-64050
+export TMS_HOOK_MODE="preload"
+export HYDRA_FULL_ERROR=1
+export LD_LIBRARY_PATH=/usr/local/Ascend/cann-9.0.0/opp/vendors/custom_transformer/op_api/lib/:${LD_LIBRARY_PATH}
+export MINDSPEED_BRIDGE_GDN_USE_TORCH_CONV=1
+
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+# Auto-source local environment when not launched via an external entrypoint
+if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
+ source "${SCRIPT_DIR}/../../entrypoint/local-npu.sh"
+fi
+
+# Forward WANDB_API_KEY into Ray workers' runtime_env (local.sh doesn't
+# propagate arbitrary env vars). No-op when the key isn't exported.
+if [ -n "${WANDB_API_KEY:-}" ]; then
+ export RUNTIME_ENV_JSON=$(echo "$RUNTIME_ENV_JSON" | jq --arg k "$WANDB_API_KEY" '.env_vars.WANDB_API_KEY = $k')
+fi
+source "${MODEL_CONFIG_DIR}/qwen35-9B.sh"
+
+PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math}"
+EXP_DIR="${EXP_DIR:-/mnt/}"
+MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
+DATA_DIR="${DATA_DIR:-${EXP_DIR}}"
+
+NUM_ROLLOUT="${NUM_ROLLOUT:=1000}"
+
+CKPT_ARGS=(
+ --hf-checkpoint ${MODEL_DIR}/models/Qwen3.5-9B
+ --ref-load ${MODEL_DIR}/models/Qwen3.5-9B
+ --megatron-to-hf-mode bridge
+# --warm-hf-checkpoint-page-cache
+
+# --load ${EXP_DIR}/Qwen3-9B_mcore_8xgpu/
+ --save ${EXP_DIR}/Qwen3-9B_mcore_8xgpu/
+ --save-interval 50
+ --max-actor-ckpt-to-keep 0
+)
+
+PROMPT_SET=${DATA_DIR}/dapo-math-17k/dapo-math-17k.jsonl
+
+ROLLOUT_ARGS=(
+ --prompt-data ${PROMPT_SET}
+ --input-key prompt
+ --label-key label
+ --apply-chat-template
+ --rollout-shuffle
+ --rm-type dapo
+ --reward-key score
+ --num-rollout ${NUM_ROLLOUT}
+ --rollout-batch-size 32
+ --n-samples-per-prompt 8
+ --rollout-max-response-len 8192
+ --rollout-temperature 1
+ --global-batch-size 256
+ --balance-data
+ --use-fault-tolerance
+)
+
+EVAL_ARGS=(
+ --log-passrate
+ --skip-eval-before-train
+ --eval-interval 20
+ --eval-prompt-data aime ${EXP_DIR}/aime-2024/aime-2024.jsonl
+ --n-samples-per-eval-prompt 8
+ --eval-max-response-len 8192
+ --eval-top-p 0.7
+)
+
+PERF_ARGS=(
+ --tensor-model-parallel-size 4
+ --sequence-parallel
+ --pipeline-model-parallel-size 1
+ --context-parallel-size 4
+ --calculate-per-token-loss
+ --expert-model-parallel-size 1
+ --expert-tensor-parallel-size 1
+
+ --recompute-granularity selective
+ # --recompute-method uniform
+ # --recompute-num-layers 1
+
+ --use-distributed-optimizer
+ --use-dynamic-batch-size
+
+ --data-pad-size-multiplier 4096
+ --max-tokens-per-gpu 8192
+ --log-probs-max-tokens-per-gpu 8192
+ --no-rope-fusion
+ --no-gradient-accumulation-fusion
+)
+
+GRPO_ARGS=(
+ --advantage-estimator grpo
+ --use-kl-loss
+ --kl-loss-coef 0.00
+ --kl-loss-type low_var_kl
+ --entropy-coef 0.00
+ --eps-clip 0.2
+ --eps-clip-high 0.28
+ --use-tis
+ # icepop: drop tokens with ratio outside [tis-clip-low, tis-clip] instead of clamping (vanilla TIS).
+ --custom-tis-function-path relax.backends.megatron.loss.icepop_function
+)
+
+OPTIMIZER_ARGS=(
+ --optimizer adam
+ --lr 1e-6
+ --lr-decay-style constant
+ --weight-decay 0.1
+ --adam-beta1 0.9
+ --adam-beta2 0.98
+
+ --optimizer-cpu-offload
+ --overlap-cpu-optimizer-d2h-h2d
+ --use-precision-aware-optimizer
+)
+
+WANDB_ARGS=(
+ --use-tensorboard
+ --use-metrics-service
+ --tb-project-name ${PROJECT_NAME}
+ --tb-experiment-name qwen35-9B-8x-${now}
+)
+SGLANG_ARGS=(
+ --rollout-num-gpus-per-engine 4
+ --sglang-mem-fraction-static 0.8
+ --sglang-cuda-graph-bs 4 8 16 32 64 128 192 256
+ --sglang-device npu
+ --sglang-disable-radix-cache
+ --sglang-chunked-prefill-size 8192
+ --sglang-max-prefill-tokens 8192
+ --sglang-enable-dp-attention
+ --sglang-enable-dp-lm-head
+ --sglang-attention-backend ascend
+)
+
+# wandb: only enabled when WANDB_API_KEY is exported (see runtime_env injection above).
+# wandb project names cannot contain / \ # ? % : — translate slashes to dashes.
+if [ -n "${WANDB_API_KEY:-}" ]; then
+ WANDB_ARGS+=(
+ --use-wandb
+ --wandb-project ${PROJECT_NAME//\//-}
+ --wandb-group qwen35-9B-8x-${now}
+ )
+fi
+
+MISC_ARGS=(
+ # default dropout in megatron is 0.1
+ --attention-dropout 0.0
+ --hidden-dropout 0.0
+ # should be good for model performance
+ --accumulate-allreduce-grads-in-fp32
+ --attention-softmax-in-fp32
+ # need to comment this when using model with MLA
+ --attention-backend flash
+ --use-flash-attn
+)
+
+mkdir -p log
+ray job submit ${RAY_NO_WAIT:+--no-wait} --address="http://127.0.0.1:8265" \
+ ${WORKING_DIR:+--working-dir "${WORKING_DIR}"} \
+ --runtime-env-json="${RUNTIME_ENV_JSON}" \
+ -- python3 -m relax.entrypoints.train \
+ --resource '{"actor": [1, 16], "rollout": [1, 16]}' \
+ --num-gpus-per-node 16 \
+ --max-staleness 0 \
+ --num-data-storage-units 1 \
+ --colocate \
+ --use-health-check \
+ "${MODEL_ARGS[@]}" \
+ "${CKPT_ARGS[@]}" \
+ "${ROLLOUT_ARGS[@]}" \
+ "${OPTIMIZER_ARGS[@]}" \
+ "${GRPO_ARGS[@]}" \
+ "${WANDB_ARGS[@]}" \
+ "${PERF_ARGS[@]}" \
+ "${EVAL_ARGS[@]}" \
+ "${SGLANG_ARGS[@]}" \
+ "${MISC_ARGS[@]}" 2>&1 | tee log/qwen35-9B-GRPO-npu16-${now}.log
From f49de709f3d026ec02198bf2b91684b60e05ce80 Mon Sep 17 00:00:00 2001
From: dabuliu123 <270334047@qq.com>
Date: Wed, 26 Aug 2026 11:26:59 +0000
Subject: [PATCH 09/16] fix(gitleaks): drop /mnt/ defaults in NPU scripts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# 🔒 Security
## Remove hardcoded internal paths from Qwen3.5 NPU scripts
- Replace /mnt/tidalfs-hwwh01 EXP_DIR defaults with the repo-standard `${SCRIPT_DIR}/../../../../exps` in run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh, run_qwen35-35B-A3B-16xnpu-colocate-thd.sh and run_qwen35_9B_mtp_8xnpu_thd.sh
- Replace bare /mnt/ placeholder in run-qwen35-9B-16xnpu-cp.sh with the same repo-standard default
- Switch the SFT script DATA_DIR default to the `${DATA_DIR:-${EXP_DIR}}` convention used by all other training scripts
---
# 🎨 Style
## End-of-file fixer
- Remove trailing blank line from docker/Dockerfile.npu
Co-Authored-By: Claude
---
docker/Dockerfile.npu | 1 -
scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh | 4 ++--
scripts/training/text/run-qwen35-9B-16xnpu-cp.sh | 2 +-
.../training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh | 2 +-
scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh | 2 +-
5 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/docker/Dockerfile.npu b/docker/Dockerfile.npu
index 85256d74d..ac1e37d42 100644
--- a/docker/Dockerfile.npu
+++ b/docker/Dockerfile.npu
@@ -160,4 +160,3 @@ RUN pip install ray==2.55.1 && pip install protobuf==6.33.6
#Clean cache
RUN pip cache purge && \
rm -rf /tmp/*
-
diff --git a/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh b/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
index e5415ad0c..9cc63821b 100644
--- a/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
+++ b/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
@@ -28,9 +28,9 @@ source "${MODEL_CONFIG_DIR}/qwen35-35B-A3B.sh"
PROJECT_NAME="${PROJECT_NAME:=Relax/sft/pokemon}"
EXP_NAME=qwen3.5-35B-A3B-mtp-sft-pokemon-gpu8
-EXP_DIR="${EXP_DIR:-/mnt/tidalfs-hwwh01/dataset/yuanhang/models}"
+EXP_DIR="${EXP_DIR:-${SCRIPT_DIR}/../../../../exps}"
MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
-DATA_DIR="${DATA_DIR:=/mnt/tidalfs-hwwh01/dataset/yuanhang/datasets}"
+DATA_DIR="${DATA_DIR:-${EXP_DIR}}"
TRAIN_FILES=(
"'${DATA_DIR}/sft/data/pokemon-gpt4o-captions/pokemon_gpt4o_en.parquet'"
"'${DATA_DIR}/sft/data/pokemon-gpt4o-captions/pokemon_gpt4o_zh.parquet'"
diff --git a/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh b/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
index 4315ee447..f66519f45 100755
--- a/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
+++ b/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
@@ -33,7 +33,7 @@ fi
source "${MODEL_CONFIG_DIR}/qwen35-9B.sh"
PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math}"
-EXP_DIR="${EXP_DIR:-/mnt/}"
+EXP_DIR="${EXP_DIR:-${SCRIPT_DIR}/../../../../exps}"
MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
DATA_DIR="${DATA_DIR:-${EXP_DIR}}"
diff --git a/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh b/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
index c0dc81952..4e899014b 100644
--- a/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
+++ b/scripts/training/text/run_qwen35-35B-A3B-16xnpu-colocate-thd.sh
@@ -36,7 +36,7 @@ if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
source "${SCRIPT_DIR}/../../entrypoint/local-npu.sh"
fi
source "${MODEL_CONFIG_DIR}/qwen35-35B-A3B.sh"
-EXP_DIR="${EXP_DIR:-/mnt/tidalfs-hwwh01/dataset/yuanhang/models}"
+EXP_DIR="${EXP_DIR:-${SCRIPT_DIR}/../../../../exps}"
PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math}"
NUM_ROLLOUT="${NUM_ROLLOUT:=3000}"
diff --git a/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh b/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
index 5e7f7b94f..4f031f57b 100644
--- a/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
+++ b/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
@@ -40,7 +40,7 @@ if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
fi
source "${MODEL_CONFIG_DIR}/qwen35-9B.sh"
# Support setting env from outside
-EXP_DIR="${EXP_DIR:-/mnt/tidalfs-hwwh01/dataset/yuanhang/models}"
+EXP_DIR="${EXP_DIR:-${SCRIPT_DIR}/../../../../exps}"
MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
DATA_DIR="${DATA_DIR:-${EXP_DIR}}"
PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math-mtp}"
From abf8a43727e404e8493ec17838f1e2983e44a85e Mon Sep 17 00:00:00 2001
From: lixionglong
Date: Tue, 1 Sep 2026 15:43:47 +0800
Subject: [PATCH 10/16] fix: enforce MTP_NUM_LAYERS=1 for Qwen3.5 NPU scripts
add validation in the *sft.sh script to ensure that MTP_NUM_LAYERS can only be 1
---
docker/npu-training.md | 4 ++++
.../training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh | 9 +++++++--
scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh | 5 +++++
3 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/docker/npu-training.md b/docker/npu-training.md
index f8909f6e4..bc4710071 100644
--- a/docker/npu-training.md
+++ b/docker/npu-training.md
@@ -126,6 +126,10 @@ bash scripts/training/text/run-qwen3-4B-8xgpu-async-npu.sh
> MISC_ARGS,显示启用FlashAttention实现 `--use-flash-attn`
+### MTP 特性说明
+
+开启 MTP 训练时,`MTP_NUM_LAYERS`(MTP 头层数,脚本透传给训练参数 `--mtp-num-layers`)只能为 `1`:由于 Qwen3.5 原始 checkpoint 中仅包含 1 层 MTP 权重(`mtp_num_hidden_layers=1`),MTP 相关启动脚本(`scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh`、`scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh`)已加入校验,当该参数被设置为非 `1` 的值时脚本会报错退出
+
## 下一步
- [ ] 性能优化:Qwen3.5-35B-A3B 等
diff --git a/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh b/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
index 9cc63821b..308a8607c 100644
--- a/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
+++ b/scripts/training/sft/run_qwen35-35B-pokemon-sft-mtp-8xnpu.sh
@@ -63,10 +63,15 @@ SFT_ARGS=(
--sft-prefetch-buffer-size 512
)
+if [[ "${MTP_NUM_LAYERS:-1}" != "1" ]]; then
+ echo "ERROR: MTP_NUM_LAYERS must be 1 for Qwen3.5 (checkpoint has mtp_num_hidden_layers=1)." >&2
+ exit 1
+fi
+
MTP_ARGS=(
- --mtp-num-layers 1
+ --mtp-num-layers ${MTP_NUM_LAYERS:-1}
--enable-mtp-training
- --mtp-loss-scaling-factor 0.2
+ --mtp-loss-scaling-factor ${MTP_LOSS_SCALING_FACTOR:-0.2}
# --ci-test
)
diff --git a/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh b/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
index 4f031f57b..dbf4142de 100644
--- a/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
+++ b/scripts/training/text/run_qwen35_9B_mtp_8xnpu_thd.sh
@@ -119,6 +119,11 @@ GRPO_ARGS=(
--custom-tis-function-path relax.backends.megatron.loss.icepop_function
)
+if [[ "${MTP_NUM_LAYERS:-1}" != "1" ]]; then
+ echo "ERROR: MTP_NUM_LAYERS must be 1 for Qwen3.5 (checkpoint has mtp_num_hidden_layers=1)." >&2
+ exit 1
+fi
+
MTP_ARGS=(
--mtp-num-layers ${MTP_NUM_LAYERS:-1}
--enable-mtp-training
From ebfc3648d773b30f982646d58263581438745555 Mon Sep 17 00:00:00 2001
From: dabuliu123 <270334047@qq.com>
Date: Tue, 1 Sep 2026 17:53:58 +0800
Subject: [PATCH 11/16] refactor(npu): drop wandb wiring from CP script
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
fix(docker): install gawk non-interactively
# 🐛 Bug Fix
## Make gawk install non-interactive in NPU Dockerfile
- Replace `apt install gawk` with `apt-get install -y --no-install-recommends gawk`
- `apt install` prompts for confirmation and aborts in unattended Docker builds
refactor(npu): drop wandb wiring from CP script
# ♻️ Refactor
## Drop script-local wandb wiring from Qwen3.5-9B CP script
- Remove WANDB_API_KEY injection into Ray runtime_env via jq
- Remove WANDB_API_KEY-gated --use-wandb/--wandb-project/--wandb-group args
- Wandb is configured centrally via --use-wandb/--wandb-key CLI args
---
docker/Dockerfile.npu | 2 +-
scripts/training/text/run-qwen35-9B-16xnpu-cp.sh | 15 ---------------
2 files changed, 1 insertion(+), 16 deletions(-)
diff --git a/docker/Dockerfile.npu b/docker/Dockerfile.npu
index ac1e37d42..ca4619d3e 100644
--- a/docker/Dockerfile.npu
+++ b/docker/Dockerfile.npu
@@ -135,7 +135,7 @@ RUN git clone https://github.com/sgl-project/sgl-kernel-npu /root/sgl-kernel-npu
# Install AscendC FLA
RUN git clone https://github.com/flashserve/flash-linear-attention-npu.git /root/flash-linear-attention-npu && \
cd /root/flash-linear-attention-npu && git checkout v26.1.0 && \
- apt update && apt install gawk && \
+ apt update && apt-get install -y --no-install-recommends gawk && \
# 编译命令,注意--soc=${soc_version}需要指定为当前机器的芯片类型{ascend910b/ascend910_93/ascend950}
bash build.sh --soc=ascend910_93 --pkg --ops=causal_conv1d,chunk_bwd_dv_local,chunk_bwd_dqkwg,chunk_gated_delta_rule_bwd_dhu,prepare_wy_repr_bwd_da,prepare_wy_repr_bwd_full,chunk_fwd_o,chunk_gated_delta_rule_fwd_h,recurrent_gated_delta_rule,recompute_wu_fwd && \
# 安装run包
diff --git a/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh b/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
index f66519f45..6db7e65e9 100755
--- a/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
+++ b/scripts/training/text/run-qwen35-9B-16xnpu-cp.sh
@@ -25,11 +25,6 @@ if [ -z "${RELAX_ENTRYPOINT_MODE:-}" ]; then
source "${SCRIPT_DIR}/../../entrypoint/local-npu.sh"
fi
-# Forward WANDB_API_KEY into Ray workers' runtime_env (local.sh doesn't
-# propagate arbitrary env vars). No-op when the key isn't exported.
-if [ -n "${WANDB_API_KEY:-}" ]; then
- export RUNTIME_ENV_JSON=$(echo "$RUNTIME_ENV_JSON" | jq --arg k "$WANDB_API_KEY" '.env_vars.WANDB_API_KEY = $k')
-fi
source "${MODEL_CONFIG_DIR}/qwen35-9B.sh"
PROJECT_NAME="${PROJECT_NAME:=Relax/dev/dapo-math}"
@@ -149,16 +144,6 @@ SGLANG_ARGS=(
--sglang-attention-backend ascend
)
-# wandb: only enabled when WANDB_API_KEY is exported (see runtime_env injection above).
-# wandb project names cannot contain / \ # ? % : — translate slashes to dashes.
-if [ -n "${WANDB_API_KEY:-}" ]; then
- WANDB_ARGS+=(
- --use-wandb
- --wandb-project ${PROJECT_NAME//\//-}
- --wandb-group qwen35-9B-8x-${now}
- )
-fi
-
MISC_ARGS=(
# default dropout in megatron is 0.1
--attention-dropout 0.0
From cda7305b739ac8095db80a93d8291d258a1abbcc Mon Sep 17 00:00:00 2001
From: Tgz27 <617796318@qq.com>
Date: Thu, 3 Sep 2026 14:40:43 +0800
Subject: [PATCH 12/16] fix(sglang-npu.patch): conv weight cache invalidation,
logprobs field name, qwen2_moe NPU alt_stream, duplicate topk
fix(sglang-npu.patch): conv weight cache invalidation, logprobs field name, qwen2_moe NPU alt_stream, duplicate topk
---
docker/npu_patch/sglang-npu.patch | 201 ++++++++++++++++++++++++++++--
1 file changed, 191 insertions(+), 10 deletions(-)
diff --git a/docker/npu_patch/sglang-npu.patch b/docker/npu_patch/sglang-npu.patch
index 52cbe46e0..13d067766 100644
--- a/docker/npu_patch/sglang-npu.patch
+++ b/docker/npu_patch/sglang-npu.patch
@@ -36,21 +36,23 @@ index 7f506e7f1a..204b9382e8 100644
class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
-@@ -109,6 +102,13 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
+@@ -109,6 +102,15 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
self._prepare_mamba_track_metadata(forward_batch)
self.graph_mode = False
+ def _get_conv_weights_t(self, layer: RadixLinearAttention) -> torch.Tensor:
+ w = getattr(layer, "_conv_weights_t", None)
-+ if w is None:
++ version = layer.conv_weights._version
++ if w is None or version != getattr(layer, "_conv_weights_t_version", None):
+ w = layer.conv_weights.transpose(0, 1).contiguous()
+ layer._conv_weights_t = w
++ layer._conv_weights_t_version = version
+ return w
+
def forward_decode(
self,
layer: RadixLinearAttention,
-@@ -125,16 +125,17 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
+@@ -125,16 +127,17 @@ class AscendGDNAttnBackend(AscendMambaAttnBackendBase):
cache_indices = self.forward_metadata.mamba_cache_indices
assert isinstance(mixed_qkv, torch.Tensor)
@@ -473,13 +475,14 @@ index e83e4157fa..19ed3f244b 100644
# Attach logprobs to logits_output (in-place modification)
if any(x > 0 for x in top_logprobs_nums):
-@@ -363,6 +547,20 @@ class Sampler(nn.Module):
- logits_output.next_token_top_logprobs_val,
- logits_output.next_token_top_logprobs_idx,
- ) = get_top_logprobs(logprobs, top_logprobs_nums, no_copy_to_cpu=True)
+@@ -362,7 +546,17 @@ class Sampler(nn.Module):
+- (
+- logits_output.next_token_top_logprobs_val,
+- logits_output.next_token_top_logprobs_idx,
+- ) = get_top_logprobs(logprobs, top_logprobs_nums, no_copy_to_cpu=True)
+ # Same extraction as get_top_logprobs, but clamp the
+ # [batch, max_k] topk result in a single kernel before
-+ # slicing per request.
++ # slicing per request. Runs topk exactly once.
+ max_k = max(top_logprobs_nums)
+ top_vals, top_idx = logprobs.topk(max_k, dim=-1)
+ if logprobs_are_probs:
@@ -494,7 +497,7 @@ index e83e4157fa..19ed3f244b 100644
if any(x is not None for x in token_ids_logprobs):
(
-@@ -372,10 +570,19 @@ class Sampler(nn.Module):
+@@ -372,10 +567,19 @@ class Sampler(nn.Module):
logprobs, token_ids_logprobs, no_copy_to_cpu=True
)
@@ -502,7 +505,7 @@ index e83e4157fa..19ed3f244b 100644
- torch.arange(len(batch_next_token_ids), device=sampling_info.device),
- batch_next_token_ids,
- ]
-+ for row in logits_output.token_ids_logprobs_val:
++ for row in logits_output.next_token_token_ids_logprobs_val:
+ if torch.is_tensor(row):
+ if logprobs_are_probs:
+ row.log_()
@@ -720,6 +723,22 @@ index d5aac381dc..e710d83b05 100644
if shared_output is not None:
final_hidden_states.add_(shared_output)
+@@ -991,7 +990,14 @@ class Qwen2MoeForCausalLM(nn.Module):
+ self.pp_group = get_pp_group()
+ self.config = config
+ self.quant_config = quant_config
+- alt_stream = torch.cuda.Stream() if _is_cuda else None
++ # Mirror qwen3_5.py: on NPU, SGLANG_NPU_USE_MULTI_STREAM also needs a
++ # real stream — _forward_deepep's dual-stream branch dereferences
++ # self.alt_stream unconditionally once enabled.
++ alt_stream = (
++ torch.cuda.Stream()
++ if _is_cuda or (is_npu() and envs.SGLANG_NPU_USE_MULTI_STREAM.get())
++ else None
++ )
+ self.model = Qwen2MoeModel(
+ config,
+ quant_config,
diff --git a/python/sglang/srt/models/qwen3_5.py b/python/sglang/srt/models/qwen3_5.py
index fed78cf888..7b6505fa10 100644
--- a/python/sglang/srt/models/qwen3_5.py
@@ -1056,3 +1075,165 @@ index 9ad577b42e..390a3721c5 100755
def _setup_tree_and_allocator(self, enable_kv_cache_events=False):
"""Helper to create a MambaRadixCache with allocator for testing."""
server_args = ServerArgs(model_path="dummy", page_size=1)
+diff --git a/test/registered/unit/layers/test_sampler_attach_logprobs.py b/test/registered/unit/layers/test_sampler_attach_logprobs.py
+new file mode 100644
+index 0000000000..9dfc930392 100644
+--- /dev/null
++++ b/test/registered/unit/layers/test_sampler_attach_logprobs.py
+@@ -0,0 +1,156 @@
++"""Unit tests for Sampler._attach_logprobs_to_output.
++
++Regression coverage for two bugs in the decode logprob attach path:
++ - iterating ``logits_output.token_ids_logprobs_val`` (a nonexistent field on
++ LogitsProcessorOutput) instead of ``next_token_token_ids_logprobs_val``
++ raised AttributeError for any request with specific token-ID logprobs;
++ - the top-logprob branch ran a full-vocab topk twice (get_top_logprobs plus
++ an immediate re-topk that overwrote the first result).
++
++The method under test does not touch ``self``, so the Sampler is allocated
++with ``__new__`` to skip the distributed-group init in ``__init__``.
++"""
++
++import unittest
++
++import torch
++
++from sglang.test.ci.ci_register import register_cpu_ci
++from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
++
++maybe_stub_sgl_kernel()
++
++from sglang.srt.layers.logits_processor import LogitsProcessorOutput
++from sglang.srt.layers.sampler import Sampler
++
++register_cpu_ci(est_time=10, suite="base-a-test-cpu")
++
++_VOCAB = 1000
++
++
++def _make_sampler() -> Sampler:
++ # _attach_logprobs_to_output never reads instance state; bypass __init__
++ # (which requires an initialized torch distributed group).
++ return Sampler.__new__(Sampler)
++
++
++def _attach(
++ logprobs: torch.Tensor,
++ top_logprobs_nums,
++ token_ids_logprobs,
++ batch_next_token_ids: torch.Tensor,
++ logprobs_are_probs: bool = False,
++) -> LogitsProcessorOutput:
++ logits_output = LogitsProcessorOutput(next_token_logits=None)
++ _make_sampler()._attach_logprobs_to_output(
++ logits_output=logits_output,
++ logprobs=logprobs,
++ top_logprobs_nums=top_logprobs_nums,
++ token_ids_logprobs=token_ids_logprobs,
++ sampling_info=None,
++ batch_next_token_ids=batch_next_token_ids,
++ logprobs_are_probs=logprobs_are_probs,
++ )
++ return logits_output
++
++
++class TestAttachTokenIdsLogprobs(CustomTestCase):
++
++ def test_token_ids_logprobs_no_attribute_error(self):
++ """Specific token-ID logprob requests must not raise AttributeError."""
++ logprobs = torch.log_softmax(torch.randn(2, _VOCAB), dim=-1)
++ out = _attach(
++ logprobs=logprobs.clone(),
++ top_logprobs_nums=[0, 0],
++ token_ids_logprobs=[[5, 7], [42]],
++ batch_next_token_ids=torch.tensor([3, 4], dtype=torch.int32),
++ )
++ self.assertIsNotNone(out.next_token_token_ids_logprobs_val)
++ self.assertIsNotNone(out.next_token_token_ids_logprobs_idx)
++
++ def test_token_ids_logprobs_values(self):
++ logprobs = torch.log_softmax(torch.randn(3, _VOCAB), dim=-1)
++ reference = logprobs.clone()
++ requested = [[5, 7], None, [42]]
++ out = _attach(
++ logprobs=logprobs.clone(),
++ top_logprobs_nums=[0, 0, 0],
++ token_ids_logprobs=requested,
++ batch_next_token_ids=torch.tensor([3, 4, 9], dtype=torch.int32),
++ )
++ vals = out.next_token_token_ids_logprobs_val
++ idxs = out.next_token_token_ids_logprobs_idx
++ self.assertEqual(len(vals), 3)
++ torch.testing.assert_close(
++ vals[0], reference[0, torch.tensor([5, 7])], rtol=0, atol=0
++ )
++ self.assertEqual(idxs[0], [5, 7])
++ # None request yields empty placeholders.
++ self.assertEqual(vals[1], [])
++ self.assertEqual(idxs[1], [])
++ torch.testing.assert_close(
++ vals[2], reference[2, torch.tensor([42])], rtol=0, atol=0
++ )
++ self.assertEqual(idxs[2], [42])
++
++ def test_token_ids_logprobs_probs_input(self):
++ """logprobs_are_probs=True applies log() before clamping, per row."""
++ probs = torch.softmax(torch.randn(2, _VOCAB), dim=-1)
++ reference = probs.clone()
++ out = _attach(
++ logprobs=probs.clone(),
++ top_logprobs_nums=[0, 0],
++ token_ids_logprobs=[[1, 2], [3]],
++ batch_next_token_ids=torch.tensor([0, 1], dtype=torch.int32),
++ logprobs_are_probs=True,
++ )
++ torch.testing.assert_close(
++ out.next_token_token_ids_logprobs_val[0],
++ reference[0, torch.tensor([1, 2])].log(),
++ rtol=1e-6,
++ atol=1e-6,
++ )
++
++
++class TestAttachTopLogprobs(CustomTestCase):
++
++ def test_top_logprobs_values(self):
++ """Per-request k slicing stays correct with the single-topk path."""
++ logprobs = torch.log_softmax(torch.randn(2, _VOCAB), dim=-1)
++ reference = logprobs.clone()
++ out = _attach(
++ logprobs=logprobs.clone(),
++ top_logprobs_nums=[3, 1],
++ token_ids_logprobs=[None, None],
++ batch_next_token_ids=torch.tensor([3, 4], dtype=torch.int32),
++ )
++ ref_vals, ref_idx = reference.topk(3, dim=-1)
++ self.assertEqual(len(out.next_token_top_logprobs_val), 2)
++ torch.testing.assert_close(
++ out.next_token_top_logprobs_val[0], ref_vals[0], rtol=0, atol=0
++ )
++ self.assertTrue(torch.equal(out.next_token_top_logprobs_idx[0], ref_idx[0]))
++ torch.testing.assert_close(
++ out.next_token_top_logprobs_val[1], ref_vals[1][:1], rtol=0, atol=0
++ )
++ self.assertTrue(torch.equal(out.next_token_top_logprobs_idx[1], ref_idx[1][:1]))
++
++ def test_next_token_logprobs_gather(self):
++ logprobs = torch.log_softmax(torch.randn(2, _VOCAB), dim=-1)
++ reference = logprobs.clone()
++ out = _attach(
++ logprobs=logprobs.clone(),
++ top_logprobs_nums=[0, 0],
++ token_ids_logprobs=[None, None],
++ batch_next_token_ids=torch.tensor([3, 4], dtype=torch.int32),
++ )
++ torch.testing.assert_close(
++ out.next_token_logprobs,
++ torch.stack([reference[0, 3], reference[1, 4]]),
++ rtol=0,
++ atol=0,
++ )
++
++
++if __name__ == "__main__":
++ unittest.main()
From c53df5b134231dc53c20ad95f73ecd06d6d48f29 Mon Sep 17 00:00:00 2001
From: dabuliu123 <270334047@qq.com>
Date: Fri, 4 Sep 2026 09:57:23 +0800
Subject: [PATCH 13/16] docs(npu): align BASE_IMAGE default with CANN 9.0.0
Dockerfile
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
feat(docker): add Ascend/NPU multi-stage image build with Makefile entrypoints
Bring the GPU image pipeline's layered design to Ascend/910C so 云效 only
selects code version, params, and scheduling while the Relax repo defines how
images are built.
- **`docker/Dockerfile.npu`**: refactor the monolithic NPU build into multi-stage
`base → train → relax`, mirroring `docker/Dockerfile` (GPU).
- `train` holds the Relax-code-independent heavy deps (torch_npu source build,
MindSpeed/Megatron/MindSpeed-Bridge/Megatron-Bridge, sglang-npu, sgl-kernel-npu)
and only `COPY docker/npu_patch` — a Relax code change no longer rebuilds them.
- `relax` copies the full checkout (incl. `.git` for versioneer) and installs it;
`ray`/`protobuf` pins stay after `pip install -e .` so they still win.
- `BASE_IMAGE` / `SOC_VERSION` / `TRAIN_IMAGE` are now build args (default base
`quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11`, SOC `ascend910_9391`).
- A plain `docker build -f docker/Dockerfile.npu .` still works via the chained
default `TRAIN_IMAGE=train`.
- **`Makefile`**: add `docker-train-ascend`, `docker-dev-ascend`, and optional
`docker-qs-ascend`, reusing the GPU tag/push/remote-skip logic.
- Tags `ascend-train|dev|qs-YYYYMMDD-`; the `ascend-` prefix keeps aarch64
artifacts from overwriting the amd64 `train-/dev-` tags in the same repository.
- `docker-qs-ascend` builds relax-ci's `Dockerfile.qs` (verified pure-python /
arch-independent) on top of the dev image via `ASCEND_QS_DOCKERFILE`; cloning
relax-ci and its credentials stay in CI, not the repo.
- **`docker/npu-training.md`**: document the multi-stage layout, the Makefile build
paths, configurable variables, and the optional QS wrapping.
Co-Authored-By: Claude
(cherry picked from commit 71f7a80c43dadf189c010c3ce5f5748784af5f21)
(cherry picked from commit d9b9265418a24d382eaea279940a5391cec7fe38)
chore: migrate references from `redai-infra` to `redai-studio` (#296)
(cherry picked from commit 03cd24bcd7d739200eaa15543e2a7892cc4c7d9a)
docs(npu): align BASE_IMAGE default with CANN 9.0.0 Dockerfile
---
.github/ISSUE_TEMPLATE/config.yml | 4 +-
.github/workflows/deploy-docs.yml | 4 +-
CONTRIBUTING.md | 6 +-
Makefile | 66 ++++++++++-
README.md | 16 +--
README_zh.md | 16 +--
docker/Dockerfile | 4 +-
docker/Dockerfile.npu | 103 ++++++++++--------
docker/npu-training.md | 32 +++++-
docs/.vitepress/config.mts | 6 +-
docs/.vitepress/theme/CallToAction.vue | 4 +-
docs/deploy-docs.sh | 2 +-
docs/en/api/actor-fwd.md | 4 +-
docs/en/api/actor.md | 4 +-
docs/en/api/genrm.md | 4 +-
docs/en/api/rollout.md | 4 +-
docs/en/guide/configuration.md | 2 +-
docs/en/guide/customize-training.md | 4 +-
docs/en/guide/fully-async-training.md | 2 +-
docs/en/guide/how-to-contribute.md | 2 +-
docs/en/guide/installation.md | 12 +-
docs/en/guide/introduction.md | 4 +-
.../reinforce-plus-plus-training-report.md | 2 +-
docs/en/index.md | 2 +-
docs/zh/api/actor-fwd.md | 4 +-
docs/zh/api/actor.md | 4 +-
docs/zh/api/genrm.md | 4 +-
docs/zh/api/rollout.md | 4 +-
docs/zh/guide/configuration.md | 2 +-
docs/zh/guide/customize-training.md | 4 +-
docs/zh/guide/fully-async-training.md | 2 +-
docs/zh/guide/how-to-contribute.md | 2 +-
docs/zh/guide/installation.md | 12 +-
docs/zh/guide/introduction.md | 4 +-
.../reinforce-plus-plus-training-report.md | 2 +-
docs/zh/index.md | 2 +-
examples/nemo_gym_agentic/README.md | 4 +-
.../nemo_gym_agentic/recipes/gsm8k/README.md | 4 +-
.../recipes/r2e-gym/README.md | 4 +-
.../recipes/workplace-assistant/README.md | 4 +-
examples/nemo_gym_agentic/service/Dockerfile | 2 +-
relax/backends/megatron/model_provider.py | 2 +-
relax/utils/arguments.py | 2 +-
relax/utils/visualize/templates.py | 4 +-
setup.py | 2 +-
skills/sglang-upgrade/SKILL.md | 2 +-
skills/sync-github/SKILL.md | 12 +-
.../references/prompt-b-dev-to-main.md | 12 +-
48 files changed, 256 insertions(+), 153 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 34faf1138..e997cf03b 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Documentation
- url: https://redai-infra.github.io/Relax
+ url: https://redai-studio.github.io/Relax
about: Check the documentation for guides and API reference
- name: Questions & Discussions
- url: https://github.com/redai-infra/Relax/discussions
+ url: https://github.com/redai-studio/Relax/discussions
about: Ask questions and discuss ideas with the community
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 9d8719240..bd797817a 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -22,7 +22,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
- if: github.repository == 'redai-infra/Relax'
+ if: github.repository == 'redai-studio/Relax'
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -61,7 +61,7 @@ jobs:
deploy:
needs: build
runs-on: ubuntu-latest
- if: github.repository == 'redai-infra/Relax'
+ if: github.repository == 'redai-studio/Relax'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 062d4ebd3..b24745f0d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,7 +42,7 @@ This project follows a standard code of conduct. Please be respectful, inclusive
```bash
# Clone the repository
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
cd Relax
# Create virtual environment (recommended)
@@ -155,7 +155,7 @@ feat(rollout): add streaming data consumption for async mode
## Reporting Bugs
-Use the [Bug Report template](https://github.com/redai-infra/Relax/issues/new?template=bug_report.md) and include:
+Use the [Bug Report template](https://github.com/redai-studio/Relax/issues/new?template=bug_report.md) and include:
- **Environment** — OS, Python version, CUDA version, GPU type
- **Steps to Reproduce** — Minimal commands to trigger the bug
@@ -165,7 +165,7 @@ Use the [Bug Report template](https://github.com/redai-infra/Relax/issues/new?te
## Requesting Features
-Use the [Feature Request template](https://github.com/redai-infra/Relax/issues/new?template=feature_request.md) and include:
+Use the [Feature Request template](https://github.com/redai-studio/Relax/issues/new?template=feature_request.md) and include:
- **Problem Statement** — What problem does this solve?
- **Proposed Solution** — How should it work?
diff --git a/Makefile b/Makefile
index 79039702b..e43221b01 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,10 @@
-.PHONY: help install test lint format clean docs docs-dev docs-build docs-preview check-registry check-train-image docker-train docker-dev
+.PHONY: help install test lint format clean docs docs-dev docs-build docs-preview check-registry check-train-image check-ascend-qs-base-image check-qs-dockerfile docker-train docker-dev docker-ascend docker-qs-ascend
DOCKER ?= docker
DOCKERFILE ?= docker/Dockerfile
+ASCEND_DOCKERFILE ?= docker/Dockerfile.npu
+SOC_VERSION ?= ascend910_9391
+ASCEND_DOCKER_BUILDKIT ?= 1
DOCKER_BUILD_PROGRESS ?= plain
DOCKER_BUILD_ARGS ?=
DO_PUSH ?= 1
@@ -13,7 +16,19 @@ IMAGE_REGISTRY := $(patsubst %/,%,$(strip $(REGISTRY)))
DEFAULT_TRAIN_IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPOSITORY):train-$(BUILD_DATE)-$(GIT_SHORT_HASH)
DEV_IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPOSITORY):dev-$(BUILD_DATE)-$(GIT_SHORT_HASH)
-ifeq ($(origin TRAIN_IMAGE),undefined)
+# Ascend/NPU images share the same repository as GPU; the ascend- tag prefix keeps
+# aarch64 artifacts from ever overwriting the amd64 train-/dev- tags.
+ASCEND_DEV_IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPOSITORY):ascend-dev-$(BUILD_DATE)-$(GIT_SHORT_HASH)
+ASCEND_QS_IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPOSITORY):ascend-qs-$(BUILD_DATE)-$(GIT_SHORT_HASH)
+
+# QS wrapping reuses the external relax-ci Dockerfile.qs (verified pure-python /
+# arch-independent, so ARM64-safe). CI is responsible for checking out relax-ci and
+# pointing ASCEND_QS_DOCKERFILE at its docker/Dockerfile.qs; the Relax repo embeds neither
+# the external repo nor its credentials. ASCEND_QS_BASE_IMAGE defaults to the dev image.
+ASCEND_QS_DOCKERFILE ?=
+ASCEND_QS_BASE_IMAGE ?= $(ASCEND_DEV_IMAGE)
+
+ifeq ($(strip $(TRAIN_IMAGE)),)
TRAIN_IMAGE := $(DEFAULT_TRAIN_IMAGE)
BUILD_DEFAULT_TRAIN := 1
else
@@ -40,6 +55,10 @@ help:
@echo " REGISTRY=... make docker-train - Build and push the Docker train stage"
@echo " REGISTRY=... make docker-dev - Build and push the Docker development image"
@echo " REGISTRY=... TRAIN_IMAGE=... make docker-dev - Build dev from an existing train image"
+ @echo " REGISTRY=... make docker-ascend - Build and push the complete Ascend/NPU image"
+ @echo " REGISTRY=... ASCEND_QS_DOCKERFILE=... make docker-qs-ascend - Wrap an Ascend dev image into a QS image (optional)"
+ @echo " Ascend targets accept BASE_IMAGE=... and SOC_VERSION=... (default ascend910_9391)"
+ @echo " Ascend targets use BuildKit by default; set ASCEND_DOCKER_BUILDKIT=0 for legacy DinD"
@echo " Set DO_PUSH=0 before make to skip pushing Docker images"
@echo " Existing remote images are skipped; DO_PUSH=0 checks local images"
@@ -84,6 +103,13 @@ check-registry:
check-train-image:
@test -n "$(strip $(TRAIN_IMAGE))" || { echo "TRAIN_IMAGE must not be empty" >&2; exit 2; }
+check-ascend-qs-base-image:
+ @test -n "$(strip $(ASCEND_QS_BASE_IMAGE))" || { echo "ASCEND_QS_BASE_IMAGE must not be empty" >&2; exit 2; }
+
+check-qs-dockerfile:
+ @test -n "$(strip $(ASCEND_QS_DOCKERFILE))" || { echo "ASCEND_QS_DOCKERFILE is required (path to relax-ci docker/Dockerfile.qs)" >&2; exit 2; }
+ @test -f "$(strip $(ASCEND_QS_DOCKERFILE))" || { echo "ASCEND_QS_DOCKERFILE not found: $(ASCEND_QS_DOCKERFILE)" >&2; exit 2; }
+
docker-train: check-registry
@echo "[docker] output train image: $(TRAIN_IMAGE)"
@set -e; \
@@ -118,3 +144,39 @@ docker-dev: check-registry check-train-image
.; \
if [ "$(DO_PUSH)" != "0" ]; then $(DOCKER) push "$(DEV_IMAGE)"; fi; \
fi
+
+docker-ascend: check-registry
+ @echo "[docker] output ascend image: $(ASCEND_DEV_IMAGE)"
+ @set -e; \
+ if $(IMAGE_INSPECT) "$(ASCEND_DEV_IMAGE)" >/dev/null 2>&1; then \
+ echo "[docker] skip existing $(IMAGE_LOCATION) ascend image: $(ASCEND_DEV_IMAGE)"; \
+ else \
+ DOCKER_BUILDKIT=$(ASCEND_DOCKER_BUILDKIT) $(DOCKER) build --progress=$(DOCKER_BUILD_PROGRESS) \
+ -f $(ASCEND_DOCKERFILE) \
+ --target relax \
+ -t "$(ASCEND_DEV_IMAGE)" \
+ --build-arg SOC_VERSION="$(SOC_VERSION)" \
+ $(PROXY_BUILD_ARGS) $(BASE_IMAGE_BUILD_ARG) $(DOCKER_BUILD_ARGS) \
+ .; \
+ if [ "$(DO_PUSH)" != "0" ]; then $(DOCKER) push "$(ASCEND_DEV_IMAGE)"; fi; \
+ fi
+
+# Optional: wrap an Ascend dev image into an internal QS image using relax-ci's
+# Dockerfile.qs. ASCEND_QS_DOCKERFILE must point at a relax-ci checkout; ASCEND_QS_BASE_IMAGE
+# defaults to the dev image built above but can be any existing Ascend dev image.
+docker-qs-ascend: check-registry check-qs-dockerfile check-ascend-qs-base-image
+ @echo "[docker] input ascend dev image: $(ASCEND_QS_BASE_IMAGE)"
+ @echo "[docker] output ascend qs image: $(ASCEND_QS_IMAGE)"
+ @set -e; \
+ if $(IMAGE_INSPECT) "$(ASCEND_QS_IMAGE)" >/dev/null 2>&1; then \
+ echo "[docker] skip existing $(IMAGE_LOCATION) ascend qs image: $(ASCEND_QS_IMAGE)"; \
+ else \
+ DOCKER_BUILDKIT=$(ASCEND_DOCKER_BUILDKIT) $(DOCKER) build --progress=$(DOCKER_BUILD_PROGRESS) \
+ --no-cache \
+ -f "$(ASCEND_QS_DOCKERFILE)" \
+ -t "$(ASCEND_QS_IMAGE)" \
+ --build-arg BASE_IMAGE="$(ASCEND_QS_BASE_IMAGE)" \
+ $(PROXY_BUILD_ARGS) $(DOCKER_BUILD_ARGS) \
+ "$(dir $(ASCEND_QS_DOCKERFILE))"; \
+ if [ "$(DO_PUSH)" != "0" ]; then $(DOCKER) push "$(ASCEND_QS_IMAGE)"; fi; \
+ fi
diff --git a/README.md b/README.md
index 03c93a2e8..79d644df8 100644
--- a/README.md
+++ b/README.md
@@ -16,13 +16,13 @@
-
+
-
+
-
+
@@ -34,7 +34,7 @@
______________________________________________________________________
-**Relax** (**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality) is a high-performance reinforcement learning post-training framework open-sourced by the Xiaohongshu AI Infra Team for multimodal large language models. Built on Ray Serve with a service-oriented architecture, Relax uses Megatron-LM as the training backend and SGLang as the inference engine. Through the [TransferQueue](https://github.com/redai-infra/TransferQueue) data transfer system, it achieves complete decoupling of training and inference, supporting end-to-end multimodal RL training from text to images, videos, and audio.
+**Relax** (**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality) is a high-performance reinforcement learning post-training framework open-sourced by the Xiaohongshu AI Infra Team for multimodal large language models. Built on Ray Serve with a service-oriented architecture, Relax uses Megatron-LM as the training backend and SGLang as the inference engine. Through the [TransferQueue](https://github.com/redai-studio/TransferQueue) data transfer system, it achieves complete decoupling of training and inference, supporting end-to-end multimodal RL training from text to images, videos, and audio.
______________________________________________________________________
@@ -129,15 +129,15 @@ The recommended way to run Relax is via the official Docker image, which ships w
```bash
# Pull the official image
-docker pull ghcr.io/redai-infra/relaxrl:latest
+docker pull ghcr.io/redai-studio/relaxrl:latest
# Launch a container with GPUs, shared memory, and your workspace mounted
docker run -it --gpus all --ipc=host --network=host \
-v /path/to/your/workspace:/root \
- ghcr.io/redai-infra/relaxrl:latest bash
+ ghcr.io/redai-studio/relaxrl:latest bash
# Inside the container
-git clone https://github.com/redai-infra/Relax.git /root/Relax
+git clone https://github.com/redai-studio/Relax.git /root/Relax
cd /root/Relax && pip install -e .
```
@@ -246,7 +246,7 @@ ______________________________________________________________________
## 📚 Documentation
-Full bilingual documentation is available at **[redai-infra.github.io/Relax](https://redai-infra.github.io/Relax)**.
+Full bilingual documentation is available at **[redai-studio.github.io/Relax](https://redai-studio.github.io/Relax)**.
______________________________________________________________________
diff --git a/README_zh.md b/README_zh.md
index af13a4c36..faf13e888 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -16,13 +16,13 @@
-
+
-
+
-
+
@@ -34,7 +34,7 @@
______________________________________________________________________
-**Relax**(**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality)是小红书 AI 平台开源的、面向多模态大模型的高性能强化学习后训练框架。Relax 基于 Ray Serve 构建面向服务的架构,以 Megatron-LM 为训练后端、SGLang 为推理引擎,通过 [TransferQueue](https://github.com/redai-infra/TransferQueue) 数据传输系统实现训练与推理的完全解耦,支持从文本到图像、视频、音频的全模态强化学习训练。
+**Relax**(**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality)是小红书 AI 平台开源的、面向多模态大模型的高性能强化学习后训练框架。Relax 基于 Ray Serve 构建面向服务的架构,以 Megatron-LM 为训练后端、SGLang 为推理引擎,通过 [TransferQueue](https://github.com/redai-studio/TransferQueue) 数据传输系统实现训练与推理的完全解耦,支持从文本到图像、视频、音频的全模态强化学习训练。
______________________________________________________________________
@@ -129,15 +129,15 @@ ______________________________________________________________________
```bash
# 拉取官方镜像
-docker pull ghcr.io/redai-infra/relaxrl:latest
+docker pull ghcr.io/redai-studio/relaxrl:latest
# 启动容器,挂载 GPU、共享内存与工作目录
docker run -it --gpus all --ipc=host --network=host \
-v /path/to/your/workspace:/root \
- ghcr.io/redai-infra/relaxrl:latest bash
+ ghcr.io/redai-studio/relaxrl:latest bash
# 容器内克隆仓库并安装
-git clone https://github.com/redai-infra/Relax.git /root/Relax
+git clone https://github.com/redai-studio/Relax.git /root/Relax
cd /root/Relax && pip install -e .
```
@@ -246,7 +246,7 @@ ______________________________________________________________________
## 📚 文档
-完整的双语文档请访问 **[redai-infra.github.io/Relax](https://redai-infra.github.io/Relax)**。
+完整的双语文档请访问 **[redai-studio.github.io/Relax](https://redai-studio.github.io/Relax)**。
______________________________________________________________________
diff --git a/docker/Dockerfile b/docker/Dockerfile
index c473e89b0..70e98cd9f 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -68,7 +68,7 @@ RUN MAX_JOBS=64 \
rm -rf /opt/flash-attention/
RUN pip -v install --no-cache-dir --no-build-isolation "transformer_engine[pytorch]==2.14.1" && \
- TMS_CUDA_MAJOR=$(python -c 'import torch; print(torch.version.cuda.split(".")[0])') pip install git+https://github.com/redai-infra/torch_memory_saver.git@afc13785c50119048e2dd8ac497cc9e29ec75bd4 --no-cache-dir --force-reinstall && \
+ TMS_CUDA_MAJOR=$(python -c 'import torch; print(torch.version.cuda.split(".")[0])') pip install git+https://github.com/redai-studio/torch_memory_saver.git@afc13785c50119048e2dd8ac497cc9e29ec75bd4 --no-cache-dir --force-reinstall && \
pip install nvidia-modelopt[torch]>=0.37.0 --no-build-isolation --no-cache-dir && \
pip install "numpy<2" nvidia-cudnn-cu12==9.16.0.29 --no-cache-dir && \
NVCC_APPEND_FLAGS="--threads 32" \
@@ -116,7 +116,7 @@ COPY requirements.txt /tmp/requirements.txt
RUN pip install --ignore-installed PyJWT && \
pip install -r /tmp/requirements.txt --no-cache-dir && \
pip install --no-cache-dir "compressed_tensors>=0.13.0" tensordict==0.10.0 pyvers==0.1.0 'nvidia-modelopt[hf]==0.44.0' --no-deps && \
- pip install "transferqueue @ git+https://github.com/redai-infra/TransferQueue.git@58054a33834aadbcf76aacd6b1e32e25c030f2c9" --no-deps
+ pip install "transferqueue @ git+https://github.com/redai-studio/TransferQueue.git@58054a33834aadbcf76aacd6b1e32e25c030f2c9" --no-deps
# sgl-router: override the official wheel (pulled by requirements.txt above) with
# slime's r3-capable fork. The official sglang-router drops the routed_experts
diff --git a/docker/Dockerfile.npu b/docker/Dockerfile.npu
index ca4619d3e..bfc4f1a12 100644
--- a/docker/Dockerfile.npu
+++ b/docker/Dockerfile.npu
@@ -1,8 +1,10 @@
-#
+# Ascend/NPU image: CANN base -> training stack -> runtime dependencies.
+# CI can reuse a published train image through TRAIN_IMAGE.
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
-FROM quay.io/ascend/cann:9.0.0-a3-ubuntu22.04-py3.11
+ARG BASE_IMAGE=quay.io/ascend/cann:9.0.0-a3-ubuntu22.04-py3.11
+FROM ${BASE_IMAGE} as base
ARG HTTP_PROXY
ARG HTTPS_PROXY
@@ -35,7 +37,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-ins
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-# Setting env
+# Configure CANN.
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/aarch64-linux/devlib/linux/aarch64:$LD_LIBRARY_PATH; \
@@ -45,9 +47,7 @@ RUN ARCH=$(uname -m) && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
source /usr/local/Ascend/nnal/atb/set_env.sh && \
source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh
-
-
-# Setting pip & git config. Global config (set once, persists across subsequent RUN layers)
+# Configure pip and git.
ARG PIP_INDEX_URL="https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
RUN pip config set global.index-url ${PIP_INDEX_URL} && \
git config --global http.sslverify false && \
@@ -55,82 +55,98 @@ RUN pip config set global.index-url ${PIP_INDEX_URL} && \
git config --global http.postBuffer 2147483648 && \
git config --global user.email "temp@example.com" && \
git config --global user.name "temp"
+WORKDIR /root
+
+FROM base as train
-# install torch
-RUN pip install --upgrade pip packaging setuptools==80.10.2 && \
- pip install torch==2.9.0 && \
- pip install numpy==1.26.0
+# Ascend chip target, defaulting to 910C.
+ARG SOC_VERSION=ascend910_9391
+ENV SOC_VERSION=${SOC_VERSION}
-# build torch_npu
-RUN pip install pyyaml && \
+WORKDIR /root
+RUN pip_install() { pip install "$@" || pip install --index-url https://pypi.org/simple "$@"; }; \
+ pip_install --upgrade pip packaging setuptools==80.10.2 && \
+ pip_install torch==2.9.0 && \
+ pip_install numpy==1.26.0
+# Build torch_npu.
+RUN pip_install() { pip install "$@" || pip install --index-url https://pypi.org/simple "$@"; }; \
+ pip_install pyyaml && \
git clone https://gitcode.com/Ascend/pytorch.git /root/pytorch && \
cd /root/pytorch && \
git checkout v26.0.1-pytorch2.9.0 && \
git cherry-pick -n f495de675bce38a2fa21edbf067b73d2a5f26733 && \
bash ci/build.sh --python=3.11 && \
pip install dist/torch_npu*.whl
-
-# install triton/TQ
RUN pip install triton-ascend==3.2.1 --extra-index-url=https://triton-ascend.osinfra.cn/pypi/simple && \
- pip install tensordict==0.10.0 pyvers==0.1.0 --no-deps && \
- pip install "transferqueue @ git+https://github.com/redai-infra/TransferQueue.git@58054a33834aadbcf76aacd6b1e32e25c030f2c9" --no-deps
-
-# Clone Megatron-LM, MindSpeed, MindSpeed-Bridge, Megatron-Bridge and install
+ cd /root && rm -rf /root/pytorch
+RUN pip_install() { pip install "$@" || pip install --index-url https://pypi.org/simple "$@"; }; \
+ pip_install tensordict==0.10.0 pyvers==0.1.0 --no-deps
+RUN git clone https://github.com/redai-studio/TransferQueue.git /root/TransferQueue && \
+ cd /root/TransferQueue && \
+ git checkout 58054a33834aadbcf76aacd6b1e32e25c030f2c9 && \
+ pip install /root/TransferQueue --no-deps --no-build-isolation && \
+ cd /root && \
+ rm -rf /root/TransferQueue
+
+# Install the MindSpeed/Megatron stack.
RUN git clone https://gitcode.com/ascend/MindSpeed.git /root/MindSpeed && \
git clone https://github.com/NVIDIA/Megatron-LM.git /root/Megatron-LM && \
git clone https://gitcode.com/ascend/MindSpeed-Ops.git /root/MindSpeed-Ops && \
git clone https://gitcode.com/ascend/MindSpeed-Bridge.git /root/MindSpeed-Bridge && \
git clone https://github.com/NVIDIA-NeMo/Megatron-Bridge.git /root/Megatron-Bridge
-RUN cd /root/MindSpeed && git checkout core_r0.16.0 && pip install -r requirements.txt && pip install -e . && git checkout e4772499 && \
+RUN pip_install() { pip install "$@" || pip install --index-url https://pypi.org/simple "$@"; }; \
+ source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
+ source /usr/local/Ascend/nnal/atb/set_env.sh && \
+ source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh && \
+ cd /root/MindSpeed && git checkout core_r0.16.0 && pip install -r requirements.txt && pip install -e . && git checkout e4772499 && \
cd /root/Megatron-LM && git checkout core_v0.16.1 && pip install -e . --no-build-isolation && \
cd /root/Megatron-Bridge && git checkout v0.3.1 && \
- cd /root/MindSpeed-Ops/ && git checkout 33ac80f7 && pip install -e . --no-build-isolation --no-deps && \
- cd /root/MindSpeed-Bridge/ && git checkout v0.3.1 && pip install -r requirements.txt && pip install -e . --no-deps
+ cd /root/MindSpeed-Ops && git checkout 33ac80f7 && pip install -e . --no-build-isolation --no-deps && \
+ cd /root/MindSpeed-Bridge && git checkout v0.3.1 && pip install -r requirements.txt && pip install -e . --no-deps
+# Keep Relax code changes out of the dependency cache.
+COPY docker/npu_patch /root/Relax/docker/npu_patch
-COPY . /root/Relax
-# Patch Megatron-LM, MindSpeed, MindSpeed-Bridge, MindSpeed-Ops
+# Apply NPU patches.
RUN cd /root/Megatron-Bridge && \
- patch -p1 < /root/Relax/docker/npu_patch/megatron-bridge.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/megatron-bridge.patch && \
git add . && git commit -m "base line" && \
cd /root/Megatron-LM && \
- patch -p1 < /root/Relax/docker/npu_patch/megatron.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/megatron.patch && \
git add . && git commit -m "base line" && \
cd /root/MindSpeed-Bridge && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed-bridge.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed-bridge.patch && \
git add . && git commit -m "base line" && \
cd /root/MindSpeed-Ops && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed-ops.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed-ops.patch && \
git add . && git commit -m "base line" && \
cd /root/MindSpeed && \
- patch -p1 < /root/Relax/docker/npu_patch/mindspeed.patch && \
+ patch -p1 < /root/Relax/docker/npu_patch/mindspeed.patch && \
git add . && git commit -m "base line"
# Install sglang
-RUN git clone https://github.com/sgl-project/sglang.git /root/sglang && \
+RUN pip_install() { pip install "$@" || pip install --index-url https://pypi.org/simple "$@"; }; \
+ git clone https://github.com/sgl-project/sglang.git /root/sglang && \
cd /root/sglang && git checkout v0.5.15 && \
mv python/pyproject.toml python/pyproject.toml.backup && \
mv python/pyproject_npu.toml python/pyproject.toml && \
pip install -e "python[srt_npu]" --constraint <(echo "torch==2.9.0") && \
- # patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch
git add . && git commit -m "install info" && \
git fetch && \
git cherry-pick ece02ffc9cc32e94382d4f1b553b2c755f83f722 && \
patch -p1 < /root/Relax/docker/npu_patch/sglang-npu.patch && \
git add . && git commit -m "sglang-npu.patch"
-
-# Install sgl-kernle-npu
+# Install SGLang NPU kernels.
RUN git clone https://github.com/sgl-project/sgl-kernel-npu /root/sgl-kernel-npu && \
cd /root/sgl-kernel-npu && git checkout 2026.7.2 && \
- # Adapt tms for colocate train.
patch -p1 < /root/Relax/docker/npu_patch/sgl-kernel-npu.patch && \
- git add . && git commit -m "sgl-kernel-npu.patch" && \
+ git add . && git commit -m "sgl-kernel-npu.patch" && \
bash build.sh && \
pip install output/*.whl && \
- cd /root
+ cd /root && rm -rf /root/sgl-kernel-npu
# Install AscendC FLA
RUN git clone https://github.com/flashserve/flash-linear-attention-npu.git /root/flash-linear-attention-npu && \
@@ -145,18 +161,15 @@ RUN git clone https://github.com/flashserve/flash-linear-attention-npu.git /root
source /usr/local/Ascend/cann-9.0.0/share/info/ascendnpu-ir/bin/set_env.sh && \
# 一键编译安装脚本,先调用torchnpugen自动接入算子,再运行setup编whl包,最后安装whl包
cd torch_custom/fla_npu && bash build.sh
-
-
-
-# Install Relax
-# git clone https://github.com/redai-infra/Relax.git
-RUN cd /root/Relax && \
- pip install -e .
+FROM train as relax
+WORKDIR /root
-RUN pip install ray==2.55.1 && pip install protobuf==6.33.6
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install -r /tmp/requirements.txt --no-cache-dir && \
+ pip install --index-url https://pypi.org/simple ray==2.55.1 protobuf==6.33.6
-#Clean cache
+# Clean caches.
RUN pip cache purge && \
- rm -rf /tmp/*
+ rm -rf /tmp/*
\ No newline at end of file
diff --git a/docker/npu-training.md b/docker/npu-training.md
index bc4710071..f5012126f 100644
--- a/docker/npu-training.md
+++ b/docker/npu-training.md
@@ -27,7 +27,7 @@
- 资源类型:`Ascend910 Snt9b23`
- 驱动版本:`Software Version 25.5.1`
- 固件版本:`Firmware Version 7.8.0.6.201`
-- 基础镜像:`quay.io/ascend/cann:8.5.1-a3-ubuntu22.04-py3.11`
+- 基础镜像:`quay.io/ascend/cann:9.0.0-a3-ubuntu22.04-py3.11`
### 环境检查
@@ -39,7 +39,35 @@
### 安装方法
-(推荐)基于 Dockerfile 构建镜像:`docker build -f docker/Dockerfile.npu -t npu-2026 .`
+`docker/Dockerfile.npu` 采用多阶段构建:
+
+- `base`:系统依赖 + CANN 工具链层;
+- `train`:安装 torch_npu、MindSpeed、Megatron、sglang-npu 和 sgl-kernel-npu;
+- `relax`:安装 Relax 的 Python 运行时依赖,不复制或安装 Relax 源码。训练任务运行时挂载目标分支代码。
+
+通过 Makefile 一次构建并推送完整 Ascend 镜像,标签形如 `ascend-dev-YYYYMMDD-`:
+
+```bash
+REGISTRY= make docker-ascend
+```
+
+910C DinD 使用传统 builder 时:
+
+```bash
+REGISTRY= ASCEND_DOCKER_BUILDKIT=0 make docker-ascend
+```
+
+可配置变量:`BASE_IMAGE`(默认 `quay.io/ascend/cann:9.0.0-a3-ubuntu22.04-py3.11`)、`SOC_VERSION`(默认 `ascend910_9391`)、`REGISTRY`、`DO_PUSH`(默认 `1`,设为 `0` 时不推送并检查本地镜像)、`ASCEND_DOCKER_BUILDKIT`(默认 `1`)。远端已存在同名镜像时会跳过构建。
+
+(可选,内部 QS 镜像)复用 `ml-engine/tools/relax-ci` 的 `docker/Dockerfile.qs`(纯 Python 依赖,架构无关,ARM64 可直接构建)。由 CI 先 checkout relax-ci,再指向其 `Dockerfile.qs`:
+
+```bash
+# 需先 checkout relax-ci,ASCEND_QS_DOCKERFILE 指向其 docker/Dockerfile.qs
+REGISTRY= \
+ ASCEND_QS_DOCKERFILE=/docker/Dockerfile.qs \
+ ASCEND_QS_BASE_IMAGE=/relax:ascend-dev-YYYYMMDD- \
+ make docker-qs-ascend
+```
## 启动配置
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index c2157f054..29622c534 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -229,7 +229,7 @@ export default defineConfig({
{
text: 'Resources',
items: [
- { text: 'GitHub', link: 'https://github.com/redai-infra/Relax' },
+ { text: 'GitHub', link: 'https://github.com/redai-studio/Relax' },
{ text: 'Paper', link: 'https://arxiv.org/abs/2604.11554' }
]
}
@@ -341,7 +341,7 @@ export default defineConfig({
{
text: '资源',
items: [
- { text: 'GitHub', link: 'https://github.com/redai-infra/Relax' },
+ { text: 'GitHub', link: 'https://github.com/redai-studio/Relax' },
{ text: '论文', link: 'https://arxiv.org/abs/2604.11554' }
]
}
@@ -466,7 +466,7 @@ export default defineConfig({
themeConfig: {
logo: '/rednote-logo.png',
socialLinks: [
- { icon: 'github', link: 'https://github.com/redai-infra/Relax' }
+ { icon: 'github', link: 'https://github.com/redai-studio/Relax' }
],
search: {
provider: 'local'
diff --git a/docs/.vitepress/theme/CallToAction.vue b/docs/.vitepress/theme/CallToAction.vue
index ed957b6d7..674f503e5 100644
--- a/docs/.vitepress/theme/CallToAction.vue
+++ b/docs/.vitepress/theme/CallToAction.vue
@@ -26,8 +26,8 @@ const subtitle = computed(() =>
const primaryLabel = computed(() => isZh.value ? '参与贡献' : 'Contribute Now')
const secondaryLabel = computed(() => isZh.value ? '讨论区' : 'Discussion')
-const primaryLink = 'https://github.com/redai-infra/Relax/blob/main/CONTRIBUTING.md'
-const secondaryLink = 'https://github.com/redai-infra/Relax/discussions'
+const primaryLink = 'https://github.com/redai-studio/Relax/blob/main/CONTRIBUTING.md'
+const secondaryLink = 'https://github.com/redai-studio/Relax/discussions'
diff --git a/docs/deploy-docs.sh b/docs/deploy-docs.sh
index 2a1b64c5b..ef470bde2 100755
--- a/docs/deploy-docs.sh
+++ b/docs/deploy-docs.sh
@@ -41,7 +41,7 @@ if [ "$1" == "github" ]; then
fi
# Push to GitHub Pages
- git push -f git@github.com:redai-infra/Relax.git gh-pages
+ git push -f git@github.com:redai-studio/Relax.git gh-pages
echo "✅ Deployed to GitHub Pages!"
diff --git a/docs/en/api/actor-fwd.md b/docs/en/api/actor-fwd.md
index b5ac42472..fa8bb38e3 100644
--- a/docs/en/api/actor-fwd.md
+++ b/docs/en/api/actor-fwd.md
@@ -37,5 +37,5 @@ The ActorFwd runs a background loop that:
## Source
-- Implementation: [`relax/components/actor_fwd.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/actor_fwd.py)
-- Base class: [`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- Implementation: [`relax/components/actor_fwd.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/actor_fwd.py)
+- Base class: [`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/en/api/actor.md b/docs/en/api/actor.md
index 384fbabdc..db079ddae 100644
--- a/docs/en/api/actor.md
+++ b/docs/en/api/actor.md
@@ -34,5 +34,5 @@ The Actor runs a background training loop that:
## Source
-- Implementation: [`relax/components/actor.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/actor.py)
-- Base class: [`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- Implementation: [`relax/components/actor.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/actor.py)
+- Base class: [`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/en/api/genrm.md b/docs/en/api/genrm.md
index 715cd3fea..da5016d06 100644
--- a/docs/en/api/genrm.md
+++ b/docs/en/api/genrm.md
@@ -45,5 +45,5 @@ See [GenRM example](/en/examples/generative-reward-model) for full configuration
## Source
-- Implementation: [`relax/components/genrm.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/genrm.py)
-- Base class: [`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- Implementation: [`relax/components/genrm.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/genrm.py)
+- Base class: [`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/en/api/rollout.md b/docs/en/api/rollout.md
index 27cd2dc8f..c89af7ce7 100644
--- a/docs/en/api/rollout.md
+++ b/docs/en/api/rollout.md
@@ -39,5 +39,5 @@ In fully-async mode, the Rollout service coordinates with the Actor for weight u
## Source
-- Implementation: [`relax/components/rollout.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/rollout.py)
-- Base class: [`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- Implementation: [`relax/components/rollout.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/rollout.py)
+- Base class: [`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/en/guide/configuration.md b/docs/en/guide/configuration.md
index f98a14739..a369eaf22 100644
--- a/docs/en/guide/configuration.md
+++ b/docs/en/guide/configuration.md
@@ -519,7 +519,7 @@ SFT also uses the general dataset flags from [Data Configuration](#data-configur
|-----------|------|---------|-------------|
| `--autoscaler-config` | str | None | Path to autoscaler YAML configuration file. Enables autoscaling when set, disabled when not set. Example: `--autoscaler-config relax/utils/autoscaler/autoscaler.yaml` |
-For autoscaler YAML configuration details, see [`relax/utils/autoscaler/autoscaler.yaml`](https://github.com/redai-infra/Relax/blob/main/relax/utils/autoscaler/autoscaler.yaml).
+For autoscaler YAML configuration details, see [`relax/utils/autoscaler/autoscaler.yaml`](https://github.com/redai-studio/Relax/blob/main/relax/utils/autoscaler/autoscaler.yaml).
### Scale-Out Operation Parameters
diff --git a/docs/en/guide/customize-training.md b/docs/en/guide/customize-training.md
index da0627e50..87733ade7 100644
--- a/docs/en/guide/customize-training.md
+++ b/docs/en/guide/customize-training.md
@@ -460,6 +460,6 @@ bash scripts/entrypoint/ray-job.sh scripts/training/multimodal/run-qwen35-9B-8xg
## Getting Help
-- [GitHub Issues](https://github.com/redai-infra/Relax/issues)
-- [Discussions](https://github.com/redai-infra/Relax/discussions)
+- [GitHub Issues](https://github.com/redai-studio/Relax/issues)
+- [Discussions](https://github.com/redai-studio/Relax/discussions)
- [Introduction](../guide/introduction.md)
diff --git a/docs/en/guide/fully-async-training.md b/docs/en/guide/fully-async-training.md
index eb1282ed3..fe24bc9f9 100644
--- a/docs/en/guide/fully-async-training.md
+++ b/docs/en/guide/fully-async-training.md
@@ -156,7 +156,7 @@ In Fully Async mode, Actor uses `StreamingDataLoader` for **streaming data consu
#### StreamingDataset
```python
-# TransferQueue (installed from https://github.com/redai-infra/TransferQueue)
+# TransferQueue (installed from https://github.com/redai-studio/TransferQueue)
class StreamingDataset(IterableDataset):
"""Streaming dataset that dynamically fetches data from TransferQueue"""
diff --git a/docs/en/guide/how-to-contribute.md b/docs/en/guide/how-to-contribute.md
index dcfe7c535..040881b6a 100644
--- a/docs/en/guide/how-to-contribute.md
+++ b/docs/en/guide/how-to-contribute.md
@@ -10,7 +10,7 @@ Create a virtual environment and install dependencies:
```bash
# Clone the repository
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
cd Relax
# Create virtual environment
diff --git a/docs/en/guide/installation.md b/docs/en/guide/installation.md
index 78db84925..81b07b2bd 100644
--- a/docs/en/guide/installation.md
+++ b/docs/en/guide/installation.md
@@ -21,13 +21,13 @@ Run the following commands to clone the repository, pull the latest image, and s
```bash
# Clone the repository
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
# Pull the Docker image
-docker pull ghcr.io/redai-infra/relaxrl:latest
+docker pull ghcr.io/redai-studio/relaxrl:latest
# Run the container, mounting the local repository to /root/Relax inside the container
-docker run -it --gpus all -v $(pwd)/Relax:/root/Relax ghcr.io/redai-infra/relaxrl:latest /bin/bash
+docker run -it --gpus all -v $(pwd)/Relax:/root/Relax ghcr.io/redai-studio/relaxrl:latest /bin/bash
```
Alternatively, build the image from the Dockerfile:
@@ -57,13 +57,13 @@ DOCKER_BUILDKIT=1 docker build \
.
```
-For more details on Docker releases, see [Docker README](https://github.com/redai-infra/Relax/blob/main/docker/README.md).
+For more details on Docker releases, see [Docker README](https://github.com/redai-studio/Relax/blob/main/docker/README.md).
### Method 2: Install from Source
```bash
# Clone the repository
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
cd Relax
# Install dependencies
@@ -87,7 +87,7 @@ export MEGATRON="your megatron path"
export PYTHONPATH=your_megatron_path:$PYTHONPATH
```
-Additionally, Relax depends on [Megatron Bridge](https://github.com/NVIDIA-NeMo/Megatron-Bridge) for weight conversion. Follow the install steps in [`docker/Dockerfile`](https://github.com/redai-infra/Relax/blob/main/docker/Dockerfile): merge the Bridge sources with the Megatron-LM submodule into a single directory and add it to `PYTHONPATH`:
+Additionally, Relax depends on [Megatron Bridge](https://github.com/NVIDIA-NeMo/Megatron-Bridge) for weight conversion. Follow the install steps in [`docker/Dockerfile`](https://github.com/redai-studio/Relax/blob/main/docker/Dockerfile): merge the Bridge sources with the Megatron-LM submodule into a single directory and add it to `PYTHONPATH`:
```bash
export MEGATRON_BRIDGE_COMMIT=2faedbf6fe3c422835a44b2b360cadcb2a116a54
diff --git a/docs/en/guide/introduction.md b/docs/en/guide/introduction.md
index b0b6438ed..8602a0e46 100644
--- a/docs/en/guide/introduction.md
+++ b/docs/en/guide/introduction.md
@@ -2,7 +2,7 @@
## What is Relax?
-**Relax** (**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality) is a high-performance reinforcement learning post-training framework for multimodal large language models. Built on Ray Serve with a service-oriented architecture, Relax uses Megatron-LM as the training backend and SGLang as the inference engine. Through the [TransferQueue](https://github.com/redai-infra/TransferQueue) data transfer system, it achieves complete decoupling of training and inference, supporting end-to-end multimodal RL training from text to images, videos, and audio.
+**Relax** (**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality) is a high-performance reinforcement learning post-training framework for multimodal large language models. Built on Ray Serve with a service-oriented architecture, Relax uses Megatron-LM as the training backend and SGLang as the inference engine. Through the [TransferQueue](https://github.com/redai-studio/TransferQueue) data transfer system, it achieves complete decoupling of training and inference, supporting end-to-end multimodal RL training from text to images, videos, and audio.
---
@@ -26,7 +26,7 @@ Relax adopts a service-oriented six-layer architecture where all components are
### ⚡ Fully Asynchronous Training via TransferQueue
-Open source at [TransferQueue](https://github.com/redai-infra/TransferQueue). See [Fully Asynchronous Training](./fully-async-training.md) for details.
+Open source at [TransferQueue](https://github.com/redai-studio/TransferQueue). See [Fully Asynchronous Training](./fully-async-training.md) for details.
In fully async mode, five roles—Rollout (inference), Actor (training), ActorFwd (forward pass), Reference (reference model), and Advantages (advantage computation)—run on **independent GPU clusters** and exchange data via TransferQueue, with weights synchronized asynchronously through DCS (Distributed Checkpoint Service).
diff --git a/docs/en/guide/reinforce-plus-plus-training-report.md b/docs/en/guide/reinforce-plus-plus-training-report.md
index bfbb72671..034299a1d 100644
--- a/docs/en/guide/reinforce-plus-plus-training-report.md
+++ b/docs/en/guide/reinforce-plus-plus-training-report.md
@@ -8,7 +8,7 @@ a claim that one algorithm is statistically superior.
## Scope and evidence boundary
-- Proposal: [Task 29 issue #192](https://github.com/redai-infra/Relax/issues/192)
+- Proposal: [Task 29 issue #192](https://github.com/redai-studio/Relax/issues/192)
- Sanitized reproducibility evidence:
[logs, expanded commands, metrics and manifest](https://github.com/zheself/Relax/releases/tag/task29-reinforcepp-evidence-c72caf1)
- Experiment source commit: `5f7cd574372288391bb1c41ca0677422cd31e725`
diff --git a/docs/en/index.md b/docs/en/index.md
index 43d99d7c8..68c8c0884 100644
--- a/docs/en/index.md
+++ b/docs/en/index.md
@@ -9,7 +9,7 @@ hero:
link: /en/guide/introduction
- theme: alt
text: View on GitHub
- link: https://github.com/redai-infra/Relax
+ link: https://github.com/redai-studio/Relax
features:
- icon: ''
diff --git a/docs/zh/api/actor-fwd.md b/docs/zh/api/actor-fwd.md
index d85628983..ff5f33b56 100644
--- a/docs/zh/api/actor-fwd.md
+++ b/docs/zh/api/actor-fwd.md
@@ -37,5 +37,5 @@ ActorFwd 运行后台循环:
## 源码
-- 实现:[`relax/components/actor_fwd.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/actor_fwd.py)
-- 基类:[`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- 实现:[`relax/components/actor_fwd.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/actor_fwd.py)
+- 基类:[`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/zh/api/actor.md b/docs/zh/api/actor.md
index e0f7406f0..8e731435c 100644
--- a/docs/zh/api/actor.md
+++ b/docs/zh/api/actor.md
@@ -34,5 +34,5 @@ Actor 运行后台训练循环:
## 源码
-- 实现:[`relax/components/actor.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/actor.py)
-- 基类:[`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- 实现:[`relax/components/actor.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/actor.py)
+- 基类:[`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/zh/api/genrm.md b/docs/zh/api/genrm.md
index b28a542b5..d13b3198a 100644
--- a/docs/zh/api/genrm.md
+++ b/docs/zh/api/genrm.md
@@ -45,5 +45,5 @@ GenRM(生成式奖励模型)服务提供基于 LLM 的响应评估。它以
## 源码
-- 实现:[`relax/components/genrm.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/genrm.py)
-- 基类:[`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- 实现:[`relax/components/genrm.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/genrm.py)
+- 基类:[`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/zh/api/rollout.md b/docs/zh/api/rollout.md
index 9e253bcfd..a50817d64 100644
--- a/docs/zh/api/rollout.md
+++ b/docs/zh/api/rollout.md
@@ -39,5 +39,5 @@ Rollout 运行后台循环:
## 源码
-- 实现:[`relax/components/rollout.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/rollout.py)
-- 基类:[`relax/components/base.py`](https://github.com/redai-infra/Relax/blob/main/relax/components/base.py)
+- 实现:[`relax/components/rollout.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/rollout.py)
+- 基类:[`relax/components/base.py`](https://github.com/redai-studio/Relax/blob/main/relax/components/base.py)
diff --git a/docs/zh/guide/configuration.md b/docs/zh/guide/configuration.md
index 9da9012ac..ec6b15800 100644
--- a/docs/zh/guide/configuration.md
+++ b/docs/zh/guide/configuration.md
@@ -519,7 +519,7 @@ SFT 还会用到通用的[数据配置](#数据配置)参数,特别是 `--inpu
|------|------|--------|------|
| `--autoscaler-config` | str | None | Autoscaler YAML 配置文件路径。设置后启用自动扩缩容,未设置则禁用。示例:`--autoscaler-config relax/utils/autoscaler/autoscaler.yaml` |
-Autoscaler YAML 配置详情请参见 [`relax/utils/autoscaler/autoscaler.yaml`](https://github.com/redai-infra/Relax/blob/main/relax/utils/autoscaler/autoscaler.yaml)。
+Autoscaler YAML 配置详情请参见 [`relax/utils/autoscaler/autoscaler.yaml`](https://github.com/redai-studio/Relax/blob/main/relax/utils/autoscaler/autoscaler.yaml)。
### Scale-Out 操作参数
diff --git a/docs/zh/guide/customize-training.md b/docs/zh/guide/customize-training.md
index bfa86f3da..d7dc0b19d 100644
--- a/docs/zh/guide/customize-training.md
+++ b/docs/zh/guide/customize-training.md
@@ -489,6 +489,6 @@ tail -f /tmp/ray/session_latest/logs/serve/*.log
## 获取帮助
-- [GitHub Issues](https://github.com/redai-infra/Relax/issues)
-- [Discussions](https://github.com/redai-infra/Relax/discussions)
+- [GitHub Issues](https://github.com/redai-studio/Relax/issues)
+- [Discussions](https://github.com/redai-studio/Relax/discussions)
- [介绍](../guide/introduction.md)
diff --git a/docs/zh/guide/fully-async-training.md b/docs/zh/guide/fully-async-training.md
index b33535f89..c19116749 100644
--- a/docs/zh/guide/fully-async-training.md
+++ b/docs/zh/guide/fully-async-training.md
@@ -156,7 +156,7 @@ TransferQueue 必须能同时缓存 `max_staleness + 1` 个 rollout batch 的数
#### StreamingDataset
```python
-# TransferQueue (installed from https://github.com/redai-infra/TransferQueue)
+# TransferQueue (installed from https://github.com/redai-studio/TransferQueue)
class StreamingDataset(IterableDataset):
"""流式数据集,从 TransferQueue 动态获取数据"""
diff --git a/docs/zh/guide/how-to-contribute.md b/docs/zh/guide/how-to-contribute.md
index b50885108..dca0c31eb 100644
--- a/docs/zh/guide/how-to-contribute.md
+++ b/docs/zh/guide/how-to-contribute.md
@@ -10,7 +10,7 @@
```bash
# 克隆仓库
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
cd Relax
# 创建虚拟环境
diff --git a/docs/zh/guide/installation.md b/docs/zh/guide/installation.md
index c23920cd0..72b8d4a18 100644
--- a/docs/zh/guide/installation.md
+++ b/docs/zh/guide/installation.md
@@ -21,13 +21,13 @@
```bash
# 克隆代码仓库
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
# 拉取 Docker 镜像
-docker pull ghcr.io/redai-infra/relaxrl:latest
+docker pull ghcr.io/redai-studio/relaxrl:latest
# 运行容器,将本地代码仓库挂载到容器内的 /root/Relax
-docker run -it --gpus all -v $(pwd)/Relax:/root/Relax ghcr.io/redai-infra/relaxrl:latest /bin/bash
+docker run -it --gpus all -v $(pwd)/Relax:/root/Relax ghcr.io/redai-studio/relaxrl:latest /bin/bash
```
或者基于 Dockerfile 构建镜像:
@@ -57,13 +57,13 @@ DOCKER_BUILDKIT=1 docker build \
.
```
-更多 Docker 发布信息请参见 [Docker README](https://github.com/redai-infra/Relax/blob/main/docker/README.md)。
+更多 Docker 发布信息请参见 [Docker README](https://github.com/redai-studio/Relax/blob/main/docker/README.md)。
### 方法 2:从源码安装
```bash
# 克隆仓库
-git clone https://github.com/redai-infra/Relax.git
+git clone https://github.com/redai-studio/Relax.git
cd Relax
# 安装依赖
@@ -87,7 +87,7 @@ export MEGATRON="your megatron path"
export PYTHONPATH=your_megatron_path:$PYTHONPATH
```
-此外 Relax 依赖 [Megatron Bridge](https://github.com/NVIDIA-NeMo/Megatron-Bridge) 进行权重转换。安装方式参考 [`docker/Dockerfile`](https://github.com/redai-infra/Relax/blob/main/docker/Dockerfile),将 Bridge 源码与 Megatron-LM submodule 合并到同一目录后加入 `PYTHONPATH`:
+此外 Relax 依赖 [Megatron Bridge](https://github.com/NVIDIA-NeMo/Megatron-Bridge) 进行权重转换。安装方式参考 [`docker/Dockerfile`](https://github.com/redai-studio/Relax/blob/main/docker/Dockerfile),将 Bridge 源码与 Megatron-LM submodule 合并到同一目录后加入 `PYTHONPATH`:
```bash
export MEGATRON_BRIDGE_COMMIT=2faedbf6fe3c422835a44b2b360cadcb2a116a54
diff --git a/docs/zh/guide/introduction.md b/docs/zh/guide/introduction.md
index ca24007b0..48a39ba86 100644
--- a/docs/zh/guide/introduction.md
+++ b/docs/zh/guide/introduction.md
@@ -2,7 +2,7 @@
## 什么是 Relax?
-**Relax**(**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality)是一个面向多模态大模型的高性能强化学习后训练框架。Relax 基于 Ray Serve 构建面向服务的架构,以 Megatron-LM 为训练后端、SGLang 为推理引擎,通过 [TransferQueue](https://github.com/redai-infra/TransferQueue) 数据传输系统实现训练与推理的完全解耦,支持从文本到图像、视频、音频的全模态强化学习训练。
+**Relax**(**R**einforcement **E**ngine **L**everaging **A**gentic **X**-modality)是一个面向多模态大模型的高性能强化学习后训练框架。Relax 基于 Ray Serve 构建面向服务的架构,以 Megatron-LM 为训练后端、SGLang 为推理引擎,通过 [TransferQueue](https://github.com/redai-studio/TransferQueue) 数据传输系统实现训练与推理的完全解耦,支持从文本到图像、视频、音频的全模态强化学习训练。
---
@@ -26,7 +26,7 @@ Relax 采用面向服务的六层架构设计,所有角色均部署为独立
### ⚡ 基于 TransferQueue 的全异步训练(Fully Async)
-开源地址 [TransferQueue](https://github.com/redai-infra/TransferQueue),详细介绍见 [全异步训练](./fully-async-training.md)。
+开源地址 [TransferQueue](https://github.com/redai-studio/TransferQueue),详细介绍见 [全异步训练](./fully-async-training.md)。
在全异步模式下,Rollout(推理)、Actor(训练)、ActorFwd(前向计算)、Reference(参考模型)和 Advantages(优势计算)五个角色运行在**独立的 GPU 集群**上,通过 TransferQueue 交换数据,通过 DCS(Distributed Checkpoint Service)异步同步权重。
diff --git a/docs/zh/guide/reinforce-plus-plus-training-report.md b/docs/zh/guide/reinforce-plus-plus-training-report.md
index 01975d395..66a0e5507 100644
--- a/docs/zh/guide/reinforce-plus-plus-training-report.md
+++ b/docs/zh/guide/reinforce-plus-plus-training-report.md
@@ -4,7 +4,7 @@
## 范围与证据边界
-- Proposal:[Task 29 issue #192](https://github.com/redai-infra/Relax/issues/192)
+- Proposal:[Task 29 issue #192](https://github.com/redai-studio/Relax/issues/192)
- 脱敏的可复现证据:[日志、展开命令、指标和 manifest](https://github.com/zheself/Relax/releases/tag/task29-reinforcepp-evidence-c72caf1)
- 实验源码 commit:`5f7cd574372288391bb1c41ca0677422cd31e725`
- 实验 upstream base:`b095ba68ce95c7d98762cf128eab630878f394e6`
diff --git a/docs/zh/index.md b/docs/zh/index.md
index fac89faa9..90da239ae 100644
--- a/docs/zh/index.md
+++ b/docs/zh/index.md
@@ -9,7 +9,7 @@ hero:
link: /zh/guide/introduction
- theme: alt
text: 在 GitHub 上查看
- link: https://github.com/redai-infra/Relax
+ link: https://github.com/redai-studio/Relax
features:
- icon: ''
diff --git a/examples/nemo_gym_agentic/README.md b/examples/nemo_gym_agentic/README.md
index d06697d22..6b938e7f1 100644
--- a/examples/nemo_gym_agentic/README.md
+++ b/examples/nemo_gym_agentic/README.md
@@ -157,7 +157,7 @@ callback 可能被错误发送到代理。
需要两个运行角色:
-1. `RELAX_IMAGE`:公开的标准 Relax 训练镜像 `ghcr.io/redai-infra/relaxrl:latest`,运行 GPU Ray
+1. `RELAX_IMAGE`:公开的标准 Relax 训练镜像 `ghcr.io/redai-studio/relaxrl:latest`,运行 GPU Ray
cluster 和训练任务;
2. `NEMO_GYM_IMAGE`:由本目录 Dockerfile 基于 `RELAX_IMAGE` 构建,运行 NeMo Gym 服务。
@@ -169,7 +169,7 @@ venv。
### 构建 NeMo Gym 镜像
```bash
-export RELAX_IMAGE="ghcr.io/redai-infra/relaxrl:latest"
+export RELAX_IMAGE="ghcr.io/redai-studio/relaxrl:latest"
export NEMO_GYM_IMAGE="relax-nemo-gym:a85670e"
export http_proxy="http://proxy.example.com:3128" # 无代理时留空
export https_proxy="${http_proxy}"
diff --git a/examples/nemo_gym_agentic/recipes/gsm8k/README.md b/examples/nemo_gym_agentic/recipes/gsm8k/README.md
index c3d4bf0fb..a6bea56ad 100644
--- a/examples/nemo_gym_agentic/recipes/gsm8k/README.md
+++ b/examples/nemo_gym_agentic/recipes/gsm8k/README.md
@@ -48,7 +48,7 @@ Relax managed session
```bash
export REPO_ROOT="$(pwd)"
-export RELAX_IMAGE="ghcr.io/redai-infra/relaxrl:latest"
+export RELAX_IMAGE="ghcr.io/redai-studio/relaxrl:latest"
export NEMO_GYM_IMAGE="relax-nemo-gym:a85670e"
export DATA_ROOT="/绝对路径/relax-nemo-data"
export MODEL_DIR="/绝对路径/models"
@@ -79,7 +79,7 @@ DOCKER_BUILDKIT=1 docker build \
.
```
-Dockerfile 默认基于 `ghcr.io/redai-infra/relaxrl:latest`。使用其他已有 Relax tag 时,给上述命令
+Dockerfile 默认基于 `ghcr.io/redai-studio/relaxrl:latest`。使用其他已有 Relax tag 时,给上述命令
增加 `--build-arg RELAX_IMAGE=""`;不需要构建 Relax 镜像。
检查镜像:
diff --git a/examples/nemo_gym_agentic/recipes/r2e-gym/README.md b/examples/nemo_gym_agentic/recipes/r2e-gym/README.md
index 55c29e785..db1653a92 100644
--- a/examples/nemo_gym_agentic/recipes/r2e-gym/README.md
+++ b/examples/nemo_gym_agentic/recipes/r2e-gym/README.md
@@ -170,7 +170,7 @@ NeMo Gym 服务本身不需要 GPU。GPU 只由 Relax 模型训练使用。
```bash
export REPO_ROOT="$(pwd)"
-export RELAX_IMAGE="ghcr.io/redai-infra/relaxrl:latest"
+export RELAX_IMAGE="ghcr.io/redai-studio/relaxrl:latest"
export NEMO_GYM_IMAGE="relax-nemo-gym:a85670e"
export R2E_DATA_DIR="/绝对路径/nemo-gym/r2e-gym"
export MODEL_DIR="/绝对路径/models"
@@ -206,7 +206,7 @@ DOCKER_BUILDKIT=1 docker build \
.
```
-Dockerfile 默认基于 `ghcr.io/redai-infra/relaxrl:latest`。使用其他已有 Relax tag 时,给上述命令
+Dockerfile 默认基于 `ghcr.io/redai-studio/relaxrl:latest`。使用其他已有 Relax tag 时,给上述命令
增加 `--build-arg RELAX_IMAGE=""`;不需要构建 Relax 镜像。
该镜像固定并 patch:
diff --git a/examples/nemo_gym_agentic/recipes/workplace-assistant/README.md b/examples/nemo_gym_agentic/recipes/workplace-assistant/README.md
index 90b2b8b6a..f23748e83 100644
--- a/examples/nemo_gym_agentic/recipes/workplace-assistant/README.md
+++ b/examples/nemo_gym_agentic/recipes/workplace-assistant/README.md
@@ -61,7 +61,7 @@ resource server 为每条请求创建独立数据库会话。集成 patch 维护
```bash
export REPO_ROOT="$(pwd)"
-export RELAX_IMAGE="ghcr.io/redai-infra/relaxrl:latest"
+export RELAX_IMAGE="ghcr.io/redai-studio/relaxrl:latest"
export NEMO_GYM_IMAGE="relax-nemo-gym:a85670e"
export DATA_ROOT="/绝对路径/relax-nemo-data"
export MODEL_DIR="/绝对路径/models"
@@ -90,7 +90,7 @@ DOCKER_BUILDKIT=1 docker build \
.
```
-Dockerfile 默认基于 `ghcr.io/redai-infra/relaxrl:latest`。使用其他已有 Relax tag 时,给上述命令
+Dockerfile 默认基于 `ghcr.io/redai-studio/relaxrl:latest`。使用其他已有 Relax tag 时,给上述命令
增加 `--build-arg RELAX_IMAGE=""`;不需要构建 Relax 镜像。
镜像在构建阶段预建 Gateway、simple agent 和 Workplace resource venv,并应用 session cleanup
diff --git a/examples/nemo_gym_agentic/service/Dockerfile b/examples/nemo_gym_agentic/service/Dockerfile
index 0964f7b9a..7bb00b8b2 100644
--- a/examples/nemo_gym_agentic/service/Dockerfile
+++ b/examples/nemo_gym_agentic/service/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-ARG RELAX_IMAGE=ghcr.io/redai-infra/relaxrl:latest
+ARG RELAX_IMAGE=ghcr.io/redai-studio/relaxrl:latest
FROM ${RELAX_IMAGE}
ARG NEMO_GYM_COMMIT=a85670eb167ba9b48cc53a36a070eed815e6c40d
diff --git a/relax/backends/megatron/model_provider.py b/relax/backends/megatron/model_provider.py
index 7e9805608..795261e51 100644
--- a/relax/backends/megatron/model_provider.py
+++ b/relax/backends/megatron/model_provider.py
@@ -243,7 +243,7 @@ def wrapped_model_provider(
"freeze_vision_projection",
"freeze_audio_model",
"freeze_audio_projection",
- # https://github.com/redai-infra/Megatron-Bridge/commit/960bb5f18800d3e1fb9815e95daa185ab06c09ea
+ # https://github.com/redai-studio/Megatron-Bridge/commit/960bb5f18800d3e1fb9815e95daa185ab06c09ea
"vision_dp_when_tp",
"vision_dp_when_cp",
"calculate_per_token_loss",
diff --git a/relax/utils/arguments.py b/relax/utils/arguments.py
index f7d1e710e..7984bfab1 100644
--- a/relax/utils/arguments.py
+++ b/relax/utils/arguments.py
@@ -36,7 +36,7 @@
# Minimum required TransferQueue version and the command to upgrade to it.
_MIN_TQ_VERSION = "0.1.10.dev0"
_TQ_UPGRADE_CMD = (
- 'pip install "transferqueue @ git+https://github.com/redai-infra/'
+ 'pip install "transferqueue @ git+https://github.com/redai-studio/'
'TransferQueue.git@58054a33834aadbcf76aacd6b1e32e25c030f2c9" --no-deps'
)
diff --git a/relax/utils/visualize/templates.py b/relax/utils/visualize/templates.py
index 275b97b65..d874965a8 100644
--- a/relax/utils/visualize/templates.py
+++ b/relax/utils/visualize/templates.py
@@ -1802,7 +1802,7 @@ def get_jsonl_viewer_html(data_dir: str, base_path: str = "") -> str:
📊
- Relax
Rollout Result Viewer
@@ -1810,7 +1810,7 @@ def get_jsonl_viewer_html(data_dir: str, base_path: str = "") -> str:
-