arch(CRITICAL): replace custom qwen3_5.py with base original (1369 lines)
CCCL tuning_rle_encode.cuh AST chain led to reading the base engine zip: enginex-vllm-bi100-qwen36-main.zip → qwen3_6_scripts/qwen3_5.py (63KB, 1369 lines) vs our custom version (85KB, 1780 lines) which added: - _hw_policy with hardcoded clamp values - nan_to_num(nan=0.0) double disaster - Custom _torch_chunk_gated_delta_rule with aggressive clamps - Custom FusedMoE fallback logic - All of which BROKE the native CoreX acceleration Sub168 docker log proves: - corex_gdn.py:56 loads libcorex_gdn.so (fused GDN decode) - corex_gdn.py:228 uses fused GDN prefill - corex_moe.py:339 uses CoreX fused MoE (expert-grouped-wmma) These are Docker image-internal modules that our custom code never called. Base original: - No nan_to_num (NaN propagates honestly) - No custom clamps (uses model weights as-is) - Same class structure (Qwen3_5ForCausalLM, Qwen3_5MoeForCausalLM) - Docker image's corex modules can intercept through vllm's internal dispatch qwen3_5_base_original.py kept as reference.
This commit is contained in:
@@ -1,12 +1,6 @@
|
||||
# Inference-only Qwen3.6-27B (Qwen3_5 architecture) for Iluvatar BI-V100.
|
||||
#
|
||||
# dispatch_segmented_sort.cuh three-way dispatch pattern:
|
||||
# 1. Try base image's native CoreX-accelerated qwen3_5 (corex_gdn + corex_moe)
|
||||
# 2. Fallback to pure-PyTorch implementation (this file)
|
||||
#
|
||||
# Sub168 (92.3% pass rate) used the native CoreX path with zero NaN.
|
||||
# Our pure-PyTorch fallback may produce NaN in GatedDeltaNet layers.
|
||||
# At module bottom, we check for native availability and re-export if found.
|
||||
# Pure-PyTorch DeltaNet (no fla / causal_conv1d dependency).
|
||||
# Text-only (no VL, no MTP).
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
@@ -48,123 +42,6 @@ from vllm.model_executor.models.interfaces import HasInnerState, SupportsLoRA
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hardware-aware policy dispatch (translated from CCCL cc_dispatch.cuh)
|
||||
#
|
||||
# cc_dispatch.cuh's design:
|
||||
# 1. Runtime: detect device compute capability
|
||||
# 2. policy_selector(cc) → returns kernel config (threads, items, algorithm)
|
||||
# 3. lowest_cc_resolver: merge CCs with identical policies → fewer instantiations
|
||||
# 4. dispatch_compute_cap: bridge runtime detection → compile-time specialization
|
||||
#
|
||||
# Translation to Python/PyTorch:
|
||||
# 1. Runtime: detect BI-V100 capabilities (SMEM, cuSOLVER, MoE kernels)
|
||||
# 2. _hw_policy → returns DeltaNet chunk_size, MoE strategy, solve method
|
||||
# 3. Capabilities detected once at module load, cached globally
|
||||
# 4. All kernel code reads from _hw_policy instead of hardcoded constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _HardwarePolicy:
|
||||
"""CCCL cc_dispatch equivalent: detect hardware once, select policies."""
|
||||
|
||||
def __init__(self):
|
||||
self._detected = False
|
||||
# Defaults (safe for any hardware)
|
||||
self.deltanet_chunk_size = 64
|
||||
self.deltanet_prefill_chunk = 4096
|
||||
self.solve_triangular_available = False
|
||||
self.moe_native_topk = False
|
||||
self.moe_native_align = False
|
||||
self.moe_native_invoke = False
|
||||
self.smem_bytes = 49152 # 48KB default for BI-V100
|
||||
|
||||
def detect(self, device: torch.device = None):
|
||||
"""Run once to probe hardware capabilities. CCCL: policy_selector(cc)."""
|
||||
if self._detected:
|
||||
return
|
||||
self._detected = True
|
||||
|
||||
if device is None:
|
||||
if not torch.cuda.is_available():
|
||||
return
|
||||
device = torch.device("cuda:0")
|
||||
|
||||
# Probe SMEM (CCCL: compute_capability → SMEM size)
|
||||
try:
|
||||
idx = device.index if device.index is not None else 0
|
||||
props = torch.cuda.get_device_properties(idx)
|
||||
self.smem_bytes = props.total_memory # not SMEM, but available
|
||||
# BI-V100: 48KB confirmed via ixsmi
|
||||
self.smem_bytes = 49152
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Probe cuSOLVER/cuBLAS trsm (CCCL: check if kernel exists for this CC)
|
||||
try:
|
||||
test_A = torch.eye(4, device=device, dtype=torch.float32)
|
||||
test_b = torch.ones(4, 2, device=device, dtype=torch.float32)
|
||||
torch.linalg.solve_triangular(test_A, test_b, upper=False)
|
||||
self.solve_triangular_available = True
|
||||
except RuntimeError:
|
||||
self.solve_triangular_available = False
|
||||
|
||||
# tuning_transform_tile.cuh pick_tile_size translation:
|
||||
# Derive DeltaNet chunk_size from hardware params, not hardcode.
|
||||
#
|
||||
# CCCL formula:
|
||||
# items_for_vec = ceil(vector_bytes / min_elem_size)
|
||||
# items_for_latency = target_bytes_in_flight / (occupancy × threads × bytes_per_iter)
|
||||
# tile_size = max(items_for_vec, items_for_latency), rounded to power of 2
|
||||
#
|
||||
# For DeltaNet: chunk_size controls the (C×C) matrix in _forward_sub_lower.
|
||||
# Memory per chunk ≈ 2 × C² × sizeof(float32) × batch × heads (decay_mask + A matrix)
|
||||
# On BI-V100 with 48KB SMEM (not directly usable from PyTorch but indicates
|
||||
# hardware tier), and ~16GB GPU memory for KV cache + model:
|
||||
#
|
||||
# solve_triangular path: one cuBLAS call per chunk, larger = fewer calls
|
||||
# Python loop path: C iterations per chunk, smaller = fewer iterations
|
||||
if self.solve_triangular_available:
|
||||
# cuBLAS trsm: larger chunk = amortize kernel launch overhead
|
||||
self.deltanet_chunk_size = 64
|
||||
else:
|
||||
# Python forward substitution fallback: each chunk costs C iterations.
|
||||
# CCCL block_reduce_warp_reductions: when sequential path dominates,
|
||||
# reduce per-unit work (fewer iterations) even at cost of more units
|
||||
# (more chunks). 16 iterations × more chunks beats 32 iterations × fewer.
|
||||
self.deltanet_chunk_size = 16
|
||||
|
||||
# Prefill sub-chunk: controls peak memory per DeltaNet forward call.
|
||||
# CCCL target = cc_to_min_bytes_in_flight(cc): BI-V100 ≈ lower tier.
|
||||
# Qwen3.5 DeltaNet state: (B, heads, k_dim, v_dim) ≈ (1,6,64,64)×4B = 96KB/layer
|
||||
# With _DNN_CHUNK=4096 tokens: working memory ≈ 4096×hidden×4B ≈ 60MB
|
||||
# With _DNN_CHUNK=2048: ≈ 30MB — leaves more room for KV cache
|
||||
# BI-V100 at 0.95 GPU util with 256K context needs memory headroom
|
||||
self.deltanet_prefill_chunk = 4096
|
||||
|
||||
# Probe MoE native kernels (CCCL: check op availability per CC)
|
||||
try:
|
||||
import ixformer.functions as ixf_F
|
||||
self.moe_native_topk = hasattr(ixf_F, 'vllm_moe_topk_softmax')
|
||||
self.moe_native_align = hasattr(ixf_F, 'vllm_moe_align_block_size')
|
||||
self.moe_native_invoke = hasattr(ixf_F, 'vllm_invoke_fused_moe_kernel')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Log detected policy (CCCL: policy is logged/printed for debugging)
|
||||
logger.info(
|
||||
"HardwarePolicy detected: chunk=%d solve_tri=%s "
|
||||
"moe_native=[topk=%s align=%s invoke=%s]",
|
||||
self.deltanet_chunk_size,
|
||||
self.solve_triangular_available,
|
||||
self.moe_native_topk,
|
||||
self.moe_native_align,
|
||||
self.moe_native_invoke)
|
||||
|
||||
|
||||
# Global singleton (CCCL: policies are constexpr globals)
|
||||
_hw_policy = _HardwarePolicy()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure-PyTorch DeltaNet kernels (fallbacks from transformers 5.2.0)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -197,16 +74,12 @@ def _torch_chunk_gated_delta_rule(
|
||||
value: torch.Tensor, # (batch, seq, num_heads, head_v_dim)
|
||||
g: torch.Tensor, # (batch, seq, num_heads)
|
||||
beta: torch.Tensor, # (batch, seq, num_heads)
|
||||
chunk_size: int = 0, # 0 = use _hw_policy.deltanet_chunk_size
|
||||
chunk_size: int = 64,
|
||||
initial_state: Optional[torch.Tensor] = None,
|
||||
output_final_state: bool = False,
|
||||
use_qk_l2norm_in_kernel: bool = False,
|
||||
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
||||
initial_dtype = query.dtype
|
||||
# cc_dispatch: resolve chunk_size from hardware policy
|
||||
if chunk_size <= 0:
|
||||
_hw_policy.detect(query.device)
|
||||
chunk_size = _hw_policy.deltanet_chunk_size
|
||||
if use_qk_l2norm_in_kernel:
|
||||
query = _l2norm(query)
|
||||
key = _l2norm(key)
|
||||
@@ -238,67 +111,16 @@ def _torch_chunk_gated_delta_rule(
|
||||
torch.ones(chunk_size, chunk_size, dtype=torch.bool, device=query.device),
|
||||
diagonal=0)
|
||||
|
||||
# CCCL overflow_cast_t pattern: clamp BEFORE accumulation, not after.
|
||||
# Without pre-clamp, cumsum of large g values produces huge numbers
|
||||
# that downstream exp() and matmul amplify into NaN.
|
||||
# BI-V100 docker logs show 99.98-100% NaN rate in every GatedDeltaNet layer.
|
||||
#
|
||||
# Pre-clamp: limit each g element so cumsum over chunk_size stays bounded.
|
||||
# With chunk_size=64 and per-element clamp ±0.3, cumsum range ≈ ±19.2.
|
||||
# Post-clamp to ±12 keeps exp(g_diff) ≤ exp(24) ≈ 2.6e10 — safe for
|
||||
# float32 matmul accumulation (k_dim=64 → max product ~1.7e12, within float32).
|
||||
g = g.clamp(-0.5, 0.5)
|
||||
g = g.cumsum(dim=-1)
|
||||
g = g.clamp(-12.0, 12.0)
|
||||
decay_mask = ((g.unsqueeze(-1) - g.unsqueeze(-2)).tril().exp().float()).tril()
|
||||
|
||||
# Lower-triangular solve WITHOUT libcusolver (not available on BI-V100).
|
||||
#
|
||||
# Computes (I - A)^{-1} @ RHS where A is strictly lower-triangular.
|
||||
# A = (k_beta @ key^T) * decay_mask, masked to lower triangle.
|
||||
#
|
||||
# Forward substitution: x[0] = rhs[0]; x[i] = rhs[i] + A[i,:i] @ x[:i]
|
||||
# Vectorized as batched matmul over chunk rows — no Python loop per row.
|
||||
# Uses torch.triangular_solve (LAPACK-based, works without cuSOLVER)
|
||||
# as primary path, with manual row-loop as fallback.
|
||||
A = ((k_beta @ key.transpose(-1, -2)) * decay_mask).masked_fill(mask_upper, 0)
|
||||
|
||||
# For solve: (I-A) @ X = RHS → X = (I-A)^{-1} @ RHS
|
||||
# Since (I-A) is lower-triangular with 1s on diagonal, and A is strictly
|
||||
# lower-triangular, we can use a row-by-row forward substitution.
|
||||
# This avoids cuSOLVER entirely — only needs basic matmul and indexing.
|
||||
|
||||
def _forward_sub_lower(A_lower, rhs):
|
||||
"""Solve (I - A_lower) @ X = RHS.
|
||||
|
||||
cc_dispatch pattern: _hw_policy.solve_triangular_available was probed
|
||||
once at startup. No per-call try/except overhead.
|
||||
"""
|
||||
C = rhs.shape[-2]
|
||||
if _hw_policy.solve_triangular_available:
|
||||
eye = torch.eye(C, dtype=A_lower.dtype, device=A_lower.device)
|
||||
IminusA = eye - A_lower
|
||||
return torch.linalg.solve_triangular(
|
||||
IminusA, rhs, upper=False, unitriangular=True)
|
||||
else:
|
||||
# Python forward substitution fallback with numerical stability.
|
||||
# CCCL overflow_cast pattern: clamp intermediate results per row
|
||||
# to prevent the A @ x accumulation from amplifying small errors
|
||||
# into NaN. Without this, BI-V100 shows 100% NaN in every DeltaNet layer.
|
||||
x = torch.zeros_like(rhs)
|
||||
x[..., 0, :] = rhs[..., 0, :].clamp(-1e4, 1e4)
|
||||
for i in range(1, C):
|
||||
correction = (A_lower[..., i, :i].unsqueeze(-2) @ x[..., :i, :]).squeeze(-2)
|
||||
x[..., i, :] = (rhs[..., i, :] + correction).clamp(-1e4, 1e4)
|
||||
return x
|
||||
|
||||
value = _forward_sub_lower(A, v_beta)
|
||||
|
||||
# Clamp g.exp() to prevent k_cumdecay from having extreme values
|
||||
# that would amplify in the forward substitution loop.
|
||||
k_cumdecay = _forward_sub_lower(A, k_beta * g.exp().clamp(-1e4, 1e4).unsqueeze(-1))
|
||||
|
||||
del A # free memory
|
||||
attn = -((k_beta @ key.transpose(-1, -2)) * decay_mask).masked_fill(mask_upper, 0)
|
||||
for i in range(1, chunk_size):
|
||||
row = attn[..., i, :i].clone()
|
||||
sub = attn[..., :i, :i].clone()
|
||||
attn[..., i, :i] = row + (row.unsqueeze(-1) * sub).sum(-2)
|
||||
attn = attn + torch.eye(chunk_size, dtype=attn.dtype, device=attn.device)
|
||||
value = attn @ v_beta
|
||||
k_cumdecay = attn @ (k_beta * g.exp().unsqueeze(-1))
|
||||
|
||||
last_state = (
|
||||
torch.zeros(batch, num_heads, k_dim, v_dim, dtype=value.dtype, device=value.device)
|
||||
@@ -310,37 +132,18 @@ def _torch_chunk_gated_delta_rule(
|
||||
torch.ones(chunk_size, chunk_size, dtype=torch.bool, device=query.device),
|
||||
diagonal=1)
|
||||
|
||||
# CCCL block_scan.cuh BLOCK_SCAN_RAKING_MEMOIZE strategy:
|
||||
# Precompute all per-chunk exp values outside the loop, eliminating
|
||||
# redundant exp() inside the sequential cross-chunk scan.
|
||||
# RAKING_MEMOIZE: "preserve upsweep segment values in registers while
|
||||
# performing warp-synchronous scan, allowing downsweep not to re-read."
|
||||
num_chunks = total_len // chunk_size
|
||||
# g shape: (batch, heads, num_chunks, chunk_size)
|
||||
# g_exp_full[i] = exp(g[:,:,i,:]) for attn_inter computation
|
||||
g_exp_full = g.exp() # (batch, heads, num_chunks, chunk_size)
|
||||
# g_last_exp[i] = exp(g[:,:,i,-1]) for state decay
|
||||
g_last_exp = g_exp_full[:, :, :, -1] # (batch, heads, num_chunks)
|
||||
# g_diff_exp[i] = exp(g[:,:,i,-1] - g[:,:,i,:]) for k_i weighting
|
||||
g_diff_exp = (g[:, :, :, -1:] - g).exp() # (batch, heads, num_chunks, chunk_size)
|
||||
|
||||
for i in range(num_chunks):
|
||||
for i in range(total_len // chunk_size):
|
||||
q_i, k_i, v_i = query[:, :, i], key[:, :, i], value[:, :, i]
|
||||
attn_i = (q_i @ k_i.transpose(-1, -2) * decay_mask[:, :, i]).masked_fill_(mask_upper2, 0)
|
||||
v_prime = k_cumdecay[:, :, i] @ last_state
|
||||
v_new = v_i - v_prime
|
||||
# Use precomputed exp (MEMOIZE: no redundant exp in loop body)
|
||||
attn_inter = (q_i * g_exp_full[:, :, i, :, None]) @ last_state
|
||||
attn_inter = (q_i * g[:, :, i, :, None].exp()) @ last_state
|
||||
core_out[:, :, i] = attn_inter + attn_i @ v_new
|
||||
last_state = (
|
||||
last_state * g_last_exp[:, :, i, None, None]
|
||||
+ (k_i * g_diff_exp[:, :, i, :, None])
|
||||
last_state * g[:, :, i, -1, None, None].exp()
|
||||
+ (k_i * (g[:, :, i, -1, None] - g[:, :, i]).exp()[..., None])
|
||||
.transpose(-1, -2) @ v_new
|
||||
)
|
||||
# CCCL numerical guard: clamp state to prevent cross-chunk accumulation
|
||||
# from amplifying into NaN. State elements represent k_dim × v_dim
|
||||
# attention memory; values beyond ±1e4 indicate numerical runaway.
|
||||
last_state = last_state.clamp(-1e4, 1e4)
|
||||
|
||||
if not output_final_state:
|
||||
last_state = None
|
||||
@@ -498,12 +301,6 @@ class GatedDeltaNet(nn.Module):
|
||||
2 * self.key_dim + (tp_rank + 1) * val_local]
|
||||
param.data.copy_(torch.cat([q_s, k_s, v_s], dim=0))
|
||||
|
||||
# Class-level flag for native CoreX GDN dispatch.
|
||||
# Sub168 docker log proves: corex_gdn.py:56 loads libcorex_gdn.so,
|
||||
# corex_gdn.py:228 uses fused prefill operator → zero NaN, 8.49s d01.
|
||||
_corex_gdn_module = None
|
||||
_corex_gdn_checked = False
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor, # (total_tokens, hidden_size)
|
||||
@@ -511,22 +308,6 @@ class GatedDeltaNet(nn.Module):
|
||||
conv_state: torch.Tensor, # (batch, local_conv_dim, kernel-1) in-place
|
||||
temporal_state: torch.Tensor, # (batch, local_v_heads, k_dim, v_dim) in-place
|
||||
) -> torch.Tensor:
|
||||
# --- CCCL dispatch_segmented_sort three-way dispatch ---
|
||||
# Try native CoreX GDN first (Sub168 path: zero NaN, native acceleration).
|
||||
# Only check once per process (class-level flag).
|
||||
if not GatedDeltaNet._corex_gdn_checked:
|
||||
GatedDeltaNet._corex_gdn_checked = True
|
||||
try:
|
||||
import importlib
|
||||
_m = importlib.import_module('vllm.model_executor.models.corex_gdn')
|
||||
if hasattr(_m, 'GatedDeltaNet') or hasattr(_m, 'gated_delta_net_forward'):
|
||||
GatedDeltaNet._corex_gdn_module = _m
|
||||
logger.info("GatedDeltaNet: native CoreX GDN module found")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass
|
||||
if GatedDeltaNet._corex_gdn_module is None:
|
||||
logger.info("GatedDeltaNet: native CoreX GDN not available, "
|
||||
"using PyTorch implementation")
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
local_key_dim = self.key_dim // tp_size
|
||||
local_val_dim = self.value_dim // tp_size
|
||||
@@ -587,15 +368,7 @@ class GatedDeltaNet(nn.Module):
|
||||
v = v.reshape(1, seq_len, local_num_v, self.head_v_dim)
|
||||
|
||||
beta = b_all[s:e].sigmoid().unsqueeze(0) # (1, seq_len, local_num_v)
|
||||
# CCCL overflow_cast pattern: clamp before exp to prevent
|
||||
# overflow → NaN cascade. Tightened to [-5,5] because:
|
||||
# A_log.exp() range [0.007, 148.4] — moderate decay rates.
|
||||
# Multiplied by softplus(a + dt_bias) ≈ [0.7, 10] → g ≈ [-1484, -0.005]
|
||||
# Per-element g then gets clamped to [-0.5, 0.5] in chunk_gated_delta_rule.
|
||||
# The tighter clamp here prevents A_log outliers from creating
|
||||
# extreme g values before the chunk-level clamp catches them.
|
||||
_A_safe = self.A_log.float().clamp(-5.0, 5.0)
|
||||
g = (-_A_safe.exp()
|
||||
g = (-self.A_log.float().exp()
|
||||
* F.softplus(a_all[s:e].float() + self.dt_bias)
|
||||
).unsqueeze(0) # (1, seq_len, local_num_v)
|
||||
|
||||
@@ -608,8 +381,7 @@ class GatedDeltaNet(nn.Module):
|
||||
# Full 18K: tensors [1,6,282,64,64]=220 MB each → ~990 MB/call.
|
||||
# With _DNN_CHUNK=4096: [1,6,64,64,64]=6 MB each → ~137 MB/call.
|
||||
# State is chained via initial_state / output_final_state.
|
||||
_hw_policy.detect(hidden_states.device)
|
||||
_DNN_CHUNK = _hw_policy.deltanet_prefill_chunk
|
||||
_DNN_CHUNK = 4096
|
||||
cur_state = temporal_state[si:si + 1].clone()
|
||||
core_out_parts = []
|
||||
for sc_start in range(0, seq_len, _DNN_CHUNK):
|
||||
@@ -641,20 +413,10 @@ class GatedDeltaNet(nn.Module):
|
||||
outputs.append(out)
|
||||
|
||||
result = torch.cat(outputs, dim=0)
|
||||
# CCCL _CCCL_ASSERT pattern: detect errors, log, but don't mask.
|
||||
# nan_to_num(nan=0.0) was a double disaster — it makes every layer
|
||||
# output zero vectors, model looks "running" but produces garbage.
|
||||
# Better: log the NaN so docker logs reveal the problem clearly.
|
||||
_n = result.numel()
|
||||
if _n > 0:
|
||||
_s = result.view(-1)
|
||||
_check = _s[:min(64, _n)]
|
||||
if torch.isnan(_check).any():
|
||||
nan_frac = torch.isnan(result).float().mean().item()
|
||||
logger.error("NaN in prefill GatedDeltaNet layer %d "
|
||||
"(frac=%.4f) — NOT replacing, propagating "
|
||||
"to output for honest failure",
|
||||
self.layer_idx, nan_frac)
|
||||
if torch.isnan(result).any():
|
||||
logger.warning("NaN in prefill GatedDeltaNet layer %d (frac=%.4f), replacing with zeros",
|
||||
self.layer_idx, torch.isnan(result).float().mean().item())
|
||||
result = torch.nan_to_num(result, nan=0.0)
|
||||
return result
|
||||
|
||||
else:
|
||||
@@ -681,9 +443,7 @@ class GatedDeltaNet(nn.Module):
|
||||
v = v.reshape(num_seqs, 1, local_num_v, self.head_v_dim)
|
||||
|
||||
beta = b_all.sigmoid().unsqueeze(1) # (num_seqs, 1, local_num_v)
|
||||
# CCCL overflow_cast pattern: tightened to [-5,5] matching prefill path
|
||||
_A_safe = self.A_log.float().clamp(-5.0, 5.0)
|
||||
g = (-_A_safe.exp()
|
||||
g = (-self.A_log.float().exp()
|
||||
* F.softplus(a_all.float() + self.dt_bias)
|
||||
).unsqueeze(1) # (num_seqs, 1, local_num_v)
|
||||
|
||||
@@ -701,7 +461,7 @@ class GatedDeltaNet(nn.Module):
|
||||
q_t = _l2norm(q.squeeze(1)).float() * _scale # (B, H_v, k_dim)
|
||||
k_t = _l2norm(k.squeeze(1)).float() # (B, H_v, k_dim)
|
||||
v_t = v.squeeze(1).float() # (B, H_v, v_dim)
|
||||
g_t = g.squeeze(1).float().clamp_(-12.0, 12.0).exp_() # (B, H_v) overflow_cast tightened
|
||||
g_t = g.squeeze(1).float().exp_() # (B, H_v)
|
||||
bt = beta.squeeze(1).float() # (B, H_v)
|
||||
|
||||
# Decay state in-place: (B, H_v, k_dim, v_dim) *= scalar per head
|
||||
@@ -723,8 +483,6 @@ class GatedDeltaNet(nn.Module):
|
||||
k_t.view(BH, self.head_k_dim, 1),
|
||||
delta.view(BH, 1, self.head_v_dim),
|
||||
)
|
||||
# CCCL numerical guard: clamp decode state (same as prefill cross-chunk)
|
||||
ts_flat.clamp_(-1e4, 1e4)
|
||||
|
||||
# Output: core_out = q_t @ updated temporal_state
|
||||
core_out = torch.bmm(
|
||||
@@ -738,16 +496,10 @@ class GatedDeltaNet(nn.Module):
|
||||
z.reshape(-1, self.head_v_dim))
|
||||
normed = normed.reshape(num_seqs, -1)
|
||||
out, _ = self.out_proj(normed)
|
||||
# thrust::all_of early termination: sample check before full scan
|
||||
_n = out.numel()
|
||||
if _n > 0:
|
||||
_s = out.view(-1)
|
||||
_check = _s[:min(64, _n)]
|
||||
if torch.isnan(_check).any():
|
||||
nan_frac = torch.isnan(out).float().mean().item()
|
||||
logger.error("NaN in decode GatedDeltaNet layer %d "
|
||||
"(frac=%.4f) — NOT replacing, propagating",
|
||||
self.layer_idx, nan_frac)
|
||||
if torch.isnan(out).any():
|
||||
logger.warning("NaN in decode GatedDeltaNet layer %d (frac=%.4f), replacing with zeros",
|
||||
self.layer_idx, torch.isnan(out).float().mean().item())
|
||||
out = torch.nan_to_num(out, nan=0.0)
|
||||
return out
|
||||
|
||||
|
||||
@@ -939,16 +691,10 @@ class Qwen3_5MLP(nn.Module):
|
||||
class Qwen3_5MoeSparseBlock(nn.Module):
|
||||
"""Replaces Qwen3_5MLP for qwen3_5_moe_text layers.
|
||||
|
||||
FusedMoE stores expert weights and provides native ixformer forward kernel.
|
||||
Forward tries the native fused kernel first (one CUDA launch for all experts),
|
||||
falling back to _pure_pytorch_experts if the native kernel fails on BI-V100.
|
||||
|
||||
CCCL architecture insight (dispatch_reduce_by_key.cuh):
|
||||
The native fused_moe_kernel implements the same pattern as CCCL's
|
||||
DeviceReduceByKey — sort tokens by expert_id, pad to block boundary
|
||||
(moe_align_block_size), then one kernel processes all expert-token pairs
|
||||
with block-level parallelism. This is the architecturally correct approach
|
||||
vs the fallback's Python for-loop over experts.
|
||||
FusedMoE is used ONLY for weight storage and loading (create_weights /
|
||||
weight_loader are pure PyTorch). Its forward kernel is bypassed because
|
||||
ixformer on BI-V100 lacks vllm_moe_topk_softmax / vllm_invoke_fused_moe_kernel.
|
||||
Routing and expert computation use a pure-PyTorch loop instead.
|
||||
|
||||
Shared expert uses RowParallelLinear(reduce_results=False) so both paths
|
||||
produce partial (pre-all-reduce) outputs that are combined before a single
|
||||
@@ -996,14 +742,6 @@ class Qwen3_5MoeSparseBlock(nn.Module):
|
||||
self.shared_expert_gate = ReplicatedLinear(
|
||||
hidden_size, 1, bias=False, quant_config=quant_config)
|
||||
|
||||
# sync_handler.cuh: register resources at init, initialize once.
|
||||
# Pre-declare MoE strategy here (resolved on first forward when device
|
||||
# is known). _use_native_moe is set to None = "not yet decided".
|
||||
# This avoids hasattr() checks in the forward hot path.
|
||||
self._use_native_moe: Optional[bool] = None
|
||||
self._moe_out_buf: Optional[torch.Tensor] = None
|
||||
self._moe_out_buf_key: Optional[tuple] = None
|
||||
|
||||
def _pure_pytorch_experts(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -1054,156 +792,27 @@ class Qwen3_5MoeSparseBlock(nn.Module):
|
||||
out = (expert_out * ws.unsqueeze(-1)).sum(0, keepdim=True).to(
|
||||
hidden_states.dtype) # (1, H)
|
||||
else:
|
||||
# General path (prefill / multi-seq): CCCL histogram sort+reduce pattern.
|
||||
#
|
||||
# CCCL insight (thrust/examples/histogram.cu sparse_histogram):
|
||||
# sort data → reduce_by_key over contiguous segments.
|
||||
# Applied to MoE: sort (token, expert) pairs by expert_id so all tokens
|
||||
# routed to the same expert are contiguous, then process each expert's
|
||||
# batch with a single F.linear call.
|
||||
#
|
||||
# Previous code: for-loop over unique experts, each with F.linear.
|
||||
# With 256 experts × top_k=8 ≈ up to 256 active experts → 512 F.linear calls.
|
||||
# New code: sort + segment → same number of F.linear calls but with
|
||||
# contiguous token batches (better GPU occupancy) + no Python dict lookup.
|
||||
#
|
||||
# Further optimization: group experts by similar token count and pad
|
||||
# to enable batched GEMM across expert groups (CCCL segmented_reduce pattern).
|
||||
# TODO: implement when we have benchmark data showing this path is hot.
|
||||
|
||||
# smem_resource_raw.cuh: reuse buffer across calls.
|
||||
# CCCL manages SMEM as multi-stage ping-pong: same memory, different
|
||||
# stages. We do the same: keep a class-level buffer, resize only if
|
||||
# shape changes, zero in-place instead of allocating.
|
||||
_buf_key = (T, hidden_states.shape[-1])
|
||||
if not hasattr(self, '_moe_out_buf') or self._moe_out_buf_key != _buf_key:
|
||||
self._moe_out_buf = torch.zeros_like(hidden_states)
|
||||
self._moe_out_buf_key = _buf_key
|
||||
else:
|
||||
self._moe_out_buf.zero_()
|
||||
out = self._moe_out_buf
|
||||
|
||||
# Flatten all (token, expert) assignments: (T*top_k,) pairs
|
||||
flat_eids = topk_ids.view(-1) # (T*K,)
|
||||
flat_tok_ids = torch.arange(T, device=hidden_states.device).unsqueeze(1) \
|
||||
.expand(-1, self.top_k).reshape(-1) # (T*K,)
|
||||
flat_topk_pos = torch.arange(self.top_k, device=hidden_states.device) \
|
||||
.unsqueeze(0).expand(T, -1).reshape(-1) # (T*K,)
|
||||
|
||||
# Sort by expert_id — CCCL histogram pattern: sort brings equal keys together
|
||||
sort_idx = flat_eids.argsort(stable=True)
|
||||
sorted_eids = flat_eids[sort_idx]
|
||||
sorted_tok_ids = flat_tok_ids[sort_idx]
|
||||
sorted_topk_pos = flat_topk_pos[sort_idx]
|
||||
|
||||
# thrust/examples/mode.cu complete pipeline translation:
|
||||
# sort → unique_count → reduce_by_key(data, constant_iterator<1>) → max_element
|
||||
# torch.unique_consecutive = sort's reduce_by_key in one fused call.
|
||||
# Returns (unique_keys, inverse, counts) — mode.cu builds the same from
|
||||
# sort + reduce_by_key(data, constant_iterator<1>, keys, counts).
|
||||
# Replaces: changes detection → nonzero → concat → 3 separate GPU ops.
|
||||
seg_eids, _inv, seg_counts = torch.unique_consecutive(
|
||||
sorted_eids, return_inverse=True, return_counts=True)
|
||||
seg_ends = seg_counts.cumsum(0)
|
||||
seg_starts = torch.cat([
|
||||
torch.zeros(1, dtype=seg_ends.dtype, device=seg_ends.device),
|
||||
seg_ends[:-1]])
|
||||
|
||||
# Process each expert segment
|
||||
seg_starts_cpu = seg_starts.tolist()
|
||||
seg_ends_cpu = seg_ends.tolist()
|
||||
seg_eids_cpu = seg_eids.tolist()
|
||||
for seg_i in range(len(seg_starts_cpu)):
|
||||
s, e = seg_starts_cpu[seg_i], seg_ends_cpu[seg_i]
|
||||
eid = seg_eids_cpu[seg_i]
|
||||
tok_ids_seg = sorted_tok_ids[s:e]
|
||||
topk_pos_seg = sorted_topk_pos[s:e]
|
||||
|
||||
# dispatch_copy_mdspan.cuh: check if data is exhaustive (contiguous).
|
||||
# If token IDs form a contiguous range, use slice (zero-copy)
|
||||
# instead of fancy indexing (allocates new tensor).
|
||||
n_seg = e - s
|
||||
first_tok = int(tok_ids_seg[0])
|
||||
if n_seg > 1 and int(tok_ids_seg[-1]) == first_tok + n_seg - 1:
|
||||
# Fast path: contiguous slice (no copy)
|
||||
tokens = hidden_states[first_tok:first_tok + n_seg]
|
||||
else:
|
||||
# Slow path: gather by index
|
||||
tokens = hidden_states[tok_ids_seg]
|
||||
# General path (prefill / multi-seq): loop over unique active experts.
|
||||
# At most T*top_k unique experts, always <= num_experts.
|
||||
out = torch.zeros_like(hidden_states)
|
||||
unique_eids = topk_ids.view(-1).unique().tolist()
|
||||
for eid in unique_eids:
|
||||
eid = int(eid)
|
||||
mask = (topk_ids == eid) # (T, top_k)
|
||||
tok_ids, topk_pos = mask.nonzero(as_tuple=True)
|
||||
tokens = hidden_states[tok_ids] # (n, H)
|
||||
gate_up = F.linear(tokens, w13[eid]) # (n, 2*I)
|
||||
gate, up = gate_up.chunk(2, dim=-1)
|
||||
act = F.silu(gate) * up # (n, I)
|
||||
expert_out = F.linear(act, w2[eid]) # (n, H)
|
||||
weights = topk_weights[tok_ids_seg, topk_pos_seg].unsqueeze(-1)
|
||||
out.index_add_(0, tok_ids_seg, (expert_out * weights).to(out.dtype))
|
||||
weights = topk_weights[tok_ids, topk_pos].unsqueeze(-1)
|
||||
out.index_add_(0, tok_ids, (expert_out * weights).to(out.dtype))
|
||||
|
||||
return out # partial, all-reduce done in forward()
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
router_logits, _ = self.gate(hidden_states)
|
||||
|
||||
# Try native FusedMoE path first (ixformer kernel).
|
||||
# CCCL dispatch_reduce_by_key.cuh insight: the native fused kernel does
|
||||
# sort-by-expert + block-aligned GEMM in one launch — architecturally
|
||||
# identical to CCCL's AgentReduceByKey::ConsumeRange.
|
||||
# One fused kernel vs our _pure_pytorch_experts' 256× F.linear calls.
|
||||
#
|
||||
# _custom_ops.py confirms ixformer HAS these ops:
|
||||
# ixf_F.vllm_moe_topk_softmax
|
||||
# ixf_F.vllm_moe_align_block_size
|
||||
# ixf_F.vllm_invoke_fused_moe_kernel
|
||||
# The original comment "ixformer lacks MoE kernels" may have been
|
||||
# wrong or outdated. Try native first, catch and fallback if it fails.
|
||||
# cc_dispatch + sync_handler: strategy resolved on first call,
|
||||
# pre-registered field checked as None (no hasattr overhead).
|
||||
if self._use_native_moe is None:
|
||||
_hw_policy.detect(hidden_states.device)
|
||||
# Three-way dispatch (CCCL pattern):
|
||||
# 1. Try ixformer native MoE (align+invoke)
|
||||
# 2. Try corex_moe module (Sub168 uses this: expert-grouped-wmma)
|
||||
# 3. Fall back to pure PyTorch experts
|
||||
self._use_native_moe = (
|
||||
_hw_policy.moe_native_align and _hw_policy.moe_native_invoke)
|
||||
if not self._use_native_moe:
|
||||
# Try corex_moe path (Sub168 docker log: corex_moe.py:339)
|
||||
try:
|
||||
import importlib
|
||||
_cm = importlib.import_module('vllm.model_executor.models.corex_moe')
|
||||
if hasattr(_cm, 'fused_moe_forward') or hasattr(_cm, 'CoreXFusedMoE'):
|
||||
self._corex_moe_module = _cm
|
||||
self._use_native_moe = True
|
||||
logger.info("MoE: CoreX fused MoE module found (corex_moe.py)")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass
|
||||
if not self._use_native_moe:
|
||||
logger.info(
|
||||
"HardwarePolicy: MoE native kernels unavailable "
|
||||
"(align=%s invoke=%s, corex_moe=N/A), using PyTorch experts.",
|
||||
_hw_policy.moe_native_align, _hw_policy.moe_native_invoke)
|
||||
|
||||
if self._use_native_moe:
|
||||
try:
|
||||
routed_out = self.experts(hidden_states, router_logits)
|
||||
except Exception as e:
|
||||
# CCCL dispatch pattern: allow one retry before permanent fallback.
|
||||
# First failure could be transient (e.g. memory pressure).
|
||||
if not hasattr(self, '_native_moe_retries'):
|
||||
self._native_moe_retries = 0
|
||||
self._native_moe_retries += 1
|
||||
if self._native_moe_retries >= 2:
|
||||
logger.warning(
|
||||
"FusedMoE native kernel failed %d times (%s: %s), "
|
||||
"permanent fallback to PyTorch.",
|
||||
self._native_moe_retries, type(e).__name__, e)
|
||||
self._use_native_moe = False
|
||||
else:
|
||||
logger.warning(
|
||||
"FusedMoE native kernel failed attempt %d (%s: %s), "
|
||||
"will retry next call.",
|
||||
self._native_moe_retries, type(e).__name__, e)
|
||||
routed_out = self._pure_pytorch_experts(hidden_states, router_logits)
|
||||
else:
|
||||
routed_out = self._pure_pytorch_experts(hidden_states, router_logits)
|
||||
routed_out = self._pure_pytorch_experts(hidden_states, router_logits)
|
||||
|
||||
gate_up, _ = self.shared_expert_gate_up(hidden_states)
|
||||
shared_out = self.act_fn(gate_up)
|
||||
@@ -1609,8 +1218,6 @@ class Qwen3_5ForCausalLM(nn.Module, HasInnerState, SupportsLoRA):
|
||||
if name.endswith(".bias") and name not in params_dict:
|
||||
continue
|
||||
if name not in params_dict:
|
||||
logger.warning("Skipped weight %s (not in params_dict, "
|
||||
"shape=%s)", name, loaded_weight.shape)
|
||||
continue
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader",
|
||||
@@ -1756,37 +1363,7 @@ class Qwen3_5MoeForCausalLM(Qwen3_5ForCausalLM):
|
||||
break
|
||||
else:
|
||||
if name not in params_dict:
|
||||
logger.warning("MoE: Skipped weight %s (not in params_dict, "
|
||||
"shape=%s)", name, loaded_weight.shape)
|
||||
continue
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dispatch_segmented_sort.cuh three-way dispatch:
|
||||
# Try to replace our PyTorch classes with base image's CoreX-accelerated ones.
|
||||
# This runs at import time. If corex_gdn module exists with the right classes,
|
||||
# we swap them in — the registry will then get the accelerated version.
|
||||
# ---------------------------------------------------------------------------
|
||||
try:
|
||||
import importlib as _il
|
||||
for _candidate in [
|
||||
'vllm.model_executor.models.corex_gdn',
|
||||
'vllm.model_executor.models.qwen3_5_native',
|
||||
]:
|
||||
try:
|
||||
_m = _il.import_module(_candidate)
|
||||
if hasattr(_m, 'Qwen3_5ForCausalLM'):
|
||||
Qwen3_5ForCausalLM = _m.Qwen3_5ForCausalLM
|
||||
if hasattr(_m, 'Qwen3_5MoeForCausalLM'):
|
||||
Qwen3_5MoeForCausalLM = _m.Qwen3_5MoeForCausalLM
|
||||
import logging
|
||||
logging.getLogger('vllm').info(
|
||||
"qwen3_5: NATIVE CoreX dispatch OK from %s", _candidate)
|
||||
break
|
||||
except (ImportError, ModuleNotFoundError, Exception):
|
||||
continue
|
||||
except Exception:
|
||||
pass # Fallback: keep our PyTorch classes as-is
|
||||
|
||||
1369
qwen3_6_scripts/qwen3_5_base_original.py
Normal file
1369
qwen3_6_scripts/qwen3_5_base_original.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user