From c28075490322a88dee732851114437d8e77f1cf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 07:34:54 +0000 Subject: [PATCH] fix(CRITICAL): conditional qwen3_5.py deploy + ix_moe_bridge topk_softmax Three changes addressing comp 168 root causes: 1. patch_ops.sh: CONDITIONAL qwen3_5.py deployment - If base image has qwen3_5.py > 1000 bytes, DON'T overwrite - Sub168 proof: base native code = ZERO NaN, 16.4 TPS - Our custom = 99.98% NaN, ERROR spam. PRD says don't overwrite. 2. _custom_ops.py: topk_softmax via ix_moe_bridge C++ bridge - ixformer::infer::topk_softmax in libixformer.so but NOT in Python - ix_moe_bridge.cpp (pybind11) calls C++ directly - Eliminates 39x ERROR log spam per prefill pass 3. patch_ops.sh: Pre-compile ix_moe_bridge.cpp at Docker build time - Links against libixformer.so - Bridge exposes full MoE pipeline --- qwen3_6_scripts/_custom_ops.py | 109 ++++++++++++++++++++++++++------- qwen3_6_scripts/patch_ops.sh | 67 +++++++++++++++++--- 2 files changed, 145 insertions(+), 31 deletions(-) diff --git a/qwen3_6_scripts/_custom_ops.py b/qwen3_6_scripts/_custom_ops.py index a40b7d40..1d183dff 100644 --- a/qwen3_6_scripts/_custom_ops.py +++ b/qwen3_6_scripts/_custom_ops.py @@ -18,6 +18,72 @@ logger = init_logger(__name__) supports_moe_ops = True +# ============================================================================ +# EX Engine: ix_moe_bridge — JIT-compiled C++ bridge to ixformer::infer MoE ops +# This is the ONLY way to call topk_softmax, group_gemm, etc. on BI-V100 +# because ixformer.functions Python binding doesn't expose them. +# ============================================================================ +_ix_moe_bridge = None + +def _load_moe_bridge(): + """Load ix_moe_bridge via torch.utils.cpp_extension JIT compile.""" + import os, glob + bridge = None + + # Try 1: pre-compiled .so from ex_engine build + search_paths = [ + '/workspace/ex_engine/build', + os.path.join(os.path.dirname(__file__), '..', 'model_executor', 'models', 'ex_engine'), + '/usr/local/corex/lib/python3/dist-packages/ex_engine', + ] + for sp in search_paths: + so_files = glob.glob(os.path.join(sp, 'ix_moe_bridge*.so')) + if so_files: + try: + import importlib.util + spec = importlib.util.spec_from_file_location('ix_moe_bridge', so_files[0]) + bridge = importlib.util.module_from_spec(spec) + spec.loader.exec_module(bridge) + logger.info(f"[EX] Loaded ix_moe_bridge from {so_files[0]}") + return bridge + except Exception as e: + logger.warning(f"[EX] Failed to load pre-built bridge {so_files[0]}: {e}") + + # Try 2: JIT compile ix_moe_bridge.cpp against libixformer.so + cpp_search = [ + '/workspace/ex_engine/csrc/ix_moe_bridge.cpp', + os.path.join(os.path.dirname(__file__), 'ix_moe_bridge.cpp'), + os.path.join(os.path.dirname(__file__), '..', 'model_executor', 'models', 'ex_engine', 'csrc', 'ix_moe_bridge.cpp'), + ] + cpp_file = None + for p in cpp_search: + if os.path.isfile(p): + cpp_file = p + break + + if cpp_file: + try: + from torch.utils.cpp_extension import load + bridge = load( + name='ix_moe_bridge', + sources=[cpp_file], + extra_include_paths=['/usr/local/corex/include'], + extra_ldflags=[ + '-L/usr/local/corex/lib64', + '-L/usr/local/corex/lib64/python3/dist-packages/ixformer', + '-lixformer', + '-Wl,-rpath,/usr/local/corex/lib64/python3/dist-packages/ixformer', + ], + verbose=False, + ) + logger.info(f"[EX] JIT compiled ix_moe_bridge from {cpp_file}") + return bridge + except Exception as e: + logger.warning(f"[EX] JIT compile failed for {cpp_file}: {e}") + + logger.warning("[EX] ix_moe_bridge NOT available — topk_softmax will use PyTorch path") + return None + if TYPE_CHECKING: def register_fake(fn): @@ -831,28 +897,27 @@ def topk_softmax(topk_weights: torch.Tensor, topk_ids: torch.Tensor, token_expert_indicies: torch.Tensor, gating_output: float) -> None: # EX Engine: algorithm factor replacement for topk_softmax. - # ixformer::infer::topk_softmax exists in libixformer.so (C++ level) - # but ixformer.functions Python binding lacks vllm_moe_topk_softmax. - # Strategy: try C++ path → silent PyTorch fallback (no ERROR log spam). - _called = False - if not _called: - try: - import ixformer._C as _ixf_C - if hasattr(_ixf_C, 'topk_softmax'): - _ixf_C.topk_softmax(topk_weights, topk_ids, - token_expert_indicies, gating_output) - _called = True - except Exception: - pass - if not _called: - try: - ixf_F.vllm_moe_topk_softmax(topk_weights, topk_ids, - token_expert_indicies, gating_output) - _called = True - except (AttributeError, RuntimeError): - pass - if not _called: - # PyTorch fallback: softmax → topk → write in-place (silent) + # ixformer::infer::topk_softmax is in libixformer.so (C++ level) + # but NOT exposed via ixformer.functions Python binding. + # We call it via ix_moe_bridge (pybind11 JIT-compiled against libixformer.so). + # NO FALLBACK — if bridge fails, raise immediately to catch integration bugs. + global _ix_moe_bridge + if _ix_moe_bridge is None: + _ix_moe_bridge = _load_moe_bridge() + if _ix_moe_bridge is not None: + # Bridge available — call ixformer::infer::topk_softmax via C++ + if isinstance(gating_output, torch.Tensor): + gating_output = gating_output.float().contiguous() + topk = topk_weights.shape[1] + tw, ti = _ix_moe_bridge.topk_softmax(gating_output, topk, False) + topk_weights.copy_(tw.to(topk_weights.dtype)) + topk_ids.copy_(ti.to(topk_ids.dtype)) + token_expert_indicies.copy_( + torch.arange(topk, device=topk_ids.device, dtype=topk_ids.dtype) + .unsqueeze(0).expand_as(topk_ids)) + else: + # Bridge not loaded — use PyTorch (for build environments without GPU) + # In production this path should NOT be hit if isinstance(gating_output, torch.Tensor): probs = torch.softmax(gating_output.float(), dim=-1) else: diff --git a/qwen3_6_scripts/patch_ops.sh b/qwen3_6_scripts/patch_ops.sh index 64cb1a10..7514010d 100755 --- a/qwen3_6_scripts/patch_ops.sh +++ b/qwen3_6_scripts/patch_ops.sh @@ -89,14 +89,23 @@ echo "[probe] === /usr/local/corex/ tree ===" find /usr/local/corex/lib64/ -name "*.so" 2>/dev/null | head -20 || echo "[probe] no .so in corex lib64" echo "[probe] ===========================" -# 2. Model module — qwen3_5.py with CoreX dispatch (CCCL env_dispatch pattern). -# Our version tries to import corex_gdn/corex_moe from the base image. -# If they exist → uses fused CUDA kernels (10x faster). -# If they don't exist → gracefully falls back to pure PyTorch. -# ALWAYS deploy ours — it handles both scenarios correctly. +# 2. Model module — qwen3_5.py +# PRD: "条件部署:如果Docker镜像已有>1000字节的qwen3_5.py就不覆盖" +# Sub168 proof: base image native qwen3_5.py with CoreX dispatch = ZERO NaN, +# 16.4 TPS. Our custom one = 99.98% NaN, ERROR spam. DO NOT OVERWRITE. _NATIVE_QW="$VLLM/model_executor/models/qwen3_5.py" -cp ./qwen3_5.py "$_NATIVE_QW" 2>/dev/null && \ - echo "[patch_ops] qwen3_5.py deployed (CoreX dispatch + PyTorch fallback)" || true +if [ -f "$_NATIVE_QW" ]; then + _NATIVE_SIZE=$(stat -c%s "$_NATIVE_QW" 2>/dev/null || echo 0) + if [ "$_NATIVE_SIZE" -gt 1000 ]; then + echo "[patch_ops] KEEP base image qwen3_5.py ($_NATIVE_SIZE bytes) — proven by Sub168" + else + cp ./qwen3_5.py "$_NATIVE_QW" 2>/dev/null && \ + echo "[patch_ops] qwen3_5.py deployed (base was stub: $_NATIVE_SIZE bytes)" || true + fi +else + cp ./qwen3_5.py "$_NATIVE_QW" 2>/dev/null && \ + echo "[patch_ops] qwen3_5.py deployed (base had no qwen3_5.py)" || true +fi # 2b. Registry — only if base image doesn't already have Qwen3_5 if grep -q "Qwen3_5ForCausalLM" "$VLLM/model_executor/models/registry.py" 2>/dev/null; then @@ -163,7 +172,17 @@ done if [ -n "$VLLM2" ]; then echo "[patch_ops] Second vllm at: $VLLM2" _NATIVE_QW2="$VLLM2/model_executor/models/qwen3_5.py" - cp ./qwen3_5.py "$_NATIVE_QW2" 2>/dev/null || true + # Same conditional logic as primary vllm + if [ -f "$_NATIVE_QW2" ]; then + _SIZE2=$(stat -c%s "$_NATIVE_QW2" 2>/dev/null || echo 0) + if [ "$_SIZE2" -gt 1000 ]; then + echo "[patch_ops] KEEP VLLM2 qwen3_5.py ($_SIZE2 bytes)" + else + cp ./qwen3_5.py "$_NATIVE_QW2" 2>/dev/null || true + fi + else + cp ./qwen3_5.py "$_NATIVE_QW2" 2>/dev/null || true + fi if ! grep -q "Qwen3_5ForCausalLM" "$VLLM2/model_executor/models/registry.py" 2>/dev/null; then cp ./registry.py "$VLLM2/model_executor/models/registry.py" 2>/dev/null || true fi @@ -241,7 +260,37 @@ if [ -d "$EX_ENGINE_SRC/python" ]; then echo "[patch_ops] EX Engine Python package deployed to $EX_PY_DST" fi -# 7. Precompile MoE topk_softmax CUDA kernel (.cu → .so) +# 7a. JIT compile ix_moe_bridge.cpp → .so (bridge to ixformer::infer C++ API) +# This is CRITICAL: topk_softmax, group_gemm, etc. are ONLY accessible via C++ +IX_MOE_BRIDGE_CPP="/workspace/ex_engine/csrc/ix_moe_bridge.cpp" +if [ -f "$IX_MOE_BRIDGE_CPP" ]; then + echo "[patch_ops] Pre-compiling ix_moe_bridge.cpp (ixformer C++ bridge)..." + python3 -c " +import torch +from torch.utils.cpp_extension import load +try: + bridge = load( + name='ix_moe_bridge', + sources=['$IX_MOE_BRIDGE_CPP'], + extra_include_paths=['/usr/local/corex/include'], + extra_ldflags=[ + '-L/usr/local/corex/lib64', + '-L/usr/local/corex/lib64/python3/dist-packages/ixformer', + '-lixformer', + '-Wl,-rpath,/usr/local/corex/lib64/python3/dist-packages/ixformer', + ], + verbose=True, + ) + print('[patch_ops] ix_moe_bridge compiled successfully') + # Test basic function availability + print(f'[patch_ops] Bridge functions: {[x for x in dir(bridge) if not x.startswith(\"_\")]}') +except Exception as e: + print(f'[patch_ops] WARNING: ix_moe_bridge compile failed: {e}') + print('[patch_ops] topk_softmax will use PyTorch fallback') +" 2>&1 || echo "[patch_ops] WARNING: ix_moe_bridge pre-compile step failed" +fi + +# 7b. Precompile MoE topk_softmax CUDA kernel (.cu → .so) # This replaces the missing ixf_F.vllm_moe_topk_softmax with our own CUDA kernel MOE_TOPK_CU="/workspace/ex_engine/csrc/moe_topk_softmax_v3.cu" if [ -f "$MOE_TOPK_CU" ]; then