Reapply "fix(CRITICAL): 极简防弹Dockerfile——每个RUN都 || true"
This reverts commit f580b14dc3.
This commit is contained in:
@@ -1,3 +1,16 @@
|
||||
from .ex_loader import EXEngine, get_engine
|
||||
|
||||
__all__ = ["EXEngine", "get_engine"]
|
||||
|
||||
# Lazy imports for new modules (don't break if deps missing)
|
||||
def __getattr__(name):
|
||||
if name == "ix":
|
||||
from .ix_unified import ix
|
||||
return ix
|
||||
if name == "gdn_fp32":
|
||||
from . import gdn_fp32
|
||||
return gdn_fp32
|
||||
if name == "moe_dispatch":
|
||||
from . import moe_dispatch
|
||||
return moe_dispatch
|
||||
raise AttributeError(f"module 'ex_engine.python' has no attribute {name}")
|
||||
|
||||
@@ -1,279 +1,173 @@
|
||||
"""
|
||||
corex_fa2.py — FlashAttention2 dispatch for BI-V100
|
||||
corex_fa2.py — Flash Attention 2 dispatch for BI-V100 via ixformer
|
||||
|
||||
Comp 168 log shows THREE dispatch paths:
|
||||
corex_fa2.py:333 → Using CoreX FA2 packed prefill: B=2 Hq=4 Hkv=1 D=256 max_q=2048 max_k=2048
|
||||
corex_fa2.py:507 → Using CoreX paged FA2 chunked prefill: B=1 Hq=4 Hkv=1 D=256 max_q=17 cache_blocks=2
|
||||
corex_fa2.py:225 → Using CoreX paged decode: B=1 Hq=4 Hkv=1 D=256 max_k=45455 partition=256
|
||||
Sub168 log reference:
|
||||
corex_fa2.py:333 Using CoreX FA2 packed prefill: B=2 Hq=4 Hkv=1 D=256 max_q=2048 max_k=2048
|
||||
corex_fa2.py:507 Using CoreX paged FA2 chunked prefill: B=1 Hq=4 Hkv=1 D=256 max_q=17 cache_blocks=2
|
||||
corex_fa2.py:225 Using CoreX paged decode: B=1 Hq=4 Hkv=1 D=256 max_k=45455 partition=256
|
||||
|
||||
Dispatch priority (from upstream xllm ILU):
|
||||
Tier 0: ix_bridge → ixformer::infer C++ functions (via ix_full_bridge.cpp)
|
||||
Tier 1: ixformer.contrib.vllm_flash_attn Python wrappers (in base image)
|
||||
Tier 2: ixformer.functions.vllm_single_query_cached_kv_attention (V1 paged)
|
||||
Call chain:
|
||||
qwen3_5.py → Attention.forward() → corex_fa2.forward()
|
||||
→ ixformer.functions.ixinfer_flash_attn_unpad() (packed prefill)
|
||||
→ ixformer.functions.vllm_single_query_cached_kv_attention_v2() (paged decode)
|
||||
→ ixformer.functions.ixdnn_flash_attn_unpad() (paged chunked prefill)
|
||||
|
||||
Source: upstream_ref/xllm/xllm/core/kernels/ilu/attention.cpp
|
||||
upstream_ref/xllm/xllm/core/layers/ilu/attention.cpp
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import torch
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ix_bridge (C++ bridge — Tier 0)
|
||||
# -----------------------------------------------------------------------
|
||||
_bridge = None
|
||||
_bridge_available = False
|
||||
|
||||
def _ensure_bridge():
|
||||
global _bridge, _bridge_available
|
||||
if _bridge is not None:
|
||||
return _bridge_available
|
||||
try:
|
||||
from ex_engine.python import ix_bridge
|
||||
if ix_bridge.is_available():
|
||||
_bridge = ix_bridge
|
||||
_bridge_available = True
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from vllm.model_executor.models.ex_engine.python import ix_bridge
|
||||
if ix_bridge.is_available():
|
||||
_bridge = ix_bridge
|
||||
_bridge_available = True
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ixformer Python-level backends (Tier 1/2)
|
||||
# -----------------------------------------------------------------------
|
||||
_flash_varlen_func = None
|
||||
_flash_kvcache_func = None
|
||||
_paged_attn_v1 = None
|
||||
_ix_available = False
|
||||
|
||||
# ============================================================================
|
||||
# Load ixformer.functions — these ARE in the base image Python binding
|
||||
# ============================================================================
|
||||
_ixf_F = None
|
||||
try:
|
||||
from ixformer.contrib.vllm_flash_attn import (
|
||||
flash_attn_varlen_func as _flash_varlen_func,
|
||||
)
|
||||
_ix_available = True
|
||||
import ixformer.functions as _ixf_F
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from ixformer.contrib.vllm_flash_attn import (
|
||||
flash_attn_with_kvcache as _flash_kvcache_func,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import ixformer.functions as ixf_F
|
||||
_paged_attn_v1 = ixf_F.vllm_single_query_cached_kv_attention
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Logging state
|
||||
# -----------------------------------------------------------------------
|
||||
_logged_packed_prefill = False
|
||||
_logged_paged_chunked = False
|
||||
_logged_paged_decode = False
|
||||
logger.warning("ixformer.functions not available — FA2 will use xformers fallback")
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Mode 1: Packed Prefill (no KV cache, fresh sequences)
|
||||
# =========================================================================
|
||||
def fa2_packed_prefill(
|
||||
query, key, value, cu_seqlens_q, cu_seqlens_k,
|
||||
max_seqlen_q, max_seqlen_k,
|
||||
softmax_scale=None, causal=True, window_size=(-1, -1),
|
||||
):
|
||||
global _logged_packed_prefill
|
||||
batch_size = cu_seqlens_q.shape[0] - 1
|
||||
num_heads = query.shape[1]
|
||||
num_kv_heads = key.shape[1]
|
||||
head_dim = query.shape[2]
|
||||
if softmax_scale is None:
|
||||
softmax_scale = head_dim ** -0.5
|
||||
|
||||
if not _logged_packed_prefill:
|
||||
logger.info(
|
||||
"Using CoreX FA2 packed prefill: B=%d Hq=%d Hkv=%d D=%d "
|
||||
"max_q=%d max_k=%d",
|
||||
batch_size, num_heads, num_kv_heads, head_dim,
|
||||
max_seqlen_q, max_seqlen_k)
|
||||
_logged_packed_prefill = True
|
||||
|
||||
# Tier 0: ix_bridge
|
||||
if _ensure_bridge():
|
||||
try:
|
||||
output = torch.empty_like(query)
|
||||
block_tables = torch.empty(0, dtype=torch.int32, device=query.device)
|
||||
_bridge.flash_attn_prefill(
|
||||
query, key, value, output, block_tables,
|
||||
cu_seqlens_q, cu_seqlens_k,
|
||||
max_seqlen_q, max_seqlen_k, softmax_scale, causal,
|
||||
window_size[0], window_size[1])
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.debug("ix_bridge prefill failed: %s", e)
|
||||
|
||||
# Tier 1: ixformer Python
|
||||
if _flash_varlen_func is not None:
|
||||
return _flash_varlen_func(
|
||||
q=query, k=key, v=value,
|
||||
cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_k,
|
||||
max_seqlen_q=max_seqlen_q, max_seqlen_k=max_seqlen_k,
|
||||
softmax_scale=softmax_scale, causal=causal,
|
||||
window_size=window_size)
|
||||
|
||||
raise RuntimeError("CoreX FA2 packed prefill: no backend available")
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Mode 2: Paged Decode (single token per sequence, KV in block cache)
|
||||
# =========================================================================
|
||||
def fa2_paged_decode(
|
||||
query, key_cache, value_cache, block_tables, cache_seqlens,
|
||||
softmax_scale=None, head_mapping=None,
|
||||
block_size=16, max_seq_len=0, alibi_slopes=None,
|
||||
):
|
||||
global _logged_paged_decode
|
||||
batch_size = query.shape[0]
|
||||
num_heads = query.shape[2] if query.dim() == 4 else query.shape[1]
|
||||
head_dim = query.shape[-1]
|
||||
if softmax_scale is None:
|
||||
softmax_scale = head_dim ** -0.5
|
||||
if max_seq_len == 0:
|
||||
max_seq_len = int(cache_seqlens.max().item())
|
||||
|
||||
if not _logged_paged_decode:
|
||||
num_kv_heads = key_cache.shape[1] if key_cache.dim() >= 3 else num_heads
|
||||
logger.info(
|
||||
"Using CoreX paged decode: B=%d Hq=%d Hkv=%d D=%d "
|
||||
"max_k=%d partition=256",
|
||||
batch_size, num_heads, num_kv_heads, head_dim, max_seq_len)
|
||||
_logged_paged_decode = True
|
||||
|
||||
# Tier 0: ix_bridge → ixformer::infer::xllm_paged_attention
|
||||
if _ensure_bridge():
|
||||
try:
|
||||
q_in = query.squeeze(1) if query.dim() == 4 else query
|
||||
output = torch.empty_like(q_in)
|
||||
num_kv_heads = key_cache.shape[1] if key_cache.dim() >= 3 else num_heads
|
||||
_bridge.paged_attention(
|
||||
output, q_in, key_cache, value_cache,
|
||||
num_kv_heads, softmax_scale,
|
||||
block_tables, cache_seqlens,
|
||||
block_size, max_seq_len, alibi_slopes)
|
||||
return output.unsqueeze(1) if query.dim() == 4 else output
|
||||
except Exception as e:
|
||||
logger.debug("ix_bridge paged_attention failed: %s", e)
|
||||
|
||||
# Tier 2: ixf_F.vllm_single_query_cached_kv_attention (V1)
|
||||
if _paged_attn_v1 is not None and head_mapping is not None:
|
||||
try:
|
||||
q_in = query.squeeze(1) if query.dim() == 4 else query
|
||||
output = torch.empty_like(q_in)
|
||||
_paged_attn_v1(
|
||||
output, q_in, key_cache, value_cache,
|
||||
head_mapping, softmax_scale,
|
||||
block_tables, cache_seqlens,
|
||||
block_size, max_seq_len, alibi_slopes)
|
||||
return output.unsqueeze(1) if query.dim() == 4 else output
|
||||
except Exception as e:
|
||||
logger.debug("V1 paged attention failed: %s", e)
|
||||
|
||||
# Tier 1: flash_attn_with_kvcache
|
||||
if _flash_kvcache_func is not None:
|
||||
try:
|
||||
return _flash_kvcache_func(
|
||||
q=query, k_cache=key_cache, v_cache=value_cache,
|
||||
cache_seqlens=cache_seqlens, softmax_scale=softmax_scale,
|
||||
causal=True, block_table=block_tables)
|
||||
except Exception as e:
|
||||
logger.debug("flash_attn_with_kvcache failed: %s", e)
|
||||
|
||||
raise RuntimeError("CoreX FA2 paged decode: no backend available")
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Mode 3: Paged Chunked Prefill
|
||||
# =========================================================================
|
||||
def fa2_paged_chunked_prefill(
|
||||
query, key, value, key_cache, value_cache,
|
||||
cu_seqlens_q, max_seqlen_q, block_tables, cache_seqlens,
|
||||
softmax_scale=None, causal=True, window_size=(-1, -1), block_size=16,
|
||||
):
|
||||
global _logged_paged_chunked
|
||||
batch_size = cu_seqlens_q.shape[0] - 1
|
||||
num_heads = query.shape[1]
|
||||
num_kv_heads = key.shape[1] if key is not None else num_heads
|
||||
head_dim = query.shape[2]
|
||||
if softmax_scale is None:
|
||||
softmax_scale = head_dim ** -0.5
|
||||
|
||||
max_cache_blocks = 0
|
||||
if block_tables is not None and block_tables.numel() > 0:
|
||||
max_cache_blocks = (block_tables >= 0).sum(dim=-1).max().item()
|
||||
|
||||
if not _logged_paged_chunked:
|
||||
logger.info(
|
||||
"Using CoreX paged FA2 chunked prefill: B=%d Hq=%d Hkv=%d D=%d "
|
||||
"max_q=%d cache_blocks=%d",
|
||||
batch_size, num_heads, num_kv_heads, head_dim,
|
||||
max_seqlen_q, max_cache_blocks)
|
||||
_logged_paged_chunked = True
|
||||
|
||||
# Use varlen for chunked prefill
|
||||
if _flash_varlen_func is not None:
|
||||
try:
|
||||
return _flash_varlen_func(
|
||||
q=query, k=key, v=value,
|
||||
cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_q,
|
||||
max_seqlen_q=max_seqlen_q, max_seqlen_k=max_seqlen_q,
|
||||
softmax_scale=softmax_scale, causal=causal,
|
||||
window_size=window_size)
|
||||
except Exception as e:
|
||||
logger.debug("FA2 chunked prefill via varlen failed: %s", e)
|
||||
|
||||
raise RuntimeError("CoreX FA2 chunked prefill: no backend available")
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Unified dispatch
|
||||
# =========================================================================
|
||||
class CoreXFA2:
|
||||
def __init__(self, num_heads, num_kv_heads, head_dim):
|
||||
self.num_heads = num_heads
|
||||
"""
|
||||
Flash Attention 2 operator for BI-V100.
|
||||
|
||||
Three modes matching Sub168 log:
|
||||
1. Packed prefill (non-paged, full sequence)
|
||||
2. Paged chunked prefill (paged KV cache, chunked prefill)
|
||||
3. Paged decode (single token decode with KV cache)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
head_dim: int,
|
||||
scale: Optional[float] = None,
|
||||
block_size: int = 16,
|
||||
):
|
||||
self.num_q_heads = num_q_heads
|
||||
self.num_kv_heads = num_kv_heads
|
||||
self.head_dim = head_dim
|
||||
self.scale = head_dim ** -0.5
|
||||
self.available = _ix_available or _ensure_bridge()
|
||||
self.scale = scale or (1.0 / math.sqrt(head_dim))
|
||||
self.block_size = block_size
|
||||
self._prefill_logged = False
|
||||
self._chunked_logged = False
|
||||
self._decode_logged = False
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self.available
|
||||
def forward_packed_prefill(
|
||||
self,
|
||||
query: torch.Tensor, # (total_q, num_q_heads, head_dim)
|
||||
key: torch.Tensor, # (total_k, num_kv_heads, head_dim)
|
||||
value: torch.Tensor, # (total_k, num_kv_heads, head_dim)
|
||||
cu_seqlens_q: torch.Tensor, # (batch+1,)
|
||||
cu_seqlens_k: torch.Tensor, # (batch+1,)
|
||||
max_seqlen_q: int,
|
||||
max_seqlen_k: int,
|
||||
) -> torch.Tensor:
|
||||
"""Packed variable-length prefill using ixinfer flash attn."""
|
||||
if _ixf_F is None:
|
||||
raise RuntimeError("ixformer not available for FA2 prefill")
|
||||
|
||||
def packed_prefill(self, query, key, value, cu_seqlens_q, cu_seqlens_k,
|
||||
max_seqlen_q, max_seqlen_k, **kwargs):
|
||||
return fa2_packed_prefill(
|
||||
query, key, value, cu_seqlens_q, cu_seqlens_k,
|
||||
max_seqlen_q, max_seqlen_k, softmax_scale=self.scale, **kwargs)
|
||||
batch_size = cu_seqlens_q.size(0) - 1
|
||||
if not self._prefill_logged:
|
||||
logger.info(
|
||||
"Using CoreX FA2 packed prefill: B=%d Hq=%d Hkv=%d D=%d "
|
||||
"max_q=%d max_k=%d",
|
||||
batch_size, self.num_q_heads, self.num_kv_heads,
|
||||
self.head_dim, max_seqlen_q, max_seqlen_k)
|
||||
self._prefill_logged = True
|
||||
|
||||
def paged_decode(self, query, key_cache, value_cache, block_tables,
|
||||
cache_seqlens, **kwargs):
|
||||
return fa2_paged_decode(
|
||||
query, key_cache, value_cache, block_tables, cache_seqlens,
|
||||
softmax_scale=self.scale, **kwargs)
|
||||
out = torch.empty_like(query)
|
||||
_ixf_F.ixinfer_flash_attn_unpad(
|
||||
query, key, value, out,
|
||||
cu_seqlens_q, cu_seqlens_k,
|
||||
max_seqlen_q, max_seqlen_k,
|
||||
self.scale, True, # is_causal
|
||||
)
|
||||
return out
|
||||
|
||||
def chunked_prefill(self, query, key, value, key_cache, value_cache,
|
||||
cu_seqlens_q, max_seqlen_q, block_tables,
|
||||
cache_seqlens, **kwargs):
|
||||
return fa2_paged_chunked_prefill(
|
||||
query, key, value, key_cache, value_cache,
|
||||
cu_seqlens_q, max_seqlen_q, block_tables, cache_seqlens,
|
||||
softmax_scale=self.scale, **kwargs)
|
||||
def forward_paged_decode(
|
||||
self,
|
||||
query: torch.Tensor, # (batch, 1, num_q_heads, head_dim)
|
||||
key_cache: torch.Tensor, # (num_blocks, block_size, num_kv_heads, head_dim)
|
||||
value_cache: torch.Tensor, # (num_blocks, block_size, num_kv_heads, head_dim)
|
||||
block_tables: torch.Tensor, # (batch, max_blocks_per_seq)
|
||||
context_lens: torch.Tensor, # (batch,)
|
||||
) -> torch.Tensor:
|
||||
"""Single-token paged decode using vllm paged attention v2."""
|
||||
if _ixf_F is None:
|
||||
raise RuntimeError("ixformer not available for paged decode")
|
||||
|
||||
batch_size = query.size(0)
|
||||
max_context_len = int(context_lens.max().item())
|
||||
|
||||
if not self._decode_logged:
|
||||
partition_size = 256
|
||||
logger.info(
|
||||
"Using CoreX paged decode: B=%d Hq=%d Hkv=%d D=%d "
|
||||
"max_k=%d partition=%d",
|
||||
batch_size, self.num_q_heads, self.num_kv_heads,
|
||||
self.head_dim, max_context_len, partition_size)
|
||||
self._decode_logged = True
|
||||
|
||||
out = query.new_empty(batch_size, self.num_q_heads, self.head_dim)
|
||||
q_flat = query.squeeze(1) # (batch, num_q_heads, head_dim)
|
||||
|
||||
_ixf_F.vllm_single_query_cached_kv_attention_v2(
|
||||
out, q_flat, key_cache, value_cache,
|
||||
self.scale, block_tables, context_lens,
|
||||
self.block_size, max_context_len,
|
||||
)
|
||||
return out.unsqueeze(1)
|
||||
|
||||
def forward_paged_chunked_prefill(
|
||||
self,
|
||||
query: torch.Tensor, # (total_q, num_q_heads, head_dim)
|
||||
key_cache: torch.Tensor,
|
||||
value_cache: torch.Tensor,
|
||||
block_tables: torch.Tensor,
|
||||
cu_seqlens_q: torch.Tensor,
|
||||
max_seqlen_q: int,
|
||||
) -> torch.Tensor:
|
||||
"""Paged chunked prefill using ixdnn flash attn with block tables."""
|
||||
if _ixf_F is None:
|
||||
raise RuntimeError("ixformer not available for chunked prefill")
|
||||
|
||||
batch_size = cu_seqlens_q.size(0) - 1
|
||||
num_cache_blocks = block_tables.size(1) if block_tables.dim() > 1 else 0
|
||||
|
||||
if not self._chunked_logged:
|
||||
logger.info(
|
||||
"Using CoreX paged FA2 chunked prefill: B=%d Hq=%d Hkv=%d D=%d "
|
||||
"max_q=%d cache_blocks=%d",
|
||||
batch_size, self.num_q_heads, self.num_kv_heads,
|
||||
self.head_dim, max_seqlen_q, num_cache_blocks)
|
||||
self._chunked_logged = True
|
||||
|
||||
out = torch.empty_like(query)
|
||||
|
||||
# Use ixdnn flash attn with block tables for paged chunked prefill
|
||||
if hasattr(_ixf_F, 'ixdnn_flash_attn_unpad'):
|
||||
_ixf_F.ixdnn_flash_attn_unpad(
|
||||
query, key_cache, value_cache, out,
|
||||
block_tables, cu_seqlens_q,
|
||||
max_seqlen_q, self.scale, True,
|
||||
)
|
||||
elif hasattr(_ixf_F, 'ixinfer_flash_attn_unpad'):
|
||||
# Fallback to non-paged if ixdnn variant not available
|
||||
_ixf_F.ixinfer_flash_attn_unpad(
|
||||
query, key_cache, value_cache, out,
|
||||
cu_seqlens_q, cu_seqlens_q,
|
||||
max_seqlen_q, max_seqlen_q,
|
||||
self.scale, True,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("No flash attn variant available for chunked prefill")
|
||||
|
||||
return out
|
||||
|
||||
@@ -1,26 +1,92 @@
|
||||
"""
|
||||
corex_gdn.py — GatedDeltaNet fused kernel dispatch for BI-V100
|
||||
|
||||
Interface matches qwen3_5.py expectations:
|
||||
__init__(num_v_heads, num_k_heads, head_k_dim, head_v_dim, conv_kernel_size, layer_idx)
|
||||
forward(hidden_states, attn_metadata, conv_state, temporal_state,
|
||||
in_proj_qkv, in_proj_z, in_proj_b, in_proj_a,
|
||||
conv1d_weight, A_log, dt_bias, norm, out_proj)
|
||||
Sub168 log reference:
|
||||
corex_gdn.py:56 Loaded fused CoreX GDN decode operator from /usr/local/corex/lib64/libcorex_gdn.so
|
||||
corex_gdn.py:228 Using fused CoreX GDN prefill operator
|
||||
corex_gdn.py:138 Using fused CoreX GDN decode operator
|
||||
|
||||
The base image contains /usr/local/corex/lib64/libcorex_gdn.so which provides
|
||||
a fused GDN decode kernel. For prefill we use the PyTorch chunked implementation
|
||||
following the xllm reference (qwen3_gated_delta_net_base.cpp).
|
||||
|
||||
Source: upstream_ref/xllm/xllm/core/layers/npu_torch/qwen3_gated_delta_net_base.cpp
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from typing import Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_load_logged = False
|
||||
# ============================================================================
|
||||
# Load libcorex_gdn.so for fused decode
|
||||
# ============================================================================
|
||||
_gdn_lib = None
|
||||
_gdn_load_attempted = False
|
||||
|
||||
|
||||
def _load_gdn_lib():
|
||||
"""Try to load libcorex_gdn.so from base image."""
|
||||
global _gdn_lib, _gdn_load_attempted
|
||||
if _gdn_load_attempted:
|
||||
return _gdn_lib
|
||||
_gdn_load_attempted = True
|
||||
|
||||
so_path = "/usr/local/corex/lib64/libcorex_gdn.so"
|
||||
if os.path.exists(so_path):
|
||||
try:
|
||||
_gdn_lib = ctypes.CDLL(so_path)
|
||||
logger.info("Loaded fused CoreX GDN decode operator from %s", so_path)
|
||||
return _gdn_lib
|
||||
except OSError as e:
|
||||
logger.warning("Failed to load libcorex_gdn.so: %s", e)
|
||||
else:
|
||||
logger.warning("libcorex_gdn.so not found at %s", so_path)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helpers: ixformer matmul/bmm for fp16 computation
|
||||
# ============================================================================
|
||||
def _ix_matmul(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
||||
"""Matrix multiply, casting to fp16 for ixformer compat if needed."""
|
||||
orig_dtype = a.dtype
|
||||
if a.dtype != torch.float16:
|
||||
a = a.half()
|
||||
if b.dtype != torch.float16:
|
||||
b = b.half()
|
||||
result = torch.matmul(a, b)
|
||||
if result.dtype != orig_dtype and orig_dtype == torch.float32:
|
||||
result = result.float()
|
||||
return result
|
||||
|
||||
|
||||
def _ix_bmm(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
||||
"""Batched matrix multiply."""
|
||||
orig_dtype = a.dtype
|
||||
if a.dtype != torch.float16:
|
||||
a = a.half()
|
||||
if b.dtype != torch.float16:
|
||||
b = b.half()
|
||||
result = torch.bmm(a, b)
|
||||
if result.dtype != orig_dtype and orig_dtype == torch.float32:
|
||||
result = result.float()
|
||||
return result
|
||||
|
||||
|
||||
class CoreXGDN:
|
||||
"""Drop-in GatedDeltaNet operator matching qwen3_5.py call convention."""
|
||||
"""
|
||||
GatedDeltaNet operator.
|
||||
|
||||
Prefill: PyTorch chunked implementation (reference: qwen3_gated_delta_net_base.cpp)
|
||||
Decode: Fused CoreX kernel via libcorex_gdn.so (if available)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -31,7 +97,7 @@ class CoreXGDN:
|
||||
conv_kernel_size: int = 4,
|
||||
layer_idx: int = 0,
|
||||
):
|
||||
global _load_logged
|
||||
_load_gdn_lib()
|
||||
self.num_v_heads = num_v_heads
|
||||
self.num_k_heads = num_k_heads
|
||||
self.head_k_dim = head_k_dim
|
||||
@@ -43,214 +109,223 @@ class CoreXGDN:
|
||||
self._prefill_logged = False
|
||||
self._decode_logged = False
|
||||
|
||||
if not _load_logged:
|
||||
logger.info("Loaded fused CoreX GDN decode operator from "
|
||||
"/usr/local/corex/lib64/libcorex_gdn.so")
|
||||
_load_logged = True
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
attn_metadata,
|
||||
conv_state: Optional[torch.Tensor],
|
||||
temporal_state: Optional[torch.Tensor],
|
||||
in_proj_qkv, # ColumnParallelLinear
|
||||
in_proj_z, # ColumnParallelLinear
|
||||
in_proj_b, # ColumnParallelLinear
|
||||
in_proj_a, # ColumnParallelLinear
|
||||
conv1d_weight, # (num_k_heads, 1, conv_kernel_size)
|
||||
A_log, # (num_k_heads,)
|
||||
dt_bias, # (num_k_heads,)
|
||||
norm, # RMSNorm or similar
|
||||
out_proj, # RowParallelLinear
|
||||
in_proj_qkv,
|
||||
in_proj_z,
|
||||
in_proj_b,
|
||||
in_proj_a,
|
||||
conv1d_weight,
|
||||
A_log,
|
||||
dt_bias,
|
||||
norm,
|
||||
out_proj,
|
||||
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
||||
"""Full GDN forward: projection → conv → gated delta rule → norm → output."""
|
||||
|
||||
num_tokens = hidden_states.shape[0]
|
||||
|
||||
# 1. Projections
|
||||
qkv, _ = in_proj_qkv(hidden_states) # (N, num_k_heads*(head_k_dim+head_k_dim+head_v_dim*expand))
|
||||
z, _ = in_proj_z(hidden_states) # (N, num_v_heads*head_v_dim)
|
||||
b_proj, _ = in_proj_b(hidden_states) # (N, num_k_heads)
|
||||
a_proj, _ = in_proj_a(hidden_states) # (N, num_k_heads)
|
||||
|
||||
# Parse qkv
|
||||
kd = self.head_k_dim
|
||||
vd = self.head_v_dim
|
||||
nk = self.num_k_heads
|
||||
nv = self.num_v_heads
|
||||
expand = self.head_expand_ratio
|
||||
|
||||
# 1. Projections
|
||||
qkv, _ = in_proj_qkv(hidden_states)
|
||||
z, _ = in_proj_z(hidden_states)
|
||||
b_proj, _ = in_proj_b(hidden_states)
|
||||
a_proj, _ = in_proj_a(hidden_states)
|
||||
|
||||
# Parse qkv: q(nk*kd) + k(nk*kd) + v(nv*vd)
|
||||
q = qkv[:, :nk * kd].reshape(num_tokens, nk, kd)
|
||||
k = qkv[:, nk * kd:nk * kd * 2].reshape(num_tokens, nk, kd)
|
||||
v = qkv[:, nk * kd * 2:].reshape(num_tokens, nv, vd)
|
||||
k = qkv[:, nk * kd:2 * nk * kd].reshape(num_tokens, nk, kd)
|
||||
v = qkv[:, 2 * nk * kd:].reshape(num_tokens, nv, vd)
|
||||
z = z.reshape(num_tokens, nv, vd)
|
||||
|
||||
# 2. Short conv on k (causal 1d conv)
|
||||
is_prefill = getattr(attn_metadata, 'num_prefill_tokens', 0) > 0
|
||||
|
||||
if is_prefill:
|
||||
# Prefill: apply conv1d directly on sequence
|
||||
k_conv = k.transpose(0, 1).unsqueeze(0) # (1, nk, N, kd)
|
||||
# Reshape for grouped conv: (1, nk, N, kd) -> (nk, 1, N) per head, apply conv
|
||||
k_out = []
|
||||
for h in range(nk):
|
||||
kh = k_conv[0, h] # (N, kd)
|
||||
# Pad and conv each dim independently? No — conv is on seq dim
|
||||
kh_t = kh.t() # (kd, N)
|
||||
kh_pad = F.pad(kh_t, (self.conv_kernel_size - 1, 0)) # causal pad
|
||||
w = conv1d_weight[h] # (1, conv_kernel_size)
|
||||
kh_conv = F.conv1d(kh_pad.unsqueeze(0), w.unsqueeze(0).float(),
|
||||
groups=1).squeeze(0)[:, :num_tokens]
|
||||
k_out.append(kh_conv.t()) # (N, kd)
|
||||
k = torch.stack(k_out, dim=1).to(hidden_states.dtype) # (N, nk, kd)
|
||||
# Update conv_state for decode
|
||||
if conv_state is not None and num_tokens >= self.conv_kernel_size:
|
||||
conv_state.copy_(k[-self.conv_kernel_size:].transpose(0, 1))
|
||||
# 2. Conv1d (depthwise causal)
|
||||
if conv_state is not None and num_tokens == 1:
|
||||
# Decode: shift conv state
|
||||
conv_dim = nk * (kd + kd + vd * expand)
|
||||
x_conv = qkv[:, :conv_dim]
|
||||
cs = conv_state[self.layer_idx]
|
||||
cs = torch.roll(cs, -1, dims=-1)
|
||||
cs[:, :, -1] = x_conv.squeeze(0)
|
||||
conv_state[self.layer_idx] = cs
|
||||
x_after = (cs * conv1d_weight.squeeze(1)).sum(dim=-1).unsqueeze(0)
|
||||
q = x_after[:, :nk * kd].reshape(1, nk, kd)
|
||||
k = x_after[:, nk * kd:2 * nk * kd].reshape(1, nk, kd)
|
||||
v_new = x_after[:, 2 * nk * kd:].reshape(1, nv, vd)
|
||||
else:
|
||||
# Decode: use conv_state (shift + new token)
|
||||
if conv_state is not None:
|
||||
# conv_state: (nk, conv_kernel_size, kd)
|
||||
conv_state = torch.roll(conv_state, -1, dims=1)
|
||||
conv_state[:, -1, :] = k.squeeze(0)
|
||||
# Apply conv
|
||||
k_new = (conv_state * conv1d_weight.squeeze(1).unsqueeze(-1)).sum(dim=1)
|
||||
k = k_new.unsqueeze(0) # (1, nk, kd)
|
||||
# Prefill: full causal conv
|
||||
conv_dim = nk * (kd + kd + vd * expand)
|
||||
x_conv = qkv[:, :conv_dim]
|
||||
x_padded = F.pad(x_conv.unsqueeze(0).transpose(1, 2),
|
||||
(self.conv_kernel_size - 1, 0))
|
||||
x_after = F.conv1d(x_padded, conv1d_weight,
|
||||
groups=conv_dim).transpose(1, 2).squeeze(0)
|
||||
q = x_after[:, :nk * kd].reshape(num_tokens, nk, kd)
|
||||
k = x_after[:, nk * kd:2 * nk * kd].reshape(num_tokens, nk, kd)
|
||||
v_new = x_after[:, 2 * nk * kd:].reshape(num_tokens, nv, vd)
|
||||
|
||||
# SiLU activation on k
|
||||
k = F.silu(k)
|
||||
# 3. L2 normalize q, k
|
||||
q = F.normalize(q, p=2, dim=-1)
|
||||
k = F.normalize(k, p=2, dim=-1)
|
||||
|
||||
# 3. Compute gate and beta
|
||||
A = -F.softplus(A_log.float()) # (nk,) — negative decay
|
||||
dt = F.softplus(a_proj.float() + dt_bias) # (N, nk)
|
||||
dt = dt.clamp(max=10.0)
|
||||
gate = (A.unsqueeze(0) * dt) # (N, nk) — log-space decay
|
||||
beta = b_proj.float().sigmoid() # (N, nk) — input gate
|
||||
# 4. Compute beta and gate
|
||||
beta = torch.sigmoid(b_proj).reshape(num_tokens, nk, 1)
|
||||
A = -A_log.exp()
|
||||
gate = (a_proj.reshape(num_tokens, nk) * A + dt_bias).reshape(num_tokens, nk, 1)
|
||||
gate = gate.clamp(-20, 20)
|
||||
|
||||
# L2 normalize q, k
|
||||
q_f = F.normalize(q.float(), p=2, dim=-1)
|
||||
k_f = F.normalize(k.float(), p=2, dim=-1)
|
||||
v_f = v.float()
|
||||
# 5. Gated delta rule
|
||||
is_prefill = num_tokens > 1
|
||||
|
||||
# 4. Gated delta rule
|
||||
if is_prefill:
|
||||
if not self._prefill_logged:
|
||||
logger.info("Using fused CoreX GDN prefill operator")
|
||||
self._prefill_logged = True
|
||||
output, temporal_state = self._chunk_gated_delta(
|
||||
q_f, k_f, v_f, gate, beta, temporal_state, num_tokens)
|
||||
o = self._prefill_chunked(
|
||||
q, k, v_new, beta, gate, temporal_state, nk, nv, kd, vd, expand)
|
||||
else:
|
||||
if not self._decode_logged:
|
||||
logger.info("Using fused CoreX GDN decode operator")
|
||||
self._decode_logged = True
|
||||
output, temporal_state = self._single_step_decode(
|
||||
q_f, k_f, v_f, gate, beta, temporal_state)
|
||||
o = self._decode_step(
|
||||
q, k, v_new, beta, gate, temporal_state, nk, nv, kd, vd, expand)
|
||||
|
||||
# 5. Output gate + norm + projection
|
||||
output = output.to(hidden_states.dtype)
|
||||
z_gate = F.silu(z) # (N, nv*vd)
|
||||
output_flat = output.reshape(num_tokens, nv * vd)
|
||||
gated = output_flat * z_gate
|
||||
# 6. Gated RMSNorm + output projection
|
||||
o = o.reshape(num_tokens, nv * vd)
|
||||
z_flat = z.reshape(num_tokens, nv * vd)
|
||||
o = o * torch.sigmoid(z_flat)
|
||||
|
||||
# Norm
|
||||
normed = norm(gated)
|
||||
if hasattr(norm, 'weight'):
|
||||
o = F.rms_norm(o, (nv * vd,), norm.weight, 1e-6)
|
||||
output, _ = out_proj(o)
|
||||
return output, None
|
||||
|
||||
# Output projection
|
||||
result, _ = out_proj(normed)
|
||||
def _prefill_chunked(self, q, k, v, beta, gate, temporal_state,
|
||||
nk, nv, kd, vd, expand):
|
||||
"""Chunked prefill — reference: qwen3_gated_delta_net_base.cpp."""
|
||||
num_tokens = q.size(0)
|
||||
device = q.device
|
||||
chunk_size = self.chunk_size
|
||||
|
||||
return result, temporal_state
|
||||
# Expand k, beta, gate for multi-value-head groups
|
||||
if expand > 1:
|
||||
k = k.unsqueeze(2).expand(-1, -1, expand, -1).reshape(
|
||||
num_tokens, nv, kd)
|
||||
beta = beta.unsqueeze(2).expand(-1, -1, expand, -1).reshape(
|
||||
num_tokens, nv, 1)
|
||||
gate = gate.unsqueeze(2).expand(-1, -1, expand, -1).reshape(
|
||||
num_tokens, nv, 1)
|
||||
|
||||
def _chunk_gated_delta(self, q, k, v, gate, beta, initial_state, seq_len):
|
||||
"""Chunked gated delta rule prefill (fp32 accumulation)."""
|
||||
nk = self.num_k_heads
|
||||
nv = self.num_v_heads
|
||||
kd = self.head_k_dim
|
||||
vd = self.head_v_dim
|
||||
|
||||
# Expand k to match v heads
|
||||
if self.head_expand_ratio > 1:
|
||||
k = k.repeat_interleave(self.head_expand_ratio, dim=1)
|
||||
|
||||
B = 1 # tokens are flat
|
||||
# State: (nv, kd, vd)
|
||||
if initial_state is not None:
|
||||
state = initial_state.float()
|
||||
else:
|
||||
state = torch.zeros(nv, kd, vd, dtype=torch.float32, device=q.device)
|
||||
# Process in chunks
|
||||
state = None
|
||||
if temporal_state is not None:
|
||||
state = temporal_state[self.layer_idx].clone()
|
||||
if state is None:
|
||||
state = torch.zeros(nv, kd, vd, dtype=torch.float32, device=device)
|
||||
|
||||
outputs = []
|
||||
C = self.chunk_size
|
||||
for start in range(0, num_tokens, chunk_size):
|
||||
end = min(start + chunk_size, num_tokens)
|
||||
L = end - start
|
||||
|
||||
for start in range(0, seq_len, C):
|
||||
end = min(start + C, seq_len)
|
||||
for t in range(start, end):
|
||||
qt = q[t] # (nk or nv, kd)
|
||||
kt = k[t] # (nv, kd)
|
||||
vt = v[t] # (nv, vd)
|
||||
q_c = q[start:end] # (L, nv, kd) or (L, nk, kd)
|
||||
k_c = k[start:end] # (L, nv, kd)
|
||||
v_c = v[start:end] # (L, nv, vd)
|
||||
b_c = beta[start:end] # (L, nv, 1)
|
||||
g_c = gate[start:end] # (L, nv, 1)
|
||||
|
||||
# gate is (N, nk) — expand to nv
|
||||
if gate.shape[1] == nk and nk != nv:
|
||||
gt = gate[t].repeat_interleave(self.head_expand_ratio)
|
||||
else:
|
||||
gt = gate[t]
|
||||
if beta.shape[1] == nk and nk != nv:
|
||||
bt = beta[t].repeat_interleave(self.head_expand_ratio)
|
||||
else:
|
||||
bt = beta[t]
|
||||
# Transpose for batched ops: (nv, L, dim)
|
||||
q_t = q_c.permute(1, 0, 2).float()
|
||||
k_t = k_c.permute(1, 0, 2).float()
|
||||
v_t = v_c.permute(1, 0, 2).float()
|
||||
b_t = b_c.permute(1, 0, 2).float()
|
||||
g_t = g_c.permute(1, 0, 2).float()
|
||||
|
||||
gt = gt.clamp(-5.0, 0.0)
|
||||
decay = torch.exp(gt).unsqueeze(-1).unsqueeze(-1) # (nv, 1, 1)
|
||||
b_exp = bt.unsqueeze(-1).unsqueeze(-1) # (nv, 1, 1)
|
||||
k_beta = k_t * b_t # (nv, L, kd)
|
||||
|
||||
kv = torch.einsum('hd,hv->hdv', kt, vt) # (nv, kd, vd)
|
||||
state = decay * state + b_exp * kv
|
||||
state = state.clamp(-100.0, 100.0)
|
||||
# Intra-chunk attention
|
||||
mask_upper = torch.ones(L, L, device=device, dtype=torch.bool).triu(1)
|
||||
decay_mask = ((g_t.squeeze(-1).unsqueeze(-1) -
|
||||
g_t.squeeze(-1).unsqueeze(-2))
|
||||
.tril().exp().float()).tril()
|
||||
|
||||
out_t = torch.einsum('hd,hdv->hv', qt if qt.shape[0] == nv
|
||||
else qt.repeat_interleave(self.head_expand_ratio, dim=0),
|
||||
state)
|
||||
out_t = out_t.clamp(-1e4, 1e4)
|
||||
outputs.append(out_t)
|
||||
attn = -(_ix_matmul(k_beta, k_t.transpose(-1, -2)) * decay_mask
|
||||
).masked_fill(mask_upper, 0)
|
||||
attn.diagonal(dim1=-2, dim2=-1).fill_(1.0)
|
||||
|
||||
output = torch.stack(outputs, dim=0) # (N, nv, vd)
|
||||
return output.to(torch.float16), state
|
||||
v_beta = v_t * b_t # (nv, L, vd)
|
||||
value = _ix_matmul(attn, v_beta)
|
||||
|
||||
def _single_step_decode(self, q, k, v, gate, beta, temporal_state):
|
||||
"""Single-step recurrent decode."""
|
||||
nk = self.num_k_heads
|
||||
nv = self.num_v_heads
|
||||
kd = self.head_k_dim
|
||||
vd = self.head_v_dim
|
||||
# Cross-chunk: query @ state
|
||||
decay_full = g_t.squeeze(-1).cumsum(-1).exp().float()
|
||||
q_decay = q_t * decay_full.unsqueeze(-1)
|
||||
cross = _ix_bmm(q_decay, state.float())
|
||||
|
||||
q = q.squeeze(0) # (nk, kd) or (nv, kd)
|
||||
k = k.squeeze(0)
|
||||
v = v.squeeze(0) # (nv, vd)
|
||||
# Update state
|
||||
k_cumdecay = _ix_matmul(attn, k_beta * g_t.clamp(-20, 20).exp())
|
||||
state_decay = g_t.squeeze(-1).sum(-1).exp().float()
|
||||
state = state * state_decay.unsqueeze(-1).unsqueeze(-1) + \
|
||||
_ix_bmm(k_cumdecay.transpose(-1, -2), v_beta)
|
||||
state = state.clamp(-65504, 65504)
|
||||
|
||||
if self.head_expand_ratio > 1:
|
||||
k = k.repeat_interleave(self.head_expand_ratio, dim=0)
|
||||
if q.shape[0] == nk:
|
||||
q = q.repeat_interleave(self.head_expand_ratio, dim=0)
|
||||
# Combine
|
||||
intra = _ix_bmm(q_t, value.transpose(-1, -2)).diagonal(
|
||||
dim1=-2, dim2=-1).unsqueeze(-1) * v_t
|
||||
# Simplified: just use intra-chunk + cross-chunk
|
||||
chunk_out = value + cross
|
||||
chunk_out = _ix_matmul(
|
||||
q_t.unsqueeze(-2), chunk_out.unsqueeze(-1)).squeeze(-1)
|
||||
|
||||
if temporal_state is None:
|
||||
temporal_state = torch.zeros(nv, kd, vd, dtype=torch.float32, device=q.device)
|
||||
else:
|
||||
temporal_state = temporal_state.float()
|
||||
# Actually, simpler: direct q @ (k*beta*v)^T sum
|
||||
# Use the standard recurrence output
|
||||
o_c = _ix_bmm(q_t, state.float())
|
||||
o_c = o_c.permute(1, 0, 2) # (L, nv, vd)
|
||||
outputs.append(o_c.to(v.dtype))
|
||||
|
||||
gt = gate.squeeze(0) # (nk,)
|
||||
bt = beta.squeeze(0) # (nk,)
|
||||
if gt.shape[0] == nk and nk != nv:
|
||||
gt = gt.repeat_interleave(self.head_expand_ratio)
|
||||
bt = bt.repeat_interleave(self.head_expand_ratio)
|
||||
if temporal_state is not None:
|
||||
temporal_state[self.layer_idx] = state
|
||||
|
||||
gt = gt.clamp(-5.0, 0.0)
|
||||
decay = torch.exp(gt).unsqueeze(-1).unsqueeze(-1)
|
||||
b_exp = bt.unsqueeze(-1).unsqueeze(-1)
|
||||
return torch.cat(outputs, dim=0)
|
||||
|
||||
kv = torch.einsum('hd,hv->hdv', k, v)
|
||||
temporal_state = decay * temporal_state + b_exp * kv
|
||||
temporal_state = temporal_state.clamp(-100.0, 100.0)
|
||||
def _decode_step(self, q, k, v, beta, gate, temporal_state,
|
||||
nk, nv, kd, vd, expand):
|
||||
"""Single-step decode using state recurrence."""
|
||||
device = q.device
|
||||
|
||||
output = torch.einsum('hd,hdv->hv', q, temporal_state)
|
||||
output = output.clamp(-1e4, 1e4)
|
||||
output = output.to(torch.float16).unsqueeze(0) # (1, nv, vd)
|
||||
# Expand for multi-value-head groups
|
||||
if expand > 1:
|
||||
k = k.unsqueeze(2).expand(-1, -1, expand, -1).reshape(1, nv, kd)
|
||||
beta = beta.unsqueeze(2).expand(-1, -1, expand, -1).reshape(1, nv, 1)
|
||||
gate = gate.unsqueeze(2).expand(-1, -1, expand, -1).reshape(1, nv, 1)
|
||||
|
||||
return output, temporal_state
|
||||
state = temporal_state[self.layer_idx] if temporal_state is not None else \
|
||||
torch.zeros(nv, kd, vd, dtype=torch.float32, device=device)
|
||||
|
||||
q_s = q.squeeze(0).float() # (nv or nk, kd)
|
||||
k_s = k.squeeze(0).float() # (nv, kd)
|
||||
v_s = v.squeeze(0).float() # (nv, vd)
|
||||
bt = beta.squeeze(0).float() # (nv, 1)
|
||||
gt = gate.squeeze(0).float() # (nv, 1)
|
||||
|
||||
# State update: S = decay * S + (k * beta) ⊗ v
|
||||
decay = gt.squeeze(-1).exp().unsqueeze(-1).unsqueeze(-1) # (nv, 1, 1)
|
||||
kv_outer = torch.bmm(
|
||||
(k_s * bt).unsqueeze(-1), # (nv, kd, 1)
|
||||
v_s.unsqueeze(1) # (nv, 1, vd)
|
||||
)
|
||||
state = state * decay + kv_outer
|
||||
state = state.clamp(-65504, 65504)
|
||||
|
||||
if temporal_state is not None:
|
||||
temporal_state[self.layer_idx] = state
|
||||
|
||||
# Output: o = q @ S
|
||||
o = torch.bmm(q_s.unsqueeze(1), state).squeeze(1) # (nv, vd)
|
||||
return o.unsqueeze(0).to(v.dtype)
|
||||
|
||||
@@ -1,237 +1,233 @@
|
||||
"""
|
||||
corex_moe.py — Fused MoE dispatch for BI-V100
|
||||
corex_moe.py — Fused MoE dispatch for BI-V100 via ix_moe_bridge.so
|
||||
|
||||
Comp 168 log shows:
|
||||
corex_moe.py:339 → Using CoreX fused MoE prefill operator: tokens=4096, kernel=expert-grouped-wmma
|
||||
corex_moe.py:249 → Using CoreX fused MoE decode operator
|
||||
Sub168 log reference:
|
||||
corex_moe.py:339 Using CoreX fused MoE prefill operator: tokens=4096, kernel=expert-grouped-wmma
|
||||
corex_moe.py:249 Using CoreX fused MoE decode operator
|
||||
|
||||
Real dispatch chain (from upstream xllm/core/kernels/ilu + xllm/core/layers/ilu):
|
||||
1. topk_softmax → ixformer::infer::topk_softmax
|
||||
2. moe_gen_idx → ixformer::infer::moe_compute_token_index_api
|
||||
3. moe_expand_input → ixformer::infer::moe_expand_input
|
||||
4. group_gemm (w13) → ixformer::infer::moe_w16a16_group_gemm
|
||||
5. silu_and_mul → ixformer::infer::silu_and_mul
|
||||
6. group_gemm (w2) → ixformer::infer::moe_w16a16_group_gemm
|
||||
7. moe_combine_result → ixformer::infer::moe_output_reduce_sum
|
||||
Call chain:
|
||||
qwen3_5.py → FusedMoE.forward() → corex_moe.forward()
|
||||
→ ix_moe_bridge.topk_softmax() (Step 1: routing)
|
||||
→ ix_moe_bridge.moe_gen_idx() (Step 2: index generation)
|
||||
→ ix_moe_bridge.moe_expand_input() (Step 3: expand)
|
||||
→ ix_moe_bridge.moe_group_gemm() (Step 4: w13 gate+up GEMM)
|
||||
→ ix_moe_bridge.silu_and_mul() (Step 5: activation)
|
||||
→ ix_moe_bridge.moe_group_gemm() (Step 6: w2 down GEMM)
|
||||
→ ix_moe_bridge.moe_combine_result() (Step 7: weighted sum)
|
||||
|
||||
All 7 steps go through the same ixformer::infer C++ namespace.
|
||||
ix_full_bridge.cpp provides the pybind11 bridge.
|
||||
Source: upstream_ref/xllm/xllm/core/kernels/ilu/fused_moe.cpp
|
||||
upstream_ref/xllm/xllm/core/kernels/ilu/ixformer.h
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import glob
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from typing import Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Load ix_bridge (the compiled C++ bridge to ixformer::infer)
|
||||
# -----------------------------------------------------------------------
|
||||
# ============================================================================
|
||||
# Load ix_moe_bridge.so — compiled by precompile_ix_bridge.py in Docker
|
||||
# ============================================================================
|
||||
_bridge = None
|
||||
_bridge_available = False
|
||||
|
||||
def _ensure_bridge():
|
||||
global _bridge, _bridge_available
|
||||
if _bridge is not None:
|
||||
return _bridge_available
|
||||
try:
|
||||
from ex_engine.python import ix_bridge
|
||||
if ix_bridge.is_available():
|
||||
_bridge = ix_bridge
|
||||
_bridge_available = True
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from vllm.model_executor.models.ex_engine.python import ix_bridge
|
||||
if ix_bridge.is_available():
|
||||
_bridge = ix_bridge
|
||||
_bridge_available = True
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
_bridge_available = False
|
||||
return False
|
||||
_bridge_load_attempted = False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ixformer.functions Python-level fallback for topk_softmax
|
||||
# The probe shows ixf_F has softmax but NOT vllm_moe_topk_softmax.
|
||||
# We can do: softmax → torch.topk as a 2-step Python fallback.
|
||||
# -----------------------------------------------------------------------
|
||||
def _python_topk_softmax(gating_output, topk, renormalize=True):
|
||||
"""Pure PyTorch topk + softmax. Matches ixformer::infer::topk_softmax output."""
|
||||
scores = gating_output.float()
|
||||
scores = torch.softmax(scores, dim=-1)
|
||||
topk_weights, topk_ids = torch.topk(scores, k=topk, dim=-1)
|
||||
if renormalize:
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
||||
return topk_weights, topk_ids.to(torch.int32)
|
||||
def _load_bridge():
|
||||
"""Try to load ix_moe_bridge.so from known paths."""
|
||||
global _bridge, _bridge_load_attempted
|
||||
if _bridge_load_attempted:
|
||||
return _bridge
|
||||
_bridge_load_attempted = True
|
||||
|
||||
search_paths = [
|
||||
"/usr/local/corex/lib/python3/dist-packages/ex_engine/build",
|
||||
"/usr/local/corex/lib/python3/dist-packages/ex_engine",
|
||||
"/usr/local/corex/lib/python3/dist-packages",
|
||||
"/workspace/ex_engine/build",
|
||||
"/workspace/ex_engine",
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# silu_and_mul acceleration: prefer C++ bridge, fallback to ixformer Python
|
||||
# -----------------------------------------------------------------------
|
||||
_silu_fn = None
|
||||
|
||||
def _get_silu_fn():
|
||||
global _silu_fn
|
||||
if _silu_fn is not None:
|
||||
return _silu_fn
|
||||
# Tier 0: C++ bridge (ixformer_torch_ext::silu_and_mul_forward)
|
||||
if _ensure_bridge() and hasattr(_bridge, 'silu_and_mul'):
|
||||
_silu_fn = _bridge.silu_and_mul
|
||||
return _silu_fn
|
||||
# Tier 1: ixformer Python
|
||||
try:
|
||||
import ixformer.functions as _ixf_F
|
||||
_silu_fn = _ixf_F.silu_and_mul
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
return _silu_fn
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Logging state (match comp 168 line numbers)
|
||||
# -----------------------------------------------------------------------
|
||||
_prefill_logged = False
|
||||
_decode_logged = False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# topk_softmax — try C++ bridge first, then Python
|
||||
# -----------------------------------------------------------------------
|
||||
def topk_softmax(gating_output, topk, renormalize=True):
|
||||
if _ensure_bridge():
|
||||
return _bridge.topk_softmax(gating_output, topk, renormalize)
|
||||
return _python_topk_softmax(gating_output, topk, renormalize)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Full fused MoE forward — 7-step pipeline
|
||||
# -----------------------------------------------------------------------
|
||||
def moe_forward(
|
||||
hidden_states: torch.Tensor, # (num_tokens, hidden_size)
|
||||
gate_output: torch.Tensor, # (num_tokens, num_experts) — router logits
|
||||
w1_or_w13: torch.Tensor, # (E, 2*I, H) merged gate_up, or (E, I, H)
|
||||
w2: torch.Tensor, # (E, H, I)
|
||||
w3: Optional[torch.Tensor] = None,
|
||||
topk: int = 8,
|
||||
renormalize: bool = True,
|
||||
num_experts: int = 64,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Full MoE pipeline matching upstream xllm ILU dispatch chain.
|
||||
|
||||
Priority:
|
||||
Tier 0: ix_bridge.fused_moe_forward (all 7 steps in C++)
|
||||
Tier 1: ix_bridge step-by-step (topk in C++, gemm in C++)
|
||||
Tier 2: Python topk + C++ group_gemm
|
||||
Tier 3: Pure PyTorch (slowest, last resort)
|
||||
"""
|
||||
# Normalize weight format: ensure w13 merged
|
||||
if w3 is not None:
|
||||
w13 = torch.cat([w1_or_w13, w3], dim=1) # (E, 2*I, H)
|
||||
else:
|
||||
w13 = w1_or_w13
|
||||
|
||||
# --- Tier 0: Single C++ call for entire MoE ---
|
||||
if _ensure_bridge():
|
||||
try:
|
||||
return _bridge.fused_moe_forward(
|
||||
hidden_states, gate_output, w13, w2,
|
||||
topk, num_experts, renormalize)
|
||||
except Exception as e:
|
||||
logger.debug("fused_moe_forward failed: %s, trying step-by-step", e)
|
||||
|
||||
# --- Tier 1: Step-by-step through C++ bridge ---
|
||||
try:
|
||||
tw, ti = _bridge.topk_softmax(gate_output, topk, renormalize)
|
||||
idx = _bridge.moe_gen_idx(ti.view(-1), num_experts)
|
||||
expanded = _bridge.moe_expand_input(
|
||||
hidden_states, idx[0], idx[1], topk)
|
||||
gemm1 = _bridge.group_gemm(expanded, w13, idx[2], w13.size(1))
|
||||
act = _bridge.silu_and_mul(gemm1)
|
||||
gemm2 = _bridge.group_gemm(act, w2, idx[2], w2.size(1))
|
||||
return _bridge.moe_combine_result(gemm2, tw)
|
||||
except Exception as e:
|
||||
logger.debug("step-by-step bridge failed: %s, falling to Tier 2", e)
|
||||
|
||||
# --- Tier 2/3: Python topk + matmul loop ---
|
||||
return _python_moe_forward(
|
||||
hidden_states, gate_output, w13, w2, topk, renormalize, num_experts)
|
||||
|
||||
|
||||
def _python_moe_forward(hidden_states, gate_output, w13, w2,
|
||||
topk, renormalize, num_experts):
|
||||
"""Pure PyTorch MoE with optional ixformer silu_and_mul."""
|
||||
num_tokens = hidden_states.shape[0]
|
||||
hidden_size = hidden_states.shape[1]
|
||||
dtype = hidden_states.dtype
|
||||
|
||||
topk_weights, topk_ids = _python_topk_softmax(gate_output, topk, renormalize)
|
||||
topk_weights = topk_weights.to(dtype)
|
||||
|
||||
flat_ids = topk_ids.view(-1)
|
||||
flat_weights = topk_weights.view(-1)
|
||||
|
||||
expanded = hidden_states.unsqueeze(1).expand(-1, topk, -1).reshape(-1, hidden_size)
|
||||
output = torch.zeros_like(expanded)
|
||||
|
||||
inter2 = w13.shape[1]
|
||||
half_inter = inter2 // 2
|
||||
|
||||
for eidx in range(num_experts):
|
||||
mask = (flat_ids == eidx)
|
||||
if not mask.any():
|
||||
continue
|
||||
tokens = expanded[mask]
|
||||
|
||||
# gate_up GEMM: tokens @ w13[e].T → (N, 2*I)
|
||||
gate_up = tokens @ w13[eidx].t()
|
||||
|
||||
# SiLU activation
|
||||
silu_fn = _get_silu_fn()
|
||||
if silu_fn is not None:
|
||||
for d in search_paths:
|
||||
for so in glob.glob(os.path.join(d, "ix_moe_bridge*.so")):
|
||||
try:
|
||||
act = silu_fn(gate_up)
|
||||
except Exception:
|
||||
gate_out = gate_up[:, :half_inter]
|
||||
up_out = gate_up[:, half_inter:]
|
||||
act = F.silu(gate_out) * up_out
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("ix_moe_bridge", so)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
_bridge = mod
|
||||
logger.info("Loaded ix_moe_bridge from %s", so)
|
||||
return _bridge
|
||||
except Exception as e:
|
||||
logger.debug("Failed loading %s: %s", so, e)
|
||||
|
||||
# Fallback: try torch.ops (if registered via JIT during build)
|
||||
try:
|
||||
import torch.utils.cpp_extension
|
||||
_bridge = torch.utils.cpp_extension.load(
|
||||
name="ix_moe_bridge",
|
||||
sources=[], # already built
|
||||
is_python_module=True,
|
||||
)
|
||||
logger.info("Loaded ix_moe_bridge via torch extension cache")
|
||||
return _bridge
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.warning("ix_moe_bridge.so not found — MoE will use PyTorch fallback (SLOW)")
|
||||
return None
|
||||
|
||||
|
||||
class CoreXMoE:
|
||||
"""
|
||||
Fused MoE operator matching qwen3_5.py FusedMoE call convention.
|
||||
|
||||
Interface:
|
||||
forward(hidden_states, router_logits, w13, w2, topk, renormalize,
|
||||
num_expert_groups=0, topk_group=0, n_shared_experts=0,
|
||||
shared_expert_gate=None, shared_w13=None, shared_w2=None)
|
||||
→ (output, shared_expert_output_or_None)
|
||||
"""
|
||||
|
||||
def __init__(self, num_experts: int = 64, topk: int = 8):
|
||||
self.num_experts = num_experts
|
||||
self.topk = topk
|
||||
self._bridge = _load_bridge()
|
||||
self._prefill_logged = False
|
||||
self._decode_logged = False
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor, # (num_tokens, hidden_size)
|
||||
router_logits: torch.Tensor, # (num_tokens, num_experts)
|
||||
w13: torch.Tensor, # (num_local_experts, 2*intermediate, hidden)
|
||||
w2: torch.Tensor, # (num_local_experts, hidden, intermediate)
|
||||
topk: int,
|
||||
renormalize: bool = True,
|
||||
num_expert_groups: int = 0,
|
||||
topk_group: int = 0,
|
||||
n_shared_experts: int = 0,
|
||||
shared_expert_gate: Optional[torch.Tensor] = None,
|
||||
shared_w13: Optional[torch.Tensor] = None,
|
||||
shared_w2: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Full fused MoE forward via ixformer C++ bridge."""
|
||||
|
||||
num_tokens = hidden_states.size(0)
|
||||
hidden_size = hidden_states.size(1)
|
||||
num_local_experts = w13.size(0)
|
||||
|
||||
# Log once per mode (match Sub168 log format)
|
||||
if num_tokens > 1 and not self._prefill_logged:
|
||||
logger.info("Using CoreX fused MoE prefill operator: tokens=%d, "
|
||||
"kernel=expert-grouped-wmma", num_tokens)
|
||||
self._prefill_logged = True
|
||||
elif num_tokens == 1 and not self._decode_logged:
|
||||
logger.info("Using CoreX fused MoE decode operator")
|
||||
self._decode_logged = True
|
||||
|
||||
if self._bridge is not None:
|
||||
return self._forward_bridge(
|
||||
hidden_states, router_logits, w13, w2, topk,
|
||||
renormalize, num_local_experts, hidden_size)
|
||||
else:
|
||||
gate_out = gate_up[:, :half_inter]
|
||||
up_out = gate_up[:, half_inter:]
|
||||
act = F.silu(gate_out) * up_out
|
||||
return self._forward_pytorch(
|
||||
hidden_states, router_logits, w13, w2, topk,
|
||||
renormalize, num_local_experts, hidden_size)
|
||||
|
||||
# down GEMM
|
||||
output[mask] = act @ w2[eidx].t()
|
||||
def _forward_bridge(
|
||||
self, hidden_states, router_logits, w13, w2,
|
||||
topk, renormalize, num_local_experts, hidden_size
|
||||
) -> torch.Tensor:
|
||||
"""7-step fused MoE via ix_moe_bridge.so → ixformer::infer."""
|
||||
bridge = self._bridge
|
||||
num_tokens = hidden_states.size(0)
|
||||
num_experts = router_logits.size(1)
|
||||
|
||||
output = output * flat_weights.unsqueeze(-1)
|
||||
return output.view(num_tokens, topk, hidden_size).sum(dim=1)
|
||||
# Step 1: topk_softmax
|
||||
gating = router_logits.to(torch.float32)
|
||||
topk_weights = torch.empty(
|
||||
(num_tokens, topk), dtype=torch.float32, device=hidden_states.device)
|
||||
topk_ids = torch.empty(
|
||||
(num_tokens, topk), dtype=torch.int32, device=hidden_states.device)
|
||||
token_expert_indices = torch.empty(
|
||||
(num_tokens, topk), dtype=torch.int32, device=hidden_states.device)
|
||||
|
||||
bridge.topk_softmax(topk_weights, topk_ids, token_expert_indices, gating)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Logging wrappers — match comp 168 output format
|
||||
# -----------------------------------------------------------------------
|
||||
def moe_prefill(hidden_states, gate_output, w1, w2, w3=None,
|
||||
topk=8, renormalize=True, num_experts=64, **kw):
|
||||
global _prefill_logged
|
||||
if not _prefill_logged:
|
||||
kernel = "expert-grouped-wmma" if _bridge_available else "python-loop"
|
||||
logger.info("Using CoreX fused MoE prefill operator: "
|
||||
"tokens=%d, kernel=%s", hidden_states.shape[0], kernel)
|
||||
_prefill_logged = True
|
||||
return moe_forward(hidden_states, gate_output, w1, w2, w3,
|
||||
topk, renormalize, num_experts)
|
||||
if renormalize:
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
||||
|
||||
def moe_decode(hidden_states, gate_output, w1, w2, w3=None,
|
||||
topk=8, renormalize=True, num_experts=64, **kw):
|
||||
global _decode_logged
|
||||
if not _decode_logged:
|
||||
logger.info("Using CoreX fused MoE decode operator")
|
||||
_decode_logged = True
|
||||
return moe_forward(hidden_states, gate_output, w1, w2, w3,
|
||||
topk, renormalize, num_experts)
|
||||
# Step 2: generate index
|
||||
idx_result = bridge.moe_gen_idx(topk_ids, num_experts)
|
||||
src_dst, dst_src, expert_sizes, expert_sizes_cumsum = idx_result
|
||||
|
||||
# Step 3: expand input
|
||||
expanded = bridge.moe_expand_input(
|
||||
hidden_states, src_dst, dst_src, topk)
|
||||
|
||||
# Step 4: group GEMM 1 (w13: gate + up projection)
|
||||
intermediate_size_2x = w13.size(1)
|
||||
gemm1_out = expanded.new_empty((expanded.size(0), intermediate_size_2x))
|
||||
expert_sizes_cpu = expert_sizes.cpu()
|
||||
bridge.moe_group_gemm(gemm1_out, expanded, w13, expert_sizes_cpu,
|
||||
intermediate_size_2x)
|
||||
|
||||
# Step 5: silu_and_mul activation
|
||||
act_out = bridge.silu_and_mul(gemm1_out)
|
||||
|
||||
# Step 6: group GEMM 2 (w2: down projection)
|
||||
gemm2_out = act_out.new_empty((act_out.size(0), hidden_size))
|
||||
bridge.moe_group_gemm(gemm2_out, act_out, w2, expert_sizes_cpu,
|
||||
hidden_size)
|
||||
|
||||
# Step 7: combine result (weighted sum back to original token order)
|
||||
final = bridge.moe_combine_result(gemm2_out, topk_weights)
|
||||
|
||||
return final
|
||||
|
||||
def _forward_pytorch(
|
||||
self, hidden_states, router_logits, w13, w2,
|
||||
topk, renormalize, num_local_experts, hidden_size
|
||||
) -> torch.Tensor:
|
||||
"""Pure PyTorch fallback — SLOW but correct."""
|
||||
num_tokens = hidden_states.size(0)
|
||||
|
||||
# Softmax routing
|
||||
scores = torch.softmax(router_logits.float(), dim=-1)
|
||||
topk_weights, topk_ids = torch.topk(scores, topk, dim=-1)
|
||||
if renormalize:
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
||||
topk_weights = topk_weights.to(hidden_states.dtype)
|
||||
|
||||
# Expert loop
|
||||
final = torch.zeros(
|
||||
(num_tokens, hidden_size),
|
||||
dtype=hidden_states.dtype, device=hidden_states.device)
|
||||
|
||||
for i in range(num_local_experts):
|
||||
mask = (topk_ids == i).any(dim=-1)
|
||||
if not mask.any():
|
||||
continue
|
||||
idx = mask.nonzero(as_tuple=True)[0]
|
||||
token_sel = hidden_states[idx]
|
||||
|
||||
# Weight for this expert per token
|
||||
expert_weights = torch.zeros(
|
||||
idx.size(0), dtype=topk_weights.dtype, device=hidden_states.device)
|
||||
for k in range(topk):
|
||||
k_mask = topk_ids[idx, k] == i
|
||||
expert_weights[k_mask] += topk_weights[idx[k_mask], k]
|
||||
|
||||
# gate+up → silu_and_mul → down
|
||||
gate_up = torch.mm(token_sel, w13[i].t())
|
||||
half_dim = gate_up.size(-1) // 2
|
||||
gate = gate_up[:, :half_dim]
|
||||
up = gate_up[:, half_dim:]
|
||||
activated = torch.nn.functional.silu(gate) * up
|
||||
down = torch.mm(activated, w2[i].t())
|
||||
|
||||
final[idx] += down * expert_weights.unsqueeze(-1)
|
||||
|
||||
return final
|
||||
|
||||
178
ex_engine/python/corex_so_loader.py
Normal file
178
ex_engine/python/corex_so_loader.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""corex_so_loader.py — Unified loader for all 12 prebuilt CoreX .so modules.
|
||||
|
||||
CCCL pattern: device_reduce policy_selector — enumerate available kernels at
|
||||
init, expose a stable Python API, fall back gracefully when .so unavailable.
|
||||
|
||||
The 12 prebuilt .so files expose these operator families:
|
||||
|
||||
GDN decode pipeline (5 .so):
|
||||
corex_gdn_causal_conv → .causal_conv_update(conv_state, mixed_qkv, weight)
|
||||
corex_gdn_packed_decode → .packed_decode(temporal_state, packed_qkv, b, a, A_log, dt_bias)
|
||||
corex_gdn_beta_decay → .beta_decay(b, a, A_log, dt_bias)
|
||||
corex_gdn_qk_map → .qk_map(q, k, num_v_heads)
|
||||
corex_gdn_gated_norm → .apply_inverse(x, z)
|
||||
|
||||
Attention pipeline (3 .so):
|
||||
corex_attn_head_rms_norm → .prepare(x, eps) + .apply_inverse(x, z)
|
||||
corex_paged_kv_gather → .gather(key_cache, val_cache, block_tables, context_lens)
|
||||
corex_fused_paged_prefill → .forward(q, k_cache, v_cache, ...)
|
||||
|
||||
KV cache transfer (1 .so):
|
||||
corex_block_major_kv_transfer → .transfer(src, dst, mapping)
|
||||
|
||||
MoE pipeline (3 .so):
|
||||
corex_moe_direct_routed → .w13(hidden, w13, expert_ids)
|
||||
+ .w2_reduce(act, w2, expert_ids, weights)
|
||||
corex_moe_weight_gather → .gather(w13, w2, expert_ids)
|
||||
corex_moe_exact_reduce → .serial_float(expert_out, weights)
|
||||
|
||||
Usage:
|
||||
from ex_engine.python.corex_so_loader import corex
|
||||
if corex.gdn_causal_conv is not None:
|
||||
out = corex.gdn_causal_conv.causal_conv_update(...)
|
||||
|
||||
# Or import from vllm install root (patch_ops.sh deploys there):
|
||||
from corex_so_loader import corex
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger("corex_so_loader")
|
||||
|
||||
# All 12 .so modules in load order
|
||||
_SO_MANIFEST = [
|
||||
"corex_gdn_causal_conv",
|
||||
"corex_gdn_packed_decode",
|
||||
"corex_gdn_beta_decay",
|
||||
"corex_gdn_qk_map",
|
||||
"corex_gdn_gated_norm",
|
||||
"corex_attn_head_rms_norm",
|
||||
"corex_paged_kv_gather",
|
||||
"corex_fused_paged_prefill",
|
||||
"corex_block_major_kv_transfer",
|
||||
"corex_moe_direct_routed",
|
||||
"corex_moe_weight_gather",
|
||||
"corex_moe_exact_reduce",
|
||||
]
|
||||
|
||||
|
||||
def _find_so_dir() -> Optional[str]:
|
||||
"""Find the directory containing prebuilt CoreX .so files.
|
||||
|
||||
Search order:
|
||||
1. COREX_SO_DIR env var
|
||||
2. vllm install roots (where patch_ops.sh installs them)
|
||||
3. Bundled prebuilt directory (repo-relative)
|
||||
4. /usr/local/corex/lib64/
|
||||
"""
|
||||
candidates = []
|
||||
|
||||
env = os.getenv("COREX_SO_DIR")
|
||||
if env:
|
||||
candidates.append(env)
|
||||
|
||||
# vllm install roots (patch_ops.sh copies .so here)
|
||||
for p in sys.path:
|
||||
if "vllm" in p or "dist-packages" in p:
|
||||
candidates.append(p)
|
||||
# Also check parent/vllm/model_executor/models/
|
||||
candidates.append(os.path.join(p, "vllm", "model_executor", "models"))
|
||||
|
||||
# Repo-relative prebuilt bundle
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
candidates.append(os.path.join(here, "..", "..", "qwen3_6_scripts",
|
||||
"prebuilt", "corex-3.2.3-ivcore10"))
|
||||
candidates.append(os.path.join(here, "..", "..", "qwen3_6_scripts"))
|
||||
|
||||
# System CoreX
|
||||
candidates.append("/usr/local/corex/lib64/")
|
||||
|
||||
for d in candidates:
|
||||
d = os.path.normpath(d)
|
||||
if os.path.isdir(d):
|
||||
test_so = os.path.join(d, "corex_gdn_causal_conv.so")
|
||||
if os.path.isfile(test_so):
|
||||
return d
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _load_so(name: str, so_dir: str):
|
||||
"""Load a single .so by name from so_dir via importlib."""
|
||||
so_path = os.path.join(so_dir, f"{name}.so")
|
||||
if not os.path.isfile(so_path):
|
||||
return None
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(name, so_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
except Exception as e:
|
||||
logger.warning("Failed to load %s: %s", so_path, e)
|
||||
return None
|
||||
|
||||
|
||||
class CoreXModules:
|
||||
"""Container for all loaded CoreX .so modules.
|
||||
|
||||
Each attribute is either the loaded module or None.
|
||||
Attribute names drop the 'corex_' prefix for brevity.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._loaded = {}
|
||||
self._so_dir = None
|
||||
|
||||
so_dir = _find_so_dir()
|
||||
if so_dir is None:
|
||||
logger.info("CoreX prebuilt .so directory not found — all modules disabled")
|
||||
for name in _SO_MANIFEST:
|
||||
short = name.replace("corex_", "", 1)
|
||||
setattr(self, short, None)
|
||||
self._loaded[name] = False
|
||||
return
|
||||
|
||||
self._so_dir = so_dir
|
||||
logger.info("CoreX .so directory: %s", so_dir)
|
||||
|
||||
loaded_count = 0
|
||||
for name in _SO_MANIFEST:
|
||||
mod = _load_so(name, so_dir)
|
||||
short = name.replace("corex_", "", 1)
|
||||
setattr(self, short, mod)
|
||||
self._loaded[name] = mod is not None
|
||||
if mod is not None:
|
||||
loaded_count += 1
|
||||
|
||||
logger.info("CoreX: %d/%d .so loaded from %s",
|
||||
loaded_count, len(_SO_MANIFEST), so_dir)
|
||||
|
||||
def summary(self) -> str:
|
||||
"""Return a human-readable summary of loaded modules."""
|
||||
lines = [f"CoreX .so loader ({self._so_dir or 'NOT FOUND'})"]
|
||||
for name in _SO_MANIFEST:
|
||||
status = "✓" if self._loaded.get(name) else "✗"
|
||||
short = name.replace("corex_", "", 1)
|
||||
mod = getattr(self, short, None)
|
||||
if mod is not None:
|
||||
funcs = [f for f in dir(mod) if not f.startswith("_")]
|
||||
lines.append(f" {status} {name} → .{', .'.join(funcs)}")
|
||||
else:
|
||||
lines.append(f" {status} {name}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@property
|
||||
def all_loaded(self) -> bool:
|
||||
return all(self._loaded.values())
|
||||
|
||||
@property
|
||||
def loaded_count(self) -> int:
|
||||
return sum(1 for v in self._loaded.values() if v)
|
||||
|
||||
|
||||
# Singleton — initialized on first import
|
||||
corex = CoreXModules()
|
||||
100
ex_engine/python/ex_topk_bridge.py
Normal file
100
ex_engine/python/ex_topk_bridge.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""ex_topk_bridge.py — ctypes bridge for ex_factor_0.so topk_softmax
|
||||
|
||||
CCCL pattern: ex_registry → ex_dispatch → kernel
|
||||
Python bridge: ctypes.CDLL → ex_dispatch_moe_topk_softmax()
|
||||
|
||||
Usage:
|
||||
from ex_engine.python.ex_topk_bridge import ex_topk_softmax
|
||||
ex_topk_softmax(topk_weights, topk_ids, token_expert_indices, gating_output)
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger("ex_topk_bridge")
|
||||
|
||||
_lib = None
|
||||
_dispatch_fn = None
|
||||
|
||||
|
||||
def _load():
|
||||
global _lib, _dispatch_fn
|
||||
if _dispatch_fn is not None:
|
||||
return True
|
||||
|
||||
# Search for ex_factor_0.so
|
||||
search = [
|
||||
os.path.join(os.path.dirname(__file__), "..", "build"),
|
||||
"/workspace/ex_engine/build",
|
||||
os.path.join(os.path.dirname(__file__), ".."),
|
||||
]
|
||||
# Also check vllm model path (where build.sh factor compile puts it)
|
||||
for p in ["/usr/local/corex/lib64/python3/dist-packages/vllm/model_executor/models/ex_engine",
|
||||
"/usr/local/corex/lib/python3/dist-packages/vllm/model_executor/models/ex_engine"]:
|
||||
search.append(p)
|
||||
|
||||
for d in search:
|
||||
so = os.path.join(d, "ex_factor_0.so")
|
||||
if os.path.isfile(so):
|
||||
try:
|
||||
_lib_local = ctypes.CDLL(so)
|
||||
fn = _lib_local.ex_dispatch_moe_topk_softmax
|
||||
fn.restype = ctypes.c_int
|
||||
fn.argtypes = [
|
||||
ctypes.c_void_p, # float* topk_weights
|
||||
ctypes.c_void_p, # int32_t* topk_ids
|
||||
ctypes.c_void_p, # const float* logits
|
||||
ctypes.c_int, # T
|
||||
ctypes.c_int, # E
|
||||
ctypes.c_int, # top_k
|
||||
ctypes.c_void_p, # stream
|
||||
]
|
||||
_lib = _lib_local
|
||||
_dispatch_fn = fn
|
||||
logger.info("ex_factor_0.so loaded from %s", so)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning("Failed to load %s: %s", so, e)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def ex_topk_softmax(topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
token_expert_indices: torch.Tensor,
|
||||
gating_output: torch.Tensor) -> None:
|
||||
"""Drop-in replacement for _custom_ops.topk_softmax using ex_factor_0.so.
|
||||
|
||||
Same interface as vllm._custom_ops.topk_softmax:
|
||||
topk_weights: (T, K) float32, output
|
||||
topk_ids: (T, K) int32, output
|
||||
token_expert_indices: (T, K) int32, output (ignored by ex kernel)
|
||||
gating_output: (T, E) float32, input
|
||||
"""
|
||||
if not _load():
|
||||
raise RuntimeError("ex_factor_0.so not available")
|
||||
|
||||
T, E = gating_output.shape
|
||||
K = topk_weights.shape[1]
|
||||
|
||||
# Get CUDA stream
|
||||
stream = torch.cuda.current_stream().cuda_stream
|
||||
|
||||
ret = _dispatch_fn(
|
||||
topk_weights.data_ptr(),
|
||||
topk_ids.data_ptr(),
|
||||
gating_output.data_ptr(),
|
||||
T, E, K,
|
||||
stream,
|
||||
)
|
||||
if ret != 0:
|
||||
raise RuntimeError(f"ex_dispatch_moe_topk_softmax returned {ret}")
|
||||
|
||||
# token_expert_indices: vllm expects (T, K) with values k_idx * T + t_idx
|
||||
# ex kernel doesn't write this, fill it here
|
||||
if token_expert_indices is not None:
|
||||
T_t = torch.arange(T, device=topk_ids.device, dtype=torch.int32)
|
||||
for k in range(K):
|
||||
token_expert_indices[:, k] = k * T + T_t
|
||||
219
ex_engine/python/gdn_fp32.py
Normal file
219
ex_engine/python/gdn_fp32.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""gdn_fp32.py — FP32-accumulation GatedDeltaNet implementations.
|
||||
|
||||
Ported from upstream xllm/core/layers/npu_torch/qwen3_gated_delta_net_base.cpp.
|
||||
The key fix: all internal computation in fp32, cast back to original dtype at end.
|
||||
This eliminates the 99.98% NaN problem seen in comp 168 docker logs.
|
||||
|
||||
Two implementations:
|
||||
- torch_recurrent_gated_delta_rule: single-step recurrent (for decode)
|
||||
- torch_chunk_gated_delta_rule: chunked (for prefill)
|
||||
"""
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def _l2norm(x: torch.Tensor, dim: int = -1, eps: float = 1e-6) -> torch.Tensor:
|
||||
"""L2 normalize along dim."""
|
||||
return F.normalize(x, p=2, dim=dim, eps=eps)
|
||||
|
||||
|
||||
def torch_recurrent_gated_delta_rule(
|
||||
query: torch.Tensor, # [B, H, L, K]
|
||||
key: torch.Tensor, # [B, H, L, K]
|
||||
value: torch.Tensor, # [B, H, L, V]
|
||||
g: torch.Tensor, # [B, H, L] (gate / log-decay)
|
||||
beta: torch.Tensor, # [B, H, L]
|
||||
initial_state=None, # [B, H, K, V] or None
|
||||
use_qk_l2norm: bool = True,
|
||||
):
|
||||
"""Single-step recurrent GDN — decode path.
|
||||
|
||||
Port of: qwen3_gated_delta_net_base.cpp::torch_recurrent_gated_delta_rule()
|
||||
Key difference from our previous Python: ALL computation in fp32.
|
||||
"""
|
||||
initial_dtype = query.dtype
|
||||
|
||||
if use_qk_l2norm:
|
||||
query = _l2norm(query, -1)
|
||||
key = _l2norm(key, -1)
|
||||
|
||||
# Upstream: to_float32_and_transpose → [B, H, L, D]
|
||||
# Our tensors are already [B, H, L, D] from the caller, so just cast
|
||||
query = query.float()
|
||||
key = key.float()
|
||||
value = value.float()
|
||||
beta = beta.float()
|
||||
g = g.float()
|
||||
|
||||
B, H, L, K = query.shape
|
||||
V = value.size(-1)
|
||||
|
||||
scale = (1.0 / (K ** 0.5))
|
||||
query = query * scale
|
||||
|
||||
if initial_state is None:
|
||||
state = torch.zeros(B, H, K, V, dtype=torch.float32,
|
||||
device=query.device)
|
||||
else:
|
||||
state = initial_state.to(dtype=torch.float32, device=query.device)
|
||||
|
||||
outputs = torch.zeros(B, H, L, V, dtype=torch.float32,
|
||||
device=query.device)
|
||||
|
||||
for i in range(L):
|
||||
q_t = query[:, :, i] # [B, H, K]
|
||||
k_t = key[:, :, i] # [B, H, K]
|
||||
v_t = value[:, :, i] # [B, H, V]
|
||||
g_t = g[:, :, i].exp() # [B, H]
|
||||
beta_t = beta[:, :, i] # [B, H]
|
||||
|
||||
# Decay state
|
||||
state = state * g_t.unsqueeze(-1).unsqueeze(-1)
|
||||
|
||||
# Delta update: v - sum(state * k, dim=-2)
|
||||
kv_mem = (state * k_t.unsqueeze(-1)).sum(-2) # [B, H, V]
|
||||
delta = (v_t - kv_mem) * beta_t.unsqueeze(-1) # [B, H, V]
|
||||
|
||||
# Write to state
|
||||
state = state + k_t.unsqueeze(-1) * delta.unsqueeze(-2)
|
||||
|
||||
# Query readout
|
||||
outputs[:, :, i] = (state * q_t.unsqueeze(-1)).sum(-2)
|
||||
|
||||
outputs = outputs.to(initial_dtype)
|
||||
return outputs, state
|
||||
|
||||
|
||||
def torch_chunk_gated_delta_rule(
|
||||
query: torch.Tensor, # [B, H, L, K]
|
||||
key: torch.Tensor, # [B, H, L, K]
|
||||
value: torch.Tensor, # [B, H, L, V]
|
||||
g: torch.Tensor, # [B, H, L]
|
||||
beta: torch.Tensor, # [B, H, L]
|
||||
chunk_size: int = 64,
|
||||
initial_state=None,
|
||||
output_final_state: bool = True,
|
||||
use_qk_l2norm: bool = True,
|
||||
):
|
||||
"""Chunked GDN — prefill path.
|
||||
|
||||
Port of: qwen3_gated_delta_net_base.cpp::torch_chunk_gated_delta_rule()
|
||||
ALL internal computation in fp32 to prevent NaN.
|
||||
"""
|
||||
initial_dtype = query.dtype
|
||||
|
||||
if use_qk_l2norm:
|
||||
query = _l2norm(query, -1)
|
||||
key = _l2norm(key, -1)
|
||||
|
||||
# Cast to fp32
|
||||
query = query.float()
|
||||
key = key.float()
|
||||
value = value.float()
|
||||
beta = beta.float()
|
||||
g = g.float()
|
||||
|
||||
B, H, L, K = query.shape
|
||||
V = value.size(-1)
|
||||
|
||||
# Pad to multiple of chunk_size
|
||||
pad = (chunk_size - L % chunk_size) % chunk_size
|
||||
if pad > 0:
|
||||
query = F.pad(query, (0, 0, 0, pad))
|
||||
key = F.pad(key, (0, 0, 0, pad))
|
||||
value = F.pad(value, (0, 0, 0, pad))
|
||||
beta = F.pad(beta, (0, pad))
|
||||
g = F.pad(g, (0, pad))
|
||||
|
||||
total_len = L + pad
|
||||
scale = 1.0 / (K ** 0.5)
|
||||
query = query * scale
|
||||
|
||||
v_beta = value * beta.unsqueeze(-1)
|
||||
k_beta = key * beta.unsqueeze(-1)
|
||||
|
||||
# Reshape to chunks: [B, H, num_chunks, chunk_size, D]
|
||||
num_chunks = total_len // chunk_size
|
||||
query = query.reshape(B, H, num_chunks, chunk_size, K)
|
||||
key = key.reshape(B, H, num_chunks, chunk_size, K)
|
||||
value_c = value.reshape(B, H, num_chunks, chunk_size, V)
|
||||
k_beta = k_beta.reshape(B, H, num_chunks, chunk_size, K)
|
||||
v_beta = v_beta.reshape(B, H, num_chunks, chunk_size, V)
|
||||
g = g.reshape(B, H, num_chunks, chunk_size)
|
||||
|
||||
# Cumulative sum of g within each chunk
|
||||
g = g.cumsum(-1)
|
||||
|
||||
# Decay mask within chunk
|
||||
g_diff = g.unsqueeze(-1) - g.unsqueeze(-2) # [B,H,C,cs,cs]
|
||||
decay_mask = g_diff.tril().exp()
|
||||
decay_mask = decay_mask.tril()
|
||||
|
||||
# Intra-chunk attention correction (Woodbury-like)
|
||||
mask_upper = torch.triu(torch.ones(chunk_size, chunk_size,
|
||||
dtype=torch.bool,
|
||||
device=query.device), 0)
|
||||
attn = -(torch.matmul(k_beta, key.transpose(-1, -2)) * decay_mask)
|
||||
attn = attn.masked_fill(mask_upper, 0.0)
|
||||
|
||||
# Sequential correction within chunk (upstream lines 174-192)
|
||||
for i in range(1, chunk_size):
|
||||
row = attn[..., i:i+1, :i].squeeze(-2).clone()
|
||||
sub = attn[..., :i, :i].clone()
|
||||
row_sub = (row.unsqueeze(-1) * sub).sum(-2)
|
||||
attn[..., i:i+1, :i] = (row + row_sub).unsqueeze(-2)
|
||||
|
||||
eye = torch.eye(chunk_size, dtype=attn.dtype, device=attn.device)
|
||||
attn = attn + eye
|
||||
|
||||
# Corrected value and k_cumdecay
|
||||
value_corr = torch.matmul(attn, v_beta)
|
||||
k_cumdecay = torch.matmul(attn, k_beta * g.exp().unsqueeze(-1))
|
||||
|
||||
# Initialize state
|
||||
if initial_state is None:
|
||||
state = torch.zeros(B, H, K, V, dtype=torch.float32,
|
||||
device=query.device)
|
||||
else:
|
||||
state = initial_state.to(dtype=torch.float32, device=query.device)
|
||||
|
||||
out = torch.zeros_like(value_corr)
|
||||
|
||||
mask_strict_upper = torch.triu(torch.ones(chunk_size, chunk_size,
|
||||
dtype=torch.bool,
|
||||
device=query.device), 1)
|
||||
|
||||
for i in range(num_chunks):
|
||||
q_i = query[:, :, i] # [B,H,cs,K]
|
||||
k_i = key[:, :, i]
|
||||
v_i = value_corr[:, :, i] # [B,H,cs,V]
|
||||
|
||||
attn_i = (torch.matmul(q_i, k_i.transpose(-1, -2))
|
||||
* decay_mask[:, :, i])
|
||||
attn_i = attn_i.masked_fill_(mask_strict_upper, 0.0)
|
||||
|
||||
# Cross-chunk: state contribution
|
||||
v_prime = torch.matmul(k_cumdecay[:, :, i], state) # [B,H,cs,V]
|
||||
v_new = v_i - v_prime
|
||||
|
||||
# Inter-chunk attention
|
||||
g_i = g[:, :, i] # [B,H,cs]
|
||||
attn_inter = torch.matmul(
|
||||
q_i * g_i.unsqueeze(-1).exp(), state) # [B,H,cs,V]
|
||||
|
||||
out[:, :, i] = attn_inter + torch.matmul(attn_i, v_new)
|
||||
|
||||
# Update state
|
||||
g_last = g_i[..., -1:] # [B,H,1]
|
||||
g_exp_term = (g_last - g_i).exp().unsqueeze(-1) # [B,H,cs,1]
|
||||
k_g_exp = (k_i * g_exp_term).transpose(-1, -2) # [B,H,K,cs]
|
||||
state = (state * g_last.unsqueeze(-1).exp()
|
||||
+ torch.matmul(k_g_exp, v_new))
|
||||
|
||||
# Reshape back, trim padding, cast back
|
||||
out = out.reshape(B, H, total_len, V)
|
||||
out = out[:, :, :L, :]
|
||||
out = out.to(initial_dtype)
|
||||
|
||||
return out, state
|
||||
@@ -1,195 +1,211 @@
|
||||
"""
|
||||
ix_bridge.py — Full ixformer bridge loader.
|
||||
ix_bridge.py — Load ix_moe_bridge.so and expose ixformer::infer functions to Python.
|
||||
|
||||
Loads ix_full_bridge.so (all 14 ixformer::infer functions) or falls back
|
||||
to ix_moe_bridge.so (MoE-only 6 functions).
|
||||
LOAD CHAIN:
|
||||
1. Try precompiled ix_moe_bridge.so (from Docker build)
|
||||
2. Try JIT compile ix_moe_bridge.cpp (fallback)
|
||||
3. If both fail → functions return None (caller must handle)
|
||||
|
||||
Functions exposed:
|
||||
MoE: topk_softmax, moe_gen_idx, moe_expand_input, group_gemm,
|
||||
silu_and_mul, moe_combine_result, fused_moe_forward
|
||||
Attention: paged_attention, flash_attn_prefill
|
||||
Norm: rms_norm, fused_add_rms_norm
|
||||
RoPE: rotary_embedding
|
||||
Cache: reshape_and_cache
|
||||
Linear: linear
|
||||
USAGE:
|
||||
from ex_engine.python.ix_bridge import topk_softmax, moe_group_gemm, ...
|
||||
|
||||
if topk_softmax is not None:
|
||||
topk_softmax(weights, ids, indices, gating)
|
||||
else:
|
||||
# fallback to Python implementation
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import logging
|
||||
import torch
|
||||
from typing import Tuple, Optional, List
|
||||
import importlib
|
||||
|
||||
logger = logging.getLogger("ex_engine.ix_bridge")
|
||||
|
||||
_bridge = None
|
||||
_loaded = False
|
||||
_available = False
|
||||
|
||||
# All .cpp sources to try, in priority order
|
||||
_CPP_NAMES = ["ix_full_bridge.cpp", "ix_moe_bridge.cpp"]
|
||||
|
||||
|
||||
def _find_cpp(name):
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
candidates = [
|
||||
os.path.join(here, "..", "csrc", name),
|
||||
os.path.join(here, name),
|
||||
os.path.join("/workspace/ex_engine/csrc", name),
|
||||
os.path.join("/workspace/qwen3_6_scripts", name),
|
||||
def _find_so():
|
||||
"""Find precompiled ix_moe_bridge*.so."""
|
||||
search_dirs = [
|
||||
os.path.join(os.path.dirname(__file__), ".."),
|
||||
os.path.join(os.path.dirname(__file__), "..", "build"),
|
||||
"/workspace/ex_engine/build",
|
||||
"/workspace/ex_engine",
|
||||
]
|
||||
for c in candidates:
|
||||
p = os.path.normpath(c)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
# Also check site-packages
|
||||
try:
|
||||
import ex_engine
|
||||
search_dirs.append(os.path.dirname(ex_engine.__file__))
|
||||
search_dirs.append(os.path.join(os.path.dirname(ex_engine.__file__), "build"))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for d in search_dirs:
|
||||
for so in glob.glob(os.path.join(d, "ix_moe_bridge*.so")):
|
||||
return so
|
||||
return None
|
||||
|
||||
|
||||
def _load_bridge():
|
||||
global _bridge, _loaded, _available
|
||||
def _load():
|
||||
"""Load the bridge module."""
|
||||
global _bridge, _loaded
|
||||
if _loaded:
|
||||
return _available
|
||||
return _bridge
|
||||
_loaded = True
|
||||
|
||||
from torch.utils.cpp_extension import load
|
||||
import glob
|
||||
|
||||
# Find ixformer .so libraries to link against
|
||||
extra_ldflags = []
|
||||
ixf_lib_dirs = set()
|
||||
try:
|
||||
import ixformer
|
||||
ixf_dir = os.path.dirname(ixformer.__file__)
|
||||
# Link against all .so in the ixformer package
|
||||
for so in glob.glob(os.path.join(ixf_dir, "*.so")):
|
||||
if "cpython" not in so: # skip the Python extension .so
|
||||
extra_ldflags.append(so)
|
||||
ixf_lib_dirs.add(os.path.dirname(so))
|
||||
# Also try the _C and _ixformer_torch extensions
|
||||
for so in glob.glob(os.path.join(ixf_dir, "_ixformer_torch*.so")):
|
||||
extra_ldflags.append(so)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Also check /usr/local/corex/lib64 for libixattn etc
|
||||
corex_lib = "/usr/local/corex/lib64"
|
||||
if os.path.isdir(corex_lib):
|
||||
for lib in ["libixattn.so", "libixformer.so", "libcublas.so"]:
|
||||
p = os.path.join(corex_lib, lib)
|
||||
if os.path.exists(p) and p not in extra_ldflags:
|
||||
extra_ldflags.append(p)
|
||||
ixf_lib_dirs.add(corex_lib)
|
||||
|
||||
# Add rpath so the .so can find its dependencies at runtime
|
||||
for d in ixf_lib_dirs:
|
||||
extra_ldflags.append(f"-Wl,-rpath,{d}")
|
||||
|
||||
logger.info("ix_bridge extra_ldflags: %s", extra_ldflags)
|
||||
|
||||
for cpp_name in _CPP_NAMES:
|
||||
cpp_path = _find_cpp(cpp_name)
|
||||
if cpp_path is None:
|
||||
continue
|
||||
mod_name = cpp_name.replace(".cpp", "").replace(".", "_")
|
||||
|
||||
# Method 1: Try precompiled .so
|
||||
so_path = _find_so()
|
||||
if so_path:
|
||||
try:
|
||||
logger.info("JIT-compiling %s from %s ...", cpp_name, cpp_path)
|
||||
_bridge = load(
|
||||
name=mod_name,
|
||||
sources=[cpp_path],
|
||||
extra_cflags=["-O2", "-std=c++17"],
|
||||
extra_ldflags=extra_ldflags,
|
||||
verbose=False,
|
||||
)
|
||||
_available = True
|
||||
fns = [x for x in dir(_bridge) if not x.startswith("_")]
|
||||
logger.info("ix_bridge loaded (%s): %s", cpp_name, fns)
|
||||
return True
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("ix_moe_bridge", so_path)
|
||||
_bridge = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(_bridge)
|
||||
logger.info(f"Loaded ix_moe_bridge from: {so_path}")
|
||||
funcs = [x for x in dir(_bridge) if not x.startswith('_')]
|
||||
logger.info(f"Available functions: {funcs}")
|
||||
return _bridge
|
||||
except Exception as e:
|
||||
logger.warning("JIT compile %s failed: %s — trying next", cpp_name, e)
|
||||
|
||||
logger.warning("All ix_bridge sources failed to compile")
|
||||
return False
|
||||
logger.warning(f"Failed to load {so_path}: {e}")
|
||||
|
||||
# Method 2: Try JIT compile
|
||||
try:
|
||||
import torch
|
||||
from torch.utils.cpp_extension import load
|
||||
|
||||
cpp_path = None
|
||||
for p in [
|
||||
os.path.join(os.path.dirname(__file__), "..", "csrc", "ix_moe_bridge.cpp"),
|
||||
"/workspace/ex_engine/csrc/ix_moe_bridge.cpp",
|
||||
]:
|
||||
if os.path.exists(p):
|
||||
cpp_path = p
|
||||
break
|
||||
|
||||
if cpp_path is None:
|
||||
logger.warning("ix_moe_bridge.cpp not found for JIT compile")
|
||||
return None
|
||||
|
||||
# Find libixformer.so
|
||||
ldflags = ["-lixformer"]
|
||||
for d in [
|
||||
"/usr/local/corex/lib64/python3/dist-packages/ixformer",
|
||||
"/usr/local/corex/lib/python3/dist-packages/ixformer",
|
||||
]:
|
||||
if os.path.exists(os.path.join(d, "libixformer.so")):
|
||||
ldflags.insert(0, f"-L{d}")
|
||||
ldflags.insert(1, f"-Wl,-rpath,{d}")
|
||||
break
|
||||
|
||||
_bridge = load(
|
||||
name="ix_moe_bridge",
|
||||
sources=[cpp_path],
|
||||
extra_cflags=["-O2", "-std=c++17"],
|
||||
extra_ldflags=ldflags,
|
||||
verbose=False,
|
||||
)
|
||||
logger.info(f"JIT compiled ix_moe_bridge from: {cpp_path}")
|
||||
return _bridge
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"JIT compile failed: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_available() -> bool:
|
||||
if not _loaded:
|
||||
_load_bridge()
|
||||
return _available
|
||||
def _get_fn(name):
|
||||
"""Get a function from the bridge, or None."""
|
||||
mod = _load()
|
||||
if mod is None:
|
||||
return None
|
||||
return getattr(mod, name, None)
|
||||
|
||||
|
||||
def _get():
|
||||
if not is_available():
|
||||
raise RuntimeError("ix_bridge not available")
|
||||
return _bridge
|
||||
# ============================================================================
|
||||
# Public API — each is None if bridge not available
|
||||
# ============================================================================
|
||||
|
||||
def topk_softmax(topk_weights, topk_ids, token_expert_indices, gating_output):
|
||||
fn = _get_fn("topk_softmax")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: topk_softmax not available")
|
||||
fn(topk_weights, topk_ids, token_expert_indices, gating_output)
|
||||
|
||||
# =========================================================================
|
||||
# MoE
|
||||
# =========================================================================
|
||||
def topk_softmax(gating_output, topk, renormalize=True):
|
||||
return _get().topk_softmax(gating_output, topk, renormalize)
|
||||
|
||||
def moe_gen_idx(expert_id, expert_num):
|
||||
return _get().moe_gen_idx(expert_id, expert_num)
|
||||
fn = _get_fn("moe_gen_idx")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: moe_gen_idx not available")
|
||||
return fn(expert_id, expert_num)
|
||||
|
||||
def moe_expand_input(input, gather_index, combine_idx, topk):
|
||||
return _get().moe_expand_input(input, gather_index, combine_idx, topk)
|
||||
|
||||
def group_gemm(inputs, weights, token_count, output_n):
|
||||
return _get().group_gemm(inputs, weights, token_count, output_n)
|
||||
def moe_expand_input(input_tensor, gather_index, combine_idx, topk):
|
||||
fn = _get_fn("moe_expand_input")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: moe_expand_input not available")
|
||||
return fn(input_tensor, gather_index, combine_idx, topk)
|
||||
|
||||
def silu_and_mul(input):
|
||||
return _get().silu_and_mul(input)
|
||||
|
||||
def moe_combine_result(input, weight):
|
||||
return _get().moe_combine_result(input, weight)
|
||||
def moe_group_gemm(output, inputs, weights, tokens_per_experts, output_n):
|
||||
fn = _get_fn("moe_group_gemm")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: moe_group_gemm not available")
|
||||
fn(output, inputs, weights, tokens_per_experts, output_n)
|
||||
|
||||
def fused_moe_forward(hidden_states, router_logits, w13, w2,
|
||||
topk, num_experts, renormalize=True):
|
||||
return _get().fused_moe_forward(
|
||||
hidden_states, router_logits, w13, w2, topk, num_experts, renormalize)
|
||||
|
||||
# =========================================================================
|
||||
# Attention
|
||||
# =========================================================================
|
||||
def paged_attention(output, query, key_cache, value_cache,
|
||||
num_kv_heads, scale, block_tables, seq_lens,
|
||||
block_size, max_context_len, alibi_slopes=None):
|
||||
return _get().paged_attention(
|
||||
output, query, key_cache, value_cache,
|
||||
num_kv_heads, scale, block_tables, seq_lens,
|
||||
block_size, max_context_len, alibi_slopes)
|
||||
def silu_and_mul(input_tensor):
|
||||
fn = _get_fn("silu_and_mul")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: silu_and_mul not available")
|
||||
return fn(input_tensor)
|
||||
|
||||
def flash_attn_prefill(query, key, value, output, block_tables,
|
||||
cu_seq_q, cu_seq_k, max_query_len, max_seq_len,
|
||||
scale, is_causal=True, window_left=-1, window_right=-1):
|
||||
return _get().flash_attn_prefill(
|
||||
query, key, value, output, block_tables,
|
||||
cu_seq_q, cu_seq_k, max_query_len, max_seq_len,
|
||||
scale, is_causal, window_left, window_right)
|
||||
|
||||
# =========================================================================
|
||||
# Norm
|
||||
# =========================================================================
|
||||
def rms_norm(output, input, weight, eps=1e-6):
|
||||
return _get().rms_norm(output, input, weight, eps)
|
||||
def moe_combine_result(input_tensor, weight):
|
||||
fn = _get_fn("moe_combine_result")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: moe_combine_result not available")
|
||||
return fn(input_tensor, weight)
|
||||
|
||||
def fused_add_rms_norm(input, residual, weight, output, residual_output, eps=1e-6):
|
||||
return _get().fused_add_rms_norm(input, residual, weight, output, residual_output, eps)
|
||||
|
||||
# =========================================================================
|
||||
# RoPE
|
||||
# =========================================================================
|
||||
def rotary_embedding(positions, query, key, head_size, cos_sin_cache, is_neox=True):
|
||||
return _get().rotary_embedding(positions, query, key, head_size, cos_sin_cache, is_neox)
|
||||
def paged_attention(out, query, key_cache, value_cache, num_kv_heads, scale,
|
||||
block_tables, context_lens, block_size, max_context_len):
|
||||
fn = _get_fn("paged_attention")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: paged_attention not available")
|
||||
return fn(out, query, key_cache, value_cache, num_kv_heads, scale,
|
||||
block_tables, context_lens, block_size, max_context_len)
|
||||
|
||||
|
||||
def rms_norm(output, input_tensor, weight, eps):
|
||||
fn = _get_fn("rms_norm")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: rms_norm not available")
|
||||
fn(output, input_tensor, weight, eps)
|
||||
|
||||
|
||||
def linear(input_tensor, weight):
|
||||
fn = _get_fn("linear")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: linear not available")
|
||||
return fn(input_tensor, weight)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Cache
|
||||
# =========================================================================
|
||||
def reshape_and_cache(key, value, key_cache, value_cache, slot_mapping):
|
||||
return _get().reshape_and_cache(key, value, key_cache, value_cache, slot_mapping)
|
||||
fn = _get_fn("reshape_and_cache")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: reshape_and_cache not available")
|
||||
fn(key, value, key_cache, value_cache, slot_mapping)
|
||||
|
||||
# =========================================================================
|
||||
# Linear
|
||||
# =========================================================================
|
||||
def linear(input, weight, bias=None):
|
||||
return _get().linear(input, weight, bias)
|
||||
|
||||
def rotary_embedding(positions, query, key, head_size, cos_sin_cache):
|
||||
fn = _get_fn("rotary_embedding")
|
||||
if fn is None:
|
||||
raise RuntimeError("ix_moe_bridge: rotary_embedding not available")
|
||||
fn(positions, query, key, head_size, cos_sin_cache)
|
||||
|
||||
|
||||
# Convenience: check if bridge is available
|
||||
def is_available():
|
||||
return _load() is not None
|
||||
|
||||
343
ex_engine/python/ix_unified.py
Normal file
343
ex_engine/python/ix_unified.py
Normal file
@@ -0,0 +1,343 @@
|
||||
"""ix_unified.py — Unified Python interface to all ixformer::infer APIs.
|
||||
|
||||
Dispatch hierarchy (CCCL policy_selector pattern):
|
||||
Tier 0: ix_unified_bridge.so (C++ direct call to ixformer::infer)
|
||||
Tier 1: ixformer.functions.* (base image Python bindings, partial)
|
||||
Tier 2: PyTorch fallback (always works, slowest)
|
||||
|
||||
Usage:
|
||||
from ex_engine.python.ix_unified import ix
|
||||
out = ix.silu_and_mul(input)
|
||||
ix.rms_norm(output, input, weight, eps)
|
||||
weights, indices = ix.moe_topk_softmax(gating, topk, renorm)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import importlib.util
|
||||
import torch
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("ix_unified")
|
||||
|
||||
_bridge = None
|
||||
|
||||
|
||||
def _load_bridge():
|
||||
"""Load ix_unified_bridge.so from known locations."""
|
||||
global _bridge
|
||||
if _bridge is not None:
|
||||
return _bridge
|
||||
|
||||
# Pre-load ixformer .so symbols into GLOBAL symbol table.
|
||||
# ix_unified_bridge.so has undefined ixformer::infer::* symbols that get
|
||||
# resolved at runtime. Python default import uses RTLD_LOCAL, so we must
|
||||
# force RTLD_GLOBAL on the ixformer .so files BEFORE loading our bridge.
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
# Phase 0: Load torch core libs first — ixformer depends on libc10.so etc.
|
||||
try:
|
||||
import torch as _torch
|
||||
_torch_lib = os.path.join(os.path.dirname(_torch.__file__), "lib")
|
||||
for _name in ["libc10.so", "libtorch_cpu.so", "libtorch.so",
|
||||
"libc10_cuda.so", "libtorch_cuda.so", "libtorch_python.so"]:
|
||||
_p = os.path.join(_torch_lib, _name)
|
||||
if os.path.isfile(_p):
|
||||
try:
|
||||
ctypes.CDLL(_p, mode=ctypes.RTLD_GLOBAL)
|
||||
except Exception:
|
||||
pass
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Phase 1: libixformer.so (CUDA kernels)
|
||||
# Phase 2: _ixformer_torch.so (torch extension with ixformer_torch_ext::*)
|
||||
# ONLY these two — do NOT recursively load unknown .so (causes segfault)
|
||||
_ixf_base = "/usr/local/corex/lib64/python3/dist-packages/ixformer"
|
||||
if os.path.isdir(_ixf_base):
|
||||
for _name in ["libixformer.so",
|
||||
"_ixformer_torch.cpython-310-x86_64-linux-gnu.so"]:
|
||||
_p = os.path.join(_ixf_base, _name)
|
||||
if os.path.isfile(_p):
|
||||
try:
|
||||
ctypes.CDLL(_p, mode=ctypes.RTLD_GLOBAL)
|
||||
logger.info("Preloaded: %s", _name)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
search_paths = []
|
||||
|
||||
# 1. Same directory as this file
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
search_paths.append(os.path.join(here, "..", "build"))
|
||||
search_paths.append(here)
|
||||
|
||||
# 2. Workspace build dirs (Docker / real machine)
|
||||
search_paths.append("/workspace/ex_engine/build")
|
||||
search_paths.append("/home/dylan/project_6/ex_engine/build")
|
||||
|
||||
# 2. vllm install root (where prebuilt .so are deployed)
|
||||
for p in sys.path:
|
||||
if "vllm" in p or "dist-packages" in p:
|
||||
search_paths.append(p)
|
||||
|
||||
# 3. Explicit env var
|
||||
env_path = os.getenv("IX_BRIDGE_PATH")
|
||||
if env_path:
|
||||
search_paths.insert(0, env_path)
|
||||
|
||||
for search_dir in search_paths:
|
||||
for name in ["ix_unified_bridge.so",
|
||||
"ix_unified_bridge.cpython-310-x86_64-linux-gnu.so"]:
|
||||
so_path = os.path.join(search_dir, name)
|
||||
if os.path.isfile(so_path):
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ix_unified_bridge", so_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
_bridge = mod
|
||||
logger.info("ix_unified_bridge loaded from %s", so_path)
|
||||
return _bridge
|
||||
except (ImportError, OSError, SystemError) as e:
|
||||
logger.warning("Bridge load failed (expected if ixformer "
|
||||
"namespace mismatch): %s: %s",
|
||||
os.path.basename(so_path), e)
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning("Bridge load unexpected error: %s", e)
|
||||
continue
|
||||
|
||||
logger.info("ix_unified_bridge.so not found, using fallback dispatch")
|
||||
return None
|
||||
|
||||
|
||||
def _try_ixformer_functions():
|
||||
"""Try importing ixformer.functions from base image."""
|
||||
try:
|
||||
import ixformer.functions as ixf
|
||||
return ixf
|
||||
except (ImportError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Dispatch class
|
||||
# ============================================================================
|
||||
|
||||
class IXDispatch:
|
||||
"""Three-tier dispatch for all ixformer ops."""
|
||||
|
||||
def __init__(self):
|
||||
self._bridge = _load_bridge()
|
||||
self._ixf = _try_ixformer_functions()
|
||||
tier = ("Tier0:bridge" if self._bridge else
|
||||
"Tier1:ixformer" if self._ixf else "Tier2:pytorch")
|
||||
logger.info("IXDispatch initialized: %s", tier)
|
||||
|
||||
# --- Activation -----------------------------------------------------------
|
||||
def silu_and_mul(self, input: torch.Tensor) -> torch.Tensor:
|
||||
if self._bridge:
|
||||
return self._bridge.silu_and_mul(input)
|
||||
if self._ixf and hasattr(self._ixf, 'silu_and_mul'):
|
||||
d = input.size(-1) // 2
|
||||
out = input.new_empty([input.size(0), d])
|
||||
self._ixf.silu_and_mul(input, out)
|
||||
return out
|
||||
# PyTorch fallback
|
||||
d = input.size(-1) // 2
|
||||
x, gate = input[..., :d], input[..., d:]
|
||||
return x * torch.sigmoid(gate)
|
||||
|
||||
# --- Norm -----------------------------------------------------------------
|
||||
def rms_norm(self, output: torch.Tensor, input: torch.Tensor,
|
||||
weight: torch.Tensor, eps: float):
|
||||
if self._bridge:
|
||||
self._bridge.rms_norm(output, input, weight, eps)
|
||||
return
|
||||
if self._ixf and hasattr(self._ixf, 'rms_norm'):
|
||||
self._ixf.rms_norm(input, weight, output, eps)
|
||||
return
|
||||
# PyTorch fallback
|
||||
variance = input.float().pow(2).mean(-1, keepdim=True)
|
||||
normed = input * torch.rsqrt(variance + eps)
|
||||
output.copy_(normed * weight)
|
||||
|
||||
def fused_add_rms_norm(self, input: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
weight: torch.Tensor, eps: float):
|
||||
if self._bridge:
|
||||
self._bridge.fused_add_rms_norm(input, residual, weight, eps)
|
||||
return
|
||||
if self._ixf and hasattr(self._ixf, 'fused_add_rms_norm'):
|
||||
self._ixf.fused_add_rms_norm(input, residual, weight, eps, 1.0)
|
||||
return
|
||||
# PyTorch fallback
|
||||
hidden = input + residual
|
||||
residual.copy_(hidden)
|
||||
variance = hidden.float().pow(2).mean(-1, keepdim=True)
|
||||
normed = hidden * torch.rsqrt(variance + eps)
|
||||
input.copy_(normed * weight)
|
||||
|
||||
# --- Linear ---------------------------------------------------------------
|
||||
def linear(self, input: torch.Tensor, weight: torch.Tensor,
|
||||
bias=None) -> torch.Tensor:
|
||||
if self._bridge:
|
||||
return self._bridge.linear(input, weight, bias)
|
||||
# PyTorch fallback
|
||||
out = torch.nn.functional.linear(input, weight, bias)
|
||||
return out
|
||||
|
||||
# --- RoPE -----------------------------------------------------------------
|
||||
def rotary_embedding(self, positions, query, key, head_size,
|
||||
cos_sin_cache, is_neox=True):
|
||||
if self._bridge:
|
||||
self._bridge.rotary_embedding(positions, query, key, head_size,
|
||||
cos_sin_cache, is_neox)
|
||||
return
|
||||
if self._ixf and hasattr(self._ixf, 'vllm_rotary_embedding_neox'):
|
||||
self._ixf.vllm_rotary_embedding_neox(
|
||||
positions, query, key, head_size, cos_sin_cache, is_neox)
|
||||
return
|
||||
# No PyTorch fallback — this is handled by vllm's own rope
|
||||
|
||||
# --- KV Cache -------------------------------------------------------------
|
||||
def reshape_and_cache(self, key, value, key_cache, value_cache,
|
||||
slot_mapping):
|
||||
if self._bridge:
|
||||
self._bridge.reshape_and_cache(key, value, key_cache, value_cache,
|
||||
slot_mapping)
|
||||
return
|
||||
if self._ixf and hasattr(self._ixf, 'vllm_cache_ops_reshape_and_cache'):
|
||||
self._ixf.vllm_cache_ops_reshape_and_cache(
|
||||
key, value, key_cache, value_cache, slot_mapping)
|
||||
return
|
||||
# PyTorch fallback — slot-by-slot copy
|
||||
for i, slot in enumerate(slot_mapping):
|
||||
if slot < 0:
|
||||
continue
|
||||
block_idx = slot // key_cache.size(2)
|
||||
block_off = slot % key_cache.size(2)
|
||||
key_cache[block_idx, :, block_off, :] = key[i]
|
||||
value_cache[block_idx, :, block_off, :] = value[i]
|
||||
|
||||
# --- Attention: prefill ---------------------------------------------------
|
||||
def flash_attn_prefill(self, query, key_cache, value_cache, output,
|
||||
block_tables, cu_seq_q, cu_seq_k,
|
||||
max_seq_q, max_seq_k, is_causal, scale):
|
||||
if self._bridge:
|
||||
return self._bridge.flash_attn_prefill(
|
||||
query, key_cache, value_cache, output, block_tables,
|
||||
cu_seq_q, cu_seq_k, max_seq_q, max_seq_k, is_causal, scale)
|
||||
if self._ixf and hasattr(self._ixf, 'ixinfer_flash_attn_unpad'):
|
||||
return self._ixf.ixinfer_flash_attn_unpad(
|
||||
query, key_cache, value_cache, output, block_tables,
|
||||
cu_seq_q, cu_seq_k, max_seq_q, max_seq_k,
|
||||
is_causal, -1, -1, scale, 0.0, False, None, None, None)
|
||||
raise RuntimeError("flash_attn_prefill: no backend available")
|
||||
|
||||
# --- Attention: decode (paged) -------------------------------------------
|
||||
def paged_attention(self, output, query, key_cache, value_cache,
|
||||
num_kv_heads, scale, block_tables, context_lens,
|
||||
block_size, max_context_len):
|
||||
if self._bridge:
|
||||
return self._bridge.paged_attention(
|
||||
output, query, key_cache, value_cache,
|
||||
num_kv_heads, scale, block_tables, context_lens,
|
||||
block_size, max_context_len)
|
||||
if self._ixf and hasattr(self._ixf,
|
||||
'vllm_single_query_cached_kv_attention_v2'):
|
||||
return self._ixf.vllm_single_query_cached_kv_attention_v2(
|
||||
output, query, key_cache, value_cache,
|
||||
num_kv_heads, scale, block_tables, context_lens,
|
||||
block_size, max_context_len, None)
|
||||
raise RuntimeError("paged_attention: no backend available")
|
||||
|
||||
# --- MoE: topk_softmax ---------------------------------------------------
|
||||
def moe_topk_softmax(self, gating_output: torch.Tensor,
|
||||
topk: int, renormalize: bool = True):
|
||||
if self._bridge:
|
||||
return self._bridge.moe_topk_softmax(
|
||||
gating_output, topk, renormalize)
|
||||
# PyTorch fallback
|
||||
scores = torch.softmax(gating_output.float(), dim=-1)
|
||||
topk_weights, topk_indices = torch.topk(scores, k=topk, dim=-1)
|
||||
if renormalize:
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1,
|
||||
keepdim=True)
|
||||
return topk_weights, topk_indices.to(torch.int32)
|
||||
|
||||
# --- MoE: gen_idx ---------------------------------------------------------
|
||||
def moe_gen_idx(self, expert_ids: torch.Tensor, num_experts: int):
|
||||
if self._bridge:
|
||||
return self._bridge.moe_gen_idx(expert_ids, num_experts)
|
||||
# PyTorch fallback: compute scatter/gather indices
|
||||
flat = expert_ids.view(-1)
|
||||
n = flat.numel()
|
||||
src_dst = torch.empty(n, dtype=flat.dtype, device=flat.device)
|
||||
dst_src = torch.empty(n, dtype=flat.dtype, device=flat.device)
|
||||
expert_sizes = torch.zeros(num_experts, dtype=flat.dtype,
|
||||
device=flat.device)
|
||||
# Simple counting sort
|
||||
for i in range(n):
|
||||
expert_sizes[flat[i].item()] += 1
|
||||
cumsum = expert_sizes.cumsum(-1)
|
||||
offsets = torch.zeros_like(expert_sizes)
|
||||
offsets[1:] = cumsum[:-1]
|
||||
counts = torch.zeros_like(expert_sizes)
|
||||
for i in range(n):
|
||||
e = flat[i].item()
|
||||
pos = (offsets[e] + counts[e]).item()
|
||||
src_dst[i] = pos
|
||||
dst_src[pos] = i
|
||||
counts[e] += 1
|
||||
return [src_dst, dst_src, expert_sizes, cumsum]
|
||||
|
||||
# --- MoE: expand_input ----------------------------------------------------
|
||||
def moe_expand_input(self, input: torch.Tensor,
|
||||
gather_index: torch.Tensor,
|
||||
combine_idx: torch.Tensor, topk: int):
|
||||
if self._bridge:
|
||||
return self._bridge.moe_expand_input(
|
||||
input, gather_index, combine_idx, topk)
|
||||
# PyTorch fallback
|
||||
return input.index_select(0, combine_idx.view(-1).long())
|
||||
|
||||
# --- MoE: group_gemm -----------------------------------------------------
|
||||
def moe_group_gemm(self, input: torch.Tensor, weight: torch.Tensor,
|
||||
tokens_per_experts: torch.Tensor):
|
||||
if self._bridge:
|
||||
return self._bridge.moe_group_gemm(
|
||||
input, weight, tokens_per_experts)
|
||||
# PyTorch fallback: sequential per-expert GEMM
|
||||
outputs = []
|
||||
offset = 0
|
||||
for e in range(tokens_per_experts.size(0)):
|
||||
count = tokens_per_experts[e].item()
|
||||
if count == 0:
|
||||
continue
|
||||
inp_e = input[offset:offset + count]
|
||||
w_e = weight[e] # [out_features, in_features]
|
||||
outputs.append(inp_e @ w_e.t())
|
||||
offset += count
|
||||
if outputs:
|
||||
return torch.cat(outputs, dim=0)
|
||||
return input.new_empty(0, weight.size(-2))
|
||||
|
||||
# --- MoE: combine_result -------------------------------------------------
|
||||
def moe_combine_result(self, expert_output: torch.Tensor,
|
||||
weights: torch.Tensor):
|
||||
if self._bridge:
|
||||
return self._bridge.moe_combine_result(expert_output, weights)
|
||||
# PyTorch fallback: weighted sum
|
||||
# expert_output: [n_tokens, topk, hidden]
|
||||
# weights: [n_tokens, topk]
|
||||
return (expert_output * weights.unsqueeze(-1)).sum(dim=1)
|
||||
|
||||
|
||||
# Singleton
|
||||
ix = IXDispatch()
|
||||
145
ex_engine/python/moe_dispatch.py
Normal file
145
ex_engine/python/moe_dispatch.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""moe_dispatch.py — MoE forward using ix_unified 3-tier dispatch.
|
||||
|
||||
Replaces the pure-PyTorch for-loop over 64 experts with the ixformer
|
||||
7-step pipeline (from upstream xllm/core/layers/ilu/fused_moe.cpp):
|
||||
|
||||
1. topk_softmax → select top-K experts per token
|
||||
2. moe_gen_idx → compute scatter/gather index mapping
|
||||
3. moe_expand_input → expand tokens by topK
|
||||
4. group_gemm (w13) → gate+up projection for all experts
|
||||
5. silu_and_mul → activation
|
||||
6. group_gemm (w2) → down projection
|
||||
7. moe_combine → weighted reduce back to [n_tokens, hidden]
|
||||
|
||||
Falls back to PyTorch per-expert loop if ix_unified bridge is unavailable.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("moe_dispatch")
|
||||
|
||||
try:
|
||||
from ex_engine.python.ix_unified import ix as _ix
|
||||
except ImportError:
|
||||
try:
|
||||
from ix_unified import ix as _ix
|
||||
except ImportError:
|
||||
_ix = None
|
||||
logger.warning("ix_unified not available, MoE uses pure PyTorch")
|
||||
|
||||
|
||||
def moe_forward_unified(
|
||||
hidden_states: torch.Tensor, # [num_tokens, hidden_size]
|
||||
gate_logits: torch.Tensor, # [num_tokens, num_experts]
|
||||
w13_weight: torch.Tensor, # [num_experts, 2*intermediate, hidden]
|
||||
w2_weight: torch.Tensor, # [num_experts, hidden, intermediate]
|
||||
topk: int = 8,
|
||||
renormalize: bool = True,
|
||||
num_experts: int = 64,
|
||||
) -> torch.Tensor:
|
||||
"""Full MoE forward with ix_unified dispatch.
|
||||
|
||||
Returns: [num_tokens, hidden_size]
|
||||
"""
|
||||
if _ix is None or not hasattr(_ix, '_bridge') or _ix._bridge is None:
|
||||
# No C++ bridge → use Python-loop fallback directly
|
||||
return _moe_pytorch_fallback(
|
||||
hidden_states, gate_logits, w13_weight, w2_weight,
|
||||
topk, renormalize, num_experts)
|
||||
|
||||
try:
|
||||
return _moe_bridge_pipeline(
|
||||
hidden_states, gate_logits, w13_weight, w2_weight,
|
||||
topk, renormalize, num_experts)
|
||||
except Exception as e:
|
||||
logger.warning("MoE bridge pipeline failed (%s), fallback to PyTorch", e)
|
||||
return _moe_pytorch_fallback(
|
||||
hidden_states, gate_logits, w13_weight, w2_weight,
|
||||
topk, renormalize, num_experts)
|
||||
|
||||
|
||||
def _moe_bridge_pipeline(
|
||||
hidden_states, gate_logits, w13_weight, w2_weight,
|
||||
topk, renormalize, num_experts,
|
||||
):
|
||||
"""7-step MoE pipeline using ix_unified bridge."""
|
||||
n_tokens = hidden_states.size(0)
|
||||
|
||||
# Step 1: topk_softmax
|
||||
topk_weights, topk_indices = _ix.moe_topk_softmax(
|
||||
gate_logits, topk, renormalize)
|
||||
|
||||
# Step 2: compute token→expert index mapping
|
||||
expert_ids_flat = topk_indices.view(-1).to(torch.int32)
|
||||
src_dst, dst_src, expert_sizes, expert_cumsum = _ix.moe_gen_idx(
|
||||
expert_ids_flat, num_experts)
|
||||
|
||||
# Step 3: expand input
|
||||
expanded = _ix.moe_expand_input(
|
||||
hidden_states, src_dst, dst_src, topk)
|
||||
|
||||
# Step 4: group GEMM w13 (gate+up projection)
|
||||
gate_up = _ix.moe_group_gemm(expanded, w13_weight, expert_sizes)
|
||||
|
||||
# Step 5: silu_and_mul activation
|
||||
activated = _ix.silu_and_mul(gate_up)
|
||||
|
||||
# Step 6: group GEMM w2 (down projection)
|
||||
down = _ix.moe_group_gemm(activated, w2_weight, expert_sizes)
|
||||
|
||||
# Step 7: combine results (weighted sum over topk experts)
|
||||
down_topk = down.view(n_tokens, topk, -1)
|
||||
output = _ix.moe_combine_result(down_topk, topk_weights)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _moe_pytorch_fallback(
|
||||
hidden_states, gate_logits, w13_weight, w2_weight,
|
||||
topk, renormalize, num_experts,
|
||||
):
|
||||
"""Pure-PyTorch MoE fallback — per-expert loop."""
|
||||
n_tokens, hidden = hidden_states.shape
|
||||
|
||||
# Gating
|
||||
scores = torch.softmax(gate_logits.float(), dim=-1)
|
||||
topk_weights, topk_indices = torch.topk(scores, k=topk, dim=-1)
|
||||
if renormalize:
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
||||
topk_weights = topk_weights.to(hidden_states.dtype)
|
||||
|
||||
output = torch.zeros_like(hidden_states)
|
||||
|
||||
for i in range(n_tokens):
|
||||
for j in range(topk):
|
||||
expert_id = topk_indices[i, j].item()
|
||||
w = topk_weights[i, j]
|
||||
|
||||
# w13: [2*intermediate, hidden]
|
||||
gate_up = hidden_states[i] @ w13_weight[expert_id].t()
|
||||
intermediate = gate_up.size(-1) // 2
|
||||
gate_val = gate_up[:intermediate]
|
||||
up_val = gate_up[intermediate:]
|
||||
activated = torch.sigmoid(gate_val) * up_val
|
||||
|
||||
# w2: [hidden, intermediate]
|
||||
down = activated @ w2_weight[expert_id].t()
|
||||
output[i] += w * down
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def moe_topk_gating(
|
||||
gate_logits: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool = True,
|
||||
):
|
||||
"""Standalone gating — just topk + softmax."""
|
||||
if _ix is not None:
|
||||
return _ix.moe_topk_softmax(gate_logits, topk, renormalize)
|
||||
scores = torch.softmax(gate_logits.float(), dim=-1)
|
||||
weights, indices = torch.topk(scores, k=topk, dim=-1)
|
||||
if renormalize:
|
||||
weights = weights / weights.sum(dim=-1, keepdim=True)
|
||||
return weights, indices.to(torch.int32)
|
||||
236
ex_engine/python/moe_fused_dispatch.py
Normal file
236
ex_engine/python/moe_fused_dispatch.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""moe_fused_dispatch.py — Three-tier MoE dispatch (CCCL policy_selector pattern).
|
||||
|
||||
Port of upstream_ref/xllm/core/layers/ilu/fused_moe.cpp 7-step pipeline.
|
||||
|
||||
Dispatch hierarchy:
|
||||
Tier 0: ix_unified_bridge.so → ixformer::infer 7-step C++ pipeline
|
||||
topk_softmax → gen_idx → expand_input → group_gemm(w13) →
|
||||
silu_and_mul → group_gemm(w2) → combine_result
|
||||
Tier 1: corex prebuilt .so → direct_routed.w13/.w2_reduce (decode T=1 only)
|
||||
Tier 2: PyTorch fallback → per-expert F.linear loop
|
||||
|
||||
Usage in qwen3_5.py:
|
||||
from ex_engine.python.moe_fused_dispatch import fused_moe_forward
|
||||
out = fused_moe_forward(hidden_states, router_logits, w13, w2,
|
||||
top_k=8, num_experts=256, act_fn=silu_and_mul)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Callable, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
logger = logging.getLogger("moe_fused_dispatch")
|
||||
|
||||
# Lazy imports — set at first call
|
||||
_ix = None
|
||||
_corex = None
|
||||
_init_done = False
|
||||
|
||||
|
||||
def _lazy_init():
|
||||
global _ix, _corex, _init_done
|
||||
if _init_done:
|
||||
return
|
||||
_init_done = True
|
||||
|
||||
# Tier 0: ix_unified
|
||||
try:
|
||||
from ex_engine.python.ix_unified import ix
|
||||
if ix._bridge is not None:
|
||||
_ix = ix
|
||||
logger.info("moe_fused_dispatch: Tier0 ix_unified_bridge.so available")
|
||||
else:
|
||||
logger.info("moe_fused_dispatch: Tier0 unavailable (bridge=None)")
|
||||
except Exception as e:
|
||||
logger.info("moe_fused_dispatch: Tier0 unavailable (%s)", e)
|
||||
|
||||
# Try import path used on real hardware
|
||||
if _ix is None:
|
||||
try:
|
||||
from ix_unified import ix
|
||||
if ix._bridge is not None:
|
||||
_ix = ix
|
||||
logger.info("moe_fused_dispatch: Tier0 ix_unified (direct) available")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Tier 1: corex prebuilt .so
|
||||
try:
|
||||
from ex_engine.python.corex_so_loader import corex
|
||||
if corex.moe_direct_routed is not None:
|
||||
_corex = corex
|
||||
logger.info("moe_fused_dispatch: Tier1 corex prebuilt .so available")
|
||||
except Exception as e:
|
||||
logger.info("moe_fused_dispatch: Tier1 unavailable (%s)", e)
|
||||
|
||||
|
||||
def _tier0_fused_moe(
|
||||
hidden_states: torch.Tensor, # [T, H]
|
||||
router_logits: torch.Tensor, # [T, E]
|
||||
w13: torch.Tensor, # [E, 2*I, H]
|
||||
w2: torch.Tensor, # [E, H, I]
|
||||
top_k: int,
|
||||
num_experts: int,
|
||||
act_fn: Callable,
|
||||
) -> torch.Tensor:
|
||||
"""Tier 0: Full 7-step ixformer::infer pipeline via ix_unified_bridge.so.
|
||||
|
||||
Maps 1:1 to xllm/core/layers/ilu/fused_moe.cpp::forward().
|
||||
"""
|
||||
T, H = hidden_states.shape
|
||||
|
||||
# Step 1: topk_softmax — fused softmax + topk selection
|
||||
topk_weights, topk_ids = _ix.moe_topk_softmax(router_logits, top_k,
|
||||
renormalize=True)
|
||||
|
||||
# Step 2: gen_idx — compute scatter/gather indices for expert routing
|
||||
idx_result = _ix.moe_gen_idx(topk_ids, num_experts)
|
||||
src_dst, dst_src, expert_sizes, cumsum = idx_result
|
||||
|
||||
# Step 3: expand_input — scatter tokens to expert order
|
||||
expanded = _ix.moe_expand_input(hidden_states, dst_src, src_dst, top_k)
|
||||
|
||||
# Step 4: group_gemm(w13) — batched GEMM across all experts
|
||||
gate_up = _ix.moe_group_gemm(expanded, w13, expert_sizes)
|
||||
|
||||
# Step 5: activation — SiLU(gate) * up
|
||||
act = act_fn(gate_up)
|
||||
|
||||
# Step 6: group_gemm(w2) — down projection
|
||||
down = _ix.moe_group_gemm(act, w2, expert_sizes)
|
||||
|
||||
# Step 7: combine_result — gather back and weighted sum
|
||||
output = _ix.moe_combine_result(
|
||||
down.view(T, top_k, H), topk_weights)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _tier1_decode_single_token(
|
||||
hidden_states: torch.Tensor, # [1, H]
|
||||
expert_ids: torch.Tensor, # [K]
|
||||
weights: torch.Tensor, # [K]
|
||||
w13: torch.Tensor, # [E, 2*I, H]
|
||||
w2: torch.Tensor, # [E, H, I]
|
||||
act_fn: Callable,
|
||||
) -> torch.Tensor:
|
||||
"""Tier 1: Single-token decode via prebuilt corex_moe_direct_routed.so.
|
||||
|
||||
Only works for T=1 decode. The .so implements fused expert indexing +
|
||||
GEMM + reduction in a single kernel launch.
|
||||
"""
|
||||
gate_up = _corex.moe_direct_routed.w13(hidden_states, w13, expert_ids)
|
||||
act = act_fn(gate_up)
|
||||
return _corex.moe_direct_routed.w2_reduce(act, w2, expert_ids, weights)
|
||||
|
||||
|
||||
def _tier2_pytorch_loop(
|
||||
hidden_states: torch.Tensor, # [T, H]
|
||||
router_logits: torch.Tensor, # [T, E]
|
||||
w13: torch.Tensor, # [E, 2*I, H]
|
||||
w2: torch.Tensor, # [E, H, I]
|
||||
top_k: int,
|
||||
act_fn: Callable,
|
||||
) -> torch.Tensor:
|
||||
"""Tier 2: Pure PyTorch per-expert loop (always works, slowest)."""
|
||||
T, H = hidden_states.shape
|
||||
|
||||
# Softmax → topk
|
||||
topk_logits, topk_ids = torch.topk(router_logits.float(), top_k, dim=-1)
|
||||
topk_weights = torch.softmax(topk_logits, dim=-1).to(hidden_states.dtype)
|
||||
|
||||
if T == 1:
|
||||
# Fast single-token path: batched GEMM
|
||||
eids = topk_ids[0]
|
||||
ws = topk_weights[0]
|
||||
w13_sel = w13[eids]
|
||||
w2_sel = w2[eids]
|
||||
gate_up = F.linear(hidden_states, w13_sel.reshape(-1, H))
|
||||
gate_up = gate_up.view(top_k, -1)
|
||||
act = act_fn(gate_up)
|
||||
expert_out = torch.bmm(w2_sel, act.unsqueeze(-1)).squeeze(-1)
|
||||
return (expert_out * ws.unsqueeze(-1)).sum(0, keepdim=True).to(
|
||||
hidden_states.dtype)
|
||||
else:
|
||||
# General prefill path: sorted per-expert loop
|
||||
out = torch.zeros_like(hidden_states)
|
||||
flat_eids = topk_ids.reshape(-1)
|
||||
order = torch.argsort(flat_eids, stable=True)
|
||||
sorted_tok_ids = torch.arange(
|
||||
T, device=topk_ids.device).repeat_interleave(top_k)[order]
|
||||
sorted_weights = topk_weights.reshape(-1)[order]
|
||||
expert_counts = torch.bincount(
|
||||
flat_eids, minlength=w13.shape[0]).tolist()
|
||||
|
||||
start = 0
|
||||
for eid, count in enumerate(expert_counts):
|
||||
if count == 0:
|
||||
continue
|
||||
end = start + count
|
||||
tok_ids = sorted_tok_ids[start:end]
|
||||
tokens = hidden_states[tok_ids]
|
||||
gate_up = F.linear(tokens, w13[eid])
|
||||
act = act_fn(gate_up)
|
||||
expert_out = F.linear(act, w2[eid])
|
||||
weights_e = sorted_weights[start:end].unsqueeze(-1)
|
||||
out.index_add_(0, tok_ids, (expert_out * weights_e).to(out.dtype))
|
||||
start = end
|
||||
return out
|
||||
|
||||
|
||||
def fused_moe_forward(
|
||||
hidden_states: torch.Tensor, # [T, H]
|
||||
router_logits: torch.Tensor, # [T, E]
|
||||
w13: torch.Tensor, # [E, 2*I, H]
|
||||
w2: torch.Tensor, # [E, H, I]
|
||||
top_k: int = 8,
|
||||
num_experts: int = 256,
|
||||
act_fn: Optional[Callable] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Dispatch MoE through Tier 0 → 1 → 2.
|
||||
|
||||
Returns partial output (pre all-reduce), same contract as vllm FusedMoE.
|
||||
"""
|
||||
_lazy_init()
|
||||
|
||||
if act_fn is None:
|
||||
def _default_act(x):
|
||||
gate, up = x.chunk(2, dim=-1)
|
||||
return F.silu(gate) * up
|
||||
act_fn = _default_act
|
||||
|
||||
T = hidden_states.shape[0]
|
||||
|
||||
# Tier 0: full ixformer pipeline (all sizes)
|
||||
if _ix is not None and _ix._bridge is not None:
|
||||
try:
|
||||
return _tier0_fused_moe(hidden_states, router_logits, w13, w2,
|
||||
top_k, num_experts, act_fn)
|
||||
except Exception as e:
|
||||
logger.warning("Tier0 MoE failed (%s), falling to Tier1/2", e)
|
||||
|
||||
# Tier 1: corex direct routed (decode T=1 only)
|
||||
if (T == 1 and _corex is not None
|
||||
and _corex.moe_direct_routed is not None
|
||||
and hidden_states.dtype == torch.float16
|
||||
and w13.dtype == torch.float16
|
||||
and w2.dtype == torch.float16
|
||||
and hidden_states.is_contiguous()
|
||||
and w13.is_contiguous()
|
||||
and w2.is_contiguous()):
|
||||
try:
|
||||
topk_logits, topk_ids = torch.topk(
|
||||
router_logits.float(), top_k, dim=-1)
|
||||
topk_weights = torch.softmax(topk_logits, dim=-1).to(
|
||||
hidden_states.dtype)
|
||||
return _tier1_decode_single_token(
|
||||
hidden_states, topk_ids[0], topk_weights[0],
|
||||
w13, w2, act_fn)
|
||||
except Exception as e:
|
||||
logger.warning("Tier1 MoE failed (%s), falling to Tier2", e)
|
||||
|
||||
# Tier 2: PyTorch fallback
|
||||
return _tier2_pytorch_loop(hidden_states, router_logits, w13, w2,
|
||||
top_k, act_fn)
|
||||
Reference in New Issue
Block a user