profile decode path and optimize tiny batch moe
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
# Text-only (no VL, no MTP).
|
||||
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
@@ -41,6 +44,59 @@ from vllm.model_executor.models.interfaces import HasInnerState, SupportsLoRA
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_ENGINEX_PROFILE_ENABLED = os.getenv("ENGINEX_PROFILE_DECODE", "0") == "1"
|
||||
_ENGINEX_PROFILE_EVERY = int(os.getenv("ENGINEX_PROFILE_EVERY", "32"))
|
||||
_ENGINEX_PROFILE_SYNC = os.getenv("ENGINEX_PROFILE_SYNC", "1") != "0"
|
||||
_enginex_profile_stats: Dict[str, List[float]] = {}
|
||||
_enginex_profile_steps = 0
|
||||
_enginex_profile_mode = "unknown"
|
||||
|
||||
|
||||
def _enginex_profile_active() -> bool:
|
||||
return _ENGINEX_PROFILE_ENABLED and torch.cuda.is_available()
|
||||
|
||||
|
||||
def _enginex_profile_sync() -> None:
|
||||
if _ENGINEX_PROFILE_SYNC:
|
||||
torch.cuda.synchronize()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _enginex_profile(label: str):
|
||||
if not _enginex_profile_active():
|
||||
yield
|
||||
return
|
||||
label = f"{_enginex_profile_mode}.{label}"
|
||||
_enginex_profile_sync()
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_enginex_profile_sync()
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
||||
stat = _enginex_profile_stats.setdefault(label, [0.0, 0.0])
|
||||
stat[0] += elapsed_ms
|
||||
stat[1] += 1.0
|
||||
|
||||
|
||||
def _enginex_profile_log(mode: str) -> None:
|
||||
global _enginex_profile_steps
|
||||
if not _enginex_profile_active():
|
||||
return
|
||||
_enginex_profile_steps += 1
|
||||
if _enginex_profile_steps % max(_ENGINEX_PROFILE_EVERY, 1) != 0:
|
||||
return
|
||||
tp_rank = get_tensor_model_parallel_rank()
|
||||
parts = []
|
||||
for label, (total_ms, count) in sorted(
|
||||
_enginex_profile_stats.items(),
|
||||
key=lambda item: item[1][0],
|
||||
reverse=True):
|
||||
avg_ms = total_ms / max(count, 1.0)
|
||||
parts.append(f"{label}: total={total_ms:.2f}ms avg={avg_ms:.3f}ms n={int(count)}")
|
||||
logger.info("[ENGINEX_PROFILE_QWEN] rank=%d steps=%d mode=%s %s",
|
||||
tp_rank, _enginex_profile_steps, mode, " | ".join(parts))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure-PyTorch DeltaNet kernels (fallbacks from transformers 5.2.0)
|
||||
@@ -613,44 +669,48 @@ class Qwen3_5FullAttention(nn.Module):
|
||||
total_tokens = hidden_states.shape[0]
|
||||
|
||||
# q_proj output includes gate (dim doubled)
|
||||
qg, _ = self.q_proj(hidden_states) # (total, local_num_heads * head_dim * 2)
|
||||
qg = qg.view(total_tokens, self.local_num_heads, self.head_dim * 2)
|
||||
q = qg[:, :, :self.head_dim].reshape(total_tokens, -1)
|
||||
gate = qg[:, :, self.head_dim:].reshape(total_tokens, -1)
|
||||
with _enginex_profile("full_attn.qkv_proj"):
|
||||
qg, _ = self.q_proj(hidden_states) # (total, local_num_heads * head_dim * 2)
|
||||
qg = qg.view(total_tokens, self.local_num_heads, self.head_dim * 2)
|
||||
q = qg[:, :, :self.head_dim].reshape(total_tokens, -1)
|
||||
gate = qg[:, :, self.head_dim:].reshape(total_tokens, -1)
|
||||
|
||||
k, _ = self.k_proj(hidden_states) # (total, proj_kv_heads * head_dim)
|
||||
v, _ = self.v_proj(hidden_states)
|
||||
k, _ = self.k_proj(hidden_states) # (total, proj_kv_heads * head_dim)
|
||||
v, _ = self.v_proj(hidden_states)
|
||||
|
||||
# q_norm on local Q heads
|
||||
q = self.q_norm.forward_cuda(
|
||||
q.view(total_tokens, self.local_num_heads, self.head_dim)
|
||||
.contiguous()).view(total_tokens, -1)
|
||||
with _enginex_profile("full_attn.norm_rope"):
|
||||
q = self.q_norm.forward_cuda(
|
||||
q.view(total_tokens, self.local_num_heads, self.head_dim)
|
||||
.contiguous()).view(total_tokens, -1)
|
||||
|
||||
# GQA-aware TP: select rank-local KV head BEFORE k_norm and rope so
|
||||
# that ixformer kernels always see num_kv_heads=1 (same as 27B path).
|
||||
# Doing k_norm/rope on 2 KV heads (proj_kv_heads=2) triggers ixformer
|
||||
# paths that can produce NaN; restricting to 1 head avoids the issue.
|
||||
if self.q_per_kv_global is not None:
|
||||
tp_rank = get_tensor_model_parallel_rank()
|
||||
kv_idx = (tp_rank * self.local_num_heads) // self.q_per_kv_global
|
||||
k = (k.view(total_tokens, self.proj_kv_heads, self.head_dim)
|
||||
[:, kv_idx, :].contiguous()) # (T, head_dim) — 1 head
|
||||
v = (v.view(total_tokens, self.proj_kv_heads, self.head_dim)
|
||||
[:, kv_idx, :].contiguous()) # (T, head_dim) — 1 head
|
||||
# GQA-aware TP: select rank-local KV head BEFORE k_norm and rope so
|
||||
# that ixformer kernels always see num_kv_heads=1 (same as 27B path).
|
||||
# Doing k_norm/rope on 2 KV heads (proj_kv_heads=2) triggers ixformer
|
||||
# paths that can produce NaN; restricting to 1 head avoids the issue.
|
||||
if self.q_per_kv_global is not None:
|
||||
tp_rank = get_tensor_model_parallel_rank()
|
||||
kv_idx = (tp_rank * self.local_num_heads) // self.q_per_kv_global
|
||||
k = (k.view(total_tokens, self.proj_kv_heads, self.head_dim)
|
||||
[:, kv_idx, :].contiguous()) # (T, head_dim) — 1 head
|
||||
v = (v.view(total_tokens, self.proj_kv_heads, self.head_dim)
|
||||
[:, kv_idx, :].contiguous()) # (T, head_dim) — 1 head
|
||||
|
||||
# k_norm on the (now always 1) rank-local KV head
|
||||
k = self.k_norm.forward_cuda(
|
||||
k.view(total_tokens, self.local_num_kv_heads, self.head_dim)
|
||||
.contiguous()).view(total_tokens, -1)
|
||||
# k_norm on the (now always 1) rank-local KV head
|
||||
k = self.k_norm.forward_cuda(
|
||||
k.view(total_tokens, self.local_num_kv_heads, self.head_dim)
|
||||
.contiguous()).view(total_tokens, -1)
|
||||
|
||||
# rope: q=(T, local_num_heads*head_dim), k=(T, 1*head_dim) — mirrors 27B
|
||||
q, k = self.rotary_emb(positions, q, k)
|
||||
# rope: q=(T, local_num_heads*head_dim), k=(T, 1*head_dim) — mirrors 27B
|
||||
q, k = self.rotary_emb(positions, q, k)
|
||||
|
||||
attn_out = self.attn(q, k, v, kv_cache, attn_metadata)
|
||||
with _enginex_profile("full_attn.paged_attention"):
|
||||
attn_out = self.attn(q, k, v, kv_cache, attn_metadata)
|
||||
|
||||
# Multiply by sigmoid gate before output projection
|
||||
attn_out = attn_out * torch.sigmoid(gate.float()).to(attn_out.dtype)
|
||||
output, _ = self.o_proj(attn_out)
|
||||
with _enginex_profile("full_attn.gate_o_proj"):
|
||||
attn_out = attn_out * torch.sigmoid(gate.float()).to(attn_out.dtype)
|
||||
output, _ = self.o_proj(attn_out)
|
||||
return output
|
||||
|
||||
|
||||
@@ -754,12 +814,13 @@ class Qwen3_5MoeSparseBlock(nn.Module):
|
||||
Output is partial (pre-all-reduce), same contract as FusedMoE
|
||||
with reduce_results=False.
|
||||
"""
|
||||
# Routing: softmax → topk → renormalise
|
||||
routing_weights = torch.softmax(router_logits.float(), dim=-1)
|
||||
topk_weights, topk_ids = torch.topk(
|
||||
routing_weights, self.top_k, dim=-1) # (T, top_k)
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
||||
topk_weights = topk_weights.to(hidden_states.dtype)
|
||||
# Routing: softmax -> topk -> renormalise
|
||||
with _enginex_profile("moe.routing_topk"):
|
||||
routing_weights = torch.softmax(router_logits.float(), dim=-1)
|
||||
topk_weights, topk_ids = torch.topk(
|
||||
routing_weights, self.top_k, dim=-1) # (T, top_k)
|
||||
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
|
||||
topk_weights = topk_weights.to(hidden_states.dtype)
|
||||
|
||||
w13 = self.experts.w13_weight # (E, 2*I, H)
|
||||
w2 = self.experts.w2_weight # (E, H, I)
|
||||
@@ -771,59 +832,90 @@ class Qwen3_5MoeSparseBlock(nn.Module):
|
||||
# gate_up: 1 large GEMM (1,H) × (K*2*I,H)^T → (1, K*2*I)
|
||||
# down: 1 bmm (K,H,I) @ (K,I,1) → (K,H)
|
||||
# Total: 3 kernel launches vs previous 16 (top_k*2).
|
||||
eids = topk_ids[0] # (K,)
|
||||
ws = topk_weights[0].to(hidden_states.dtype) # (K,)
|
||||
w13_sel = w13[eids] # (K, 2*I, H)
|
||||
w2_sel = w2[eids] # (K, H, I)
|
||||
with _enginex_profile("moe.routed_decode_experts"):
|
||||
eids = topk_ids[0] # (K,)
|
||||
ws = topk_weights[0].to(hidden_states.dtype) # (K,)
|
||||
w13_sel = w13[eids] # (K, 2*I, H)
|
||||
w2_sel = w2[eids] # (K, H, I)
|
||||
|
||||
H = hidden_states.shape[-1]
|
||||
H = hidden_states.shape[-1]
|
||||
|
||||
gate_up = F.linear(
|
||||
hidden_states,
|
||||
w13_sel.reshape(-1, H), # (K*2*I, H) — contiguous after indexing
|
||||
) # (1, K*2*I)
|
||||
gate_up = gate_up.view(self.top_k, -1) # (K, 2*I)
|
||||
gate, up = gate_up.chunk(2, dim=-1) # (K, I) each
|
||||
act = F.silu(gate) * up # (K, I)
|
||||
gate_up = F.linear(
|
||||
hidden_states,
|
||||
w13_sel.reshape(-1, H), # (K*2*I, H) - contiguous after indexing
|
||||
) # (1, K*2*I)
|
||||
gate_up = gate_up.view(self.top_k, -1) # (K, 2*I)
|
||||
gate, up = gate_up.chunk(2, dim=-1) # (K, I) each
|
||||
act = F.silu(gate) * up # (K, I)
|
||||
|
||||
# bmm: (K,H,I) @ (K,I,1) → (K,H,1) → (K,H)
|
||||
expert_out = torch.bmm(w2_sel, act.unsqueeze(-1)).squeeze(-1) # (K, H)
|
||||
# bmm: (K,H,I) @ (K,I,1) -> (K,H,1) -> (K,H)
|
||||
expert_out = torch.bmm(w2_sel, act.unsqueeze(-1)).squeeze(-1) # (K, H)
|
||||
|
||||
out = (expert_out * ws.unsqueeze(-1)).sum(0, keepdim=True).to(
|
||||
hidden_states.dtype) # (1, H)
|
||||
out = (expert_out * ws.unsqueeze(-1)).sum(0, keepdim=True).to(
|
||||
hidden_states.dtype) # (1, H)
|
||||
elif T <= 2:
|
||||
# Fast path: tiny decode batch. With max_num_seqs=2, normal decode
|
||||
# often has T=2 and should not fall back to the Python per-expert
|
||||
# loop used for long prefill chunks.
|
||||
with _enginex_profile("moe.routed_tiny_batch_experts"):
|
||||
H = hidden_states.shape[-1]
|
||||
flat_eids = topk_ids.reshape(-1) # (T*K,)
|
||||
flat_ws = topk_weights.reshape(-1).to(hidden_states.dtype)
|
||||
|
||||
w13_sel = w13[flat_eids] # (T*K, 2*I, H)
|
||||
w2_sel = w2[flat_eids] # (T*K, H, I)
|
||||
x = (hidden_states[:, None, :]
|
||||
.expand(T, self.top_k, H)
|
||||
.reshape(-1, 1, H)) # (T*K, 1, H)
|
||||
|
||||
gate_up = torch.bmm(
|
||||
x, w13_sel.transpose(1, 2)).squeeze(1) # (T*K, 2*I)
|
||||
gate, up = gate_up.chunk(2, dim=-1)
|
||||
act = F.silu(gate) * up # (T*K, I)
|
||||
expert_out = torch.bmm(
|
||||
w2_sel, act.unsqueeze(-1)).squeeze(-1) # (T*K, H)
|
||||
|
||||
out = (expert_out * flat_ws.unsqueeze(-1)).view(
|
||||
T, self.top_k, H).sum(1).to(hidden_states.dtype)
|
||||
else:
|
||||
# General path (prefill / multi-seq): loop over unique active experts.
|
||||
# At most T*top_k unique experts, always <= num_experts.
|
||||
out = torch.zeros_like(hidden_states)
|
||||
unique_eids = topk_ids.view(-1).unique().tolist()
|
||||
for eid in unique_eids:
|
||||
eid = int(eid)
|
||||
mask = (topk_ids == eid) # (T, top_k)
|
||||
tok_ids, topk_pos = mask.nonzero(as_tuple=True)
|
||||
tokens = hidden_states[tok_ids] # (n, H)
|
||||
gate_up = F.linear(tokens, w13[eid]) # (n, 2*I)
|
||||
gate, up = gate_up.chunk(2, dim=-1)
|
||||
act = F.silu(gate) * up # (n, I)
|
||||
expert_out = F.linear(act, w2[eid]) # (n, H)
|
||||
weights = topk_weights[tok_ids, topk_pos].unsqueeze(-1)
|
||||
out.index_add_(0, tok_ids, (expert_out * weights).to(out.dtype))
|
||||
with _enginex_profile("moe.routed_prefill_experts"):
|
||||
out = torch.zeros_like(hidden_states)
|
||||
unique_eids = topk_ids.view(-1).unique().tolist()
|
||||
for eid in unique_eids:
|
||||
eid = int(eid)
|
||||
mask = (topk_ids == eid) # (T, top_k)
|
||||
tok_ids, topk_pos = mask.nonzero(as_tuple=True)
|
||||
tokens = hidden_states[tok_ids] # (n, H)
|
||||
gate_up = F.linear(tokens, w13[eid]) # (n, 2*I)
|
||||
gate, up = gate_up.chunk(2, dim=-1)
|
||||
act = F.silu(gate) * up # (n, I)
|
||||
expert_out = F.linear(act, w2[eid]) # (n, H)
|
||||
weights = topk_weights[tok_ids, topk_pos].unsqueeze(-1)
|
||||
out.index_add_(0, tok_ids, (expert_out * weights).to(out.dtype))
|
||||
|
||||
return out # partial, all-reduce done in forward()
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
router_logits, _ = self.gate(hidden_states)
|
||||
routed_out = self._pure_pytorch_experts(hidden_states, router_logits)
|
||||
with _enginex_profile("moe.gate"):
|
||||
router_logits, _ = self.gate(hidden_states)
|
||||
with _enginex_profile("moe.routed_total"):
|
||||
routed_out = self._pure_pytorch_experts(hidden_states, router_logits)
|
||||
|
||||
gate_up, _ = self.shared_expert_gate_up(hidden_states)
|
||||
shared_out = self.act_fn(gate_up)
|
||||
shared_out, _ = self.shared_expert_down(shared_out)
|
||||
# Scalar sigmoid gate (Qwen2-MoE / Qwen3.5-MoE style)
|
||||
gate_score, _ = self.shared_expert_gate(hidden_states) # (T, 1)
|
||||
shared_out = shared_out * torch.sigmoid(gate_score)
|
||||
with _enginex_profile("moe.shared_expert"):
|
||||
gate_up, _ = self.shared_expert_gate_up(hidden_states)
|
||||
shared_out = self.act_fn(gate_up)
|
||||
shared_out, _ = self.shared_expert_down(shared_out)
|
||||
# Scalar sigmoid gate (Qwen2-MoE / Qwen3.5-MoE style)
|
||||
gate_score, _ = self.shared_expert_gate(hidden_states) # (T, 1)
|
||||
shared_out = shared_out * torch.sigmoid(gate_score)
|
||||
|
||||
out = routed_out + shared_out
|
||||
with _enginex_profile("moe.combine"):
|
||||
out = routed_out + shared_out
|
||||
if self.experts.tp_size > 1:
|
||||
out = tensor_model_parallel_all_reduce(out)
|
||||
with _enginex_profile("moe.tp_all_reduce"):
|
||||
out = tensor_model_parallel_all_reduce(out)
|
||||
return out
|
||||
|
||||
|
||||
@@ -883,21 +975,27 @@ class Qwen3_5DecoderLayer(nn.Module):
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
if residual is None:
|
||||
residual = hidden_states
|
||||
hidden_states = self.input_layernorm(hidden_states)
|
||||
with _enginex_profile("layer.input_norm"):
|
||||
hidden_states = self.input_layernorm(hidden_states)
|
||||
else:
|
||||
hidden_states, residual = self.input_layernorm(hidden_states, residual)
|
||||
with _enginex_profile("layer.input_norm"):
|
||||
hidden_states, residual = self.input_layernorm(hidden_states, residual)
|
||||
|
||||
if self.layer_type == "linear_attention":
|
||||
hidden_states = self.linear_attn(
|
||||
hidden_states, attn_metadata, conv_state, temporal_state)
|
||||
with _enginex_profile("layer.linear_attention"):
|
||||
hidden_states = self.linear_attn(
|
||||
hidden_states, attn_metadata, conv_state, temporal_state)
|
||||
else:
|
||||
hidden_states = self.self_attn(
|
||||
positions, hidden_states, kv_cache, attn_metadata)
|
||||
with _enginex_profile("layer.full_attention"):
|
||||
hidden_states = self.self_attn(
|
||||
positions, hidden_states, kv_cache, attn_metadata)
|
||||
|
||||
hidden_states, residual = self.post_attention_layernorm(
|
||||
hidden_states, residual)
|
||||
with _enginex_profile("layer.post_attn_norm"):
|
||||
hidden_states, residual = self.post_attention_layernorm(
|
||||
hidden_states, residual)
|
||||
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
with _enginex_profile("layer.mlp"):
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
|
||||
return hidden_states, residual
|
||||
|
||||
@@ -934,6 +1032,9 @@ class Qwen3_5Model(nn.Module):
|
||||
conv_states: torch.Tensor, # (num_linear_layers, batch, ...)
|
||||
temporal_states: torch.Tensor, # (num_linear_layers, batch, ...)
|
||||
) -> torch.Tensor:
|
||||
global _enginex_profile_mode
|
||||
mode = "prefill" if attn_metadata.num_prefill_tokens > 0 else "decode"
|
||||
_enginex_profile_mode = mode
|
||||
hidden_states = self.embed_tokens(input_ids)
|
||||
residual = None
|
||||
|
||||
@@ -961,6 +1062,7 @@ class Qwen3_5Model(nn.Module):
|
||||
attn_idx += 1
|
||||
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
_enginex_profile_log(mode)
|
||||
return hidden_states
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user