Skip to content

Wire QEffGptOssMLP into shared MoEBlockMixin - #1312

Open
Shrubabati7 wants to merge 2 commits into
quic:mainfrom
Shrubabati7:gpt_oss_moe_transforms
Open

Shrubabati7 wants to merge 2 commits into
quic:mainfrom
Shrubabati7:gpt_oss_moe_transforms

Conversation

@Shrubabati7

@Shrubabati7 Shrubabati7 commented Sep 8, 2026

Copy link
Copy Markdown
  • Aligned gpt_oss MoE with MoE ubench implemetation
  • Using common MoE transforms class now in gpt_oss
  • TTFT for 2L, ctx-len 8192, prefill-seq-len 512, moe-prefill-packed-chunk-size 256: 1.10sec in SDK 1.23.0.41 assert

Comment on lines 4 to -374
@@ -88,180 +87,7 @@ def transform_weights(self) -> MoEWeights:
return self.moe_weights


class _QEffGptOssLegacyBlockedMixin:
def forward(self, hidden: torch.Tensor):
if os.environ.get("NUM_FFN_BLOCKS", None) is not None:
return self.blocked_ffn_forward(hidden)
return super().forward(hidden)

def blocked_ffn_forward(self, hidden: torch.Tensor):
if not getattr(self, "weights_transformed", False):
raise RuntimeError(f"{type(self).__name__} weights are not transformed; run OptimizedMoEWeightsTransform")
B, S, H = hidden.shape
T = B * S
hidden = hidden.view(T, H)
weights = self.moe_weights

# Router computation
router_logits = F.linear(hidden, self.router.weight, self.router.bias)

# Top-k selection
top_w, top_i = torch.topk(router_logits, self.router.top_k, dim=-1) # both [T, K]
top_w = torch.nn.functional.softmax(top_w, dim=1, dtype=top_w.dtype)

masked_logits = torch.zeros_like(router_logits)
masked_logits.scatter_(1, top_i, top_w)

# Routing weights for each expert [T, E]
routing_weights = masked_logits

# ────────────────── allocate the output tensor ─────
expert_out = hidden.new_zeros((T, H)) # accumulation buffer
target_blocks = int(os.environ.get("NUM_FFN_BLOCKS", 1))
block_positions = []
for j in range(target_blocks):
block_positions.append(j * (T // target_blocks))
# ───────────────────────── Expert computation loop ─────────────────────────────
for e in range(self.experts.num_experts):
routing_weight = routing_weights[:, e].unsqueeze(-1) # [T, 1]

W_g, W_u = weights.gate[e], weights.up[e] # [H, I], [H, I]
b_g, b_u = weights.gate_bias[e], weights.up_bias[e] # [I], [I]
W_d = weights.down[e] # [I, H]
b_d = weights.down_bias[e] # [H]

block_count = 0
outs = []
for block_idx in range(target_blocks):
block_count += 1
qi = block_positions[block_idx]

# Calculate block size (last block should be handled with remainder)
if block_idx == target_blocks - 1:
real_q_len = T - qi
else:
real_q_len = block_positions[block_idx + 1] - qi

tgb = hidden[qi : qi + real_q_len, :]
# Gate and Up projections
# Gate and Up projections
gate = (tgb @ W_g) + b_g # [T, I]
up = (tgb @ W_u) + b_u # [T, I]

# Apply GptOss activation with clamping
gate = gate.clamp(min=torch.finfo(torch.float16).min, max=self.experts.limit)
up = up.clamp(min=-self.experts.limit, max=self.experts.limit)

# GLU activation
glu = gate * torch.sigmoid(gate * self.experts.alpha)
intermediate = (up + 1) * glu # [T, I]

# Down projection
down_out_block = (intermediate @ W_d) + b_d # [T, H]

outs.append(down_out_block)

down_out = torch.cat(outs, dim=0)

# Apply routing weights and accumulate
expert_out += down_out * routing_weight

# original shape [B, S, H]
return expert_out.view(B, S, H), router_logits

def blocked_ffn_forward_block_weights(self, hidden: torch.Tensor):
if not getattr(self, "weights_transformed", False):
raise RuntimeError(f"{type(self).__name__} weights are not transformed; run OptimizedMoEWeightsTransform")
B, S, H = hidden.shape
T = B * S
hidden = hidden.view(T, H)
weights = self.moe_weights

# Router computation
router_logits = F.linear(hidden, self.router.weight, self.router.bias)

# Top-k selection
top_w, top_i = torch.topk(router_logits, self.router.top_k, dim=-1) # both [T, K]
top_w = torch.nn.functional.softmax(top_w, dim=1, dtype=top_w.dtype)

masked_logits = torch.zeros_like(router_logits)
masked_logits.scatter_(1, top_i, top_w)

# Routing weights for each expert [T, E]
routing_weights = masked_logits

# ────────────────── allocate the output tensor ─────
expert_out = hidden.new_zeros((T, H)) # accumulation buffer
target_blocks = int(os.environ.get("NUM_BLOCKS", 1))
block_positions = []
for j in range(target_blocks):
block_positions.append(j * (T // target_blocks))
# ───────────────────────── Expert computation loop ─────────────────────────────
for e in range(self.experts.num_experts):
routing_weight = routing_weights[:, e].unsqueeze(-1) # [T, 1]

W_g, W_u = weights.gate[e], weights.up[e] # [H, I], [H, I]
b_g, b_u = weights.gate_bias[e], weights.up_bias[e] # [I], [I]
W_d = weights.down[e] # [I, H]
b_d = weights.down_bias[e] # [H]

block_count = 0
outs = []
for block_idx in range(target_blocks):
block_count += 1
qi = block_positions[block_idx]

# Calculate block size (last block should be handled with remainder)
if block_idx == target_blocks - 1:
real_q_len = T - qi
else:
real_q_len = block_positions[block_idx + 1] - qi

tgb = hidden[qi : qi + real_q_len, :]
# Gate and Up projections

wg_col_shape = W_g.shape[1]
wg_num_blocks = math.ceil(wg_col_shape / 128)
last_block_size = wg_col_shape % 128 if wg_col_shape % 128 != 0 else 128

intermediates = []
for i in range(wg_num_blocks):
if i == wg_num_blocks - 1:
cur_gate = (tgb @ W_g[:, -last_block_size:]) + b_g[-last_block_size:]
cur_up = (tgb @ W_u[:, -last_block_size:]) + b_u[-last_block_size:]
else:
cur_gate = (tgb @ W_g[:, i * 128 : (i + 1) * 128]) + b_g[i * 128 : (i + 1) * 128]
cur_up = (tgb @ W_u[:, i * 128 : (i + 1) * 128]) + b_u[i * 128 : (i + 1) * 128]

cur_gate = cur_gate.clamp(min=torch.finfo(torch.float16).min, max=self.experts.limit)
cur_up = cur_up.clamp(min=-self.experts.limit, max=self.experts.limit)
cur_glu = cur_gate * torch.sigmoid(cur_gate * self.experts.alpha)
cur_intermediate = (cur_up + 1) * cur_glu
intermediates.append(cur_intermediate)

intermediate = torch.cat(intermediates, dim=-1)

downs = []
for i in range(wg_num_blocks):
if i == wg_num_blocks - 1:
downs.append((intermediate @ W_d[:, -last_block_size:]) + b_d[-last_block_size:])
else:
downs.append((intermediate @ W_d[:, i * 128 : (i + 1) * 128]) + b_d[i * 128 : (i + 1) * 128])

down_out_block = torch.cat(downs, dim=1)
outs.append(down_out_block)

down_out = torch.cat(outs, dim=0)

# Apply routing weights and accumulate
masked_down = torch.where(routing_weight > 0, down_out * routing_weight, torch.zeros_like(expert_out))
expert_out += masked_down

# original shape [B, S, H]
return expert_out.view(B, S, H), router_logits


class QEffGptOssMLP(_QEffGptOssLegacyBlockedMixin, QEffMoEBlockMixin, GptOssMLP):
class QEffGptOssMLP(QEffMoEBlockMixin, GptOssMLP):
_moe_return_router_logits = True
supported_moe_flavours = (
MoEFlavour.SIMPLE_LOOP,
@@ -286,164 +112,12 @@ def moe_profile(self) -> MoEProfile:

def route(self, x: torch.Tensor):
router_logits = F.linear(x, self.router.weight, self.router.bias)
top_w, top_i = torch.topk(router_logits, self.router.top_k, dim=-1)
top_w = F.softmax(top_w, dim=1, dtype=top_w.dtype)
probs = F.softmax(router_logits, dim=-1, dtype=torch.float)
top_w, top_i = torch.topk(probs, self.router.top_k, dim=-1)
top_w = top_w / top_w.sum(dim=-1, keepdim=True)
top_w = top_w.to(x.dtype)
return (top_i, top_w), router_logits

# ------------------- Gather based, weights as activation approach ---------------
def forward_weights_as_activation(self, hidden_states):
if not getattr(self, "weights_transformed", False):
raise RuntimeError(f"{type(self).__name__} weights are not transformed; run OptimizedMoEWeightsTransform")
bs, seq_len, _ = hidden_states.shape
hidden_states = hidden_states.view(bs * seq_len, self.experts.hidden_size)
weights = self.moe_weights

# Router computation
router_logits = F.linear(hidden_states, self.router.weight, self.router.bias)
router_top_value, router_indices = torch.topk(router_logits, self.router.top_k, dim=-1)
router_top_value = torch.nn.functional.softmax(router_top_value, dim=1, dtype=router_top_value.dtype)

# GATHER - collect weights for selected experts
gate_proj = weights.gate[router_indices.flatten()]
up_proj = weights.up[router_indices.flatten()]
gate_proj_bias = weights.gate_bias[router_indices.flatten()]
up_proj_bias = weights.up_bias[router_indices.flatten()]
down_proj = weights.down[router_indices.flatten()]
down_proj_bias = weights.down_bias[router_indices.flatten()]

# Apply Chosen Experts (without routing weights first)
# expert_in = hidden_states.repeat_interleave(self.router.top_k, dim=0)
# expert_in = expert_in.view(-1, 1, self.experts.hidden_size)
# Reshape for bmm: (bs*seq_len*top_k, 1, hidden_size)
expert_in = (
hidden_states.unsqueeze(1)
.expand(-1, self.router.top_k, -1)
.contiguous()
.view(-1, 1, self.experts.hidden_size)
)

gate = torch.bmm(expert_in, gate_proj) + gate_proj_bias.unsqueeze(1)
up = torch.bmm(expert_in, up_proj) + up_proj_bias.unsqueeze(1)

# Apply activation with clamping
gate = gate.clamp(min=None, max=self.experts.limit)
up = up.clamp(min=-self.experts.limit, max=self.experts.limit)
glu = gate * torch.sigmoid(gate * self.experts.alpha)
gated_output = (up + 1) * glu

experts_out = torch.bmm(gated_output, down_proj) + down_proj_bias.unsqueeze(1)
experts_out = experts_out.view(bs * seq_len, self.router.top_k, self.experts.hidden_size)

# Apply routing weights AFTER expert computation (This is before on Llama4)
experts_out = experts_out * router_top_value.unsqueeze(-1)
experts_out = torch.einsum("bnd->bd", experts_out)

return experts_out, router_logits

def forward(self, hidden_states):
if os.environ.get("NUM_FFN_BLOCKS", None) is not None:
return self.blocked_ffn_forward(hidden_states)
return QEffMoEBlockMixin.forward(self, hidden_states)

def optimized_moe_forward(self, hidden_states: torch.Tensor):
if not getattr(self, "weights_transformed", False):
raise RuntimeError(f"{type(self).__name__} weights are not transformed; run OptimizedMoEWeightsTransform")
B, S, H = hidden_states.shape
T = B * S
hidden_states = hidden_states.view(T, H)
weights = self.moe_weights

# Router computation
router_logits = F.linear(hidden_states, self.router.weight, self.router.bias)

# Top-k selection
top_w, selected_experts = torch.topk(router_logits, self.router.top_k, dim=-1) # both [T, K]
top_w = torch.nn.functional.softmax(top_w, dim=1, dtype=top_w.dtype)

# Creating experts mask and routing weights masked
awesome_experts_mask_1 = (
torch.nn.functional.one_hot(selected_experts[:, 0], num_classes=self.experts.num_experts)
.bool()
.T.unsqueeze(-1)
)
awesome_experts_mask_2 = (
torch.nn.functional.one_hot(selected_experts[:, 1], num_classes=self.experts.num_experts)
.bool()
.T.unsqueeze(-1)
)
awesome_experts_mask_3 = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes the only GPT-OSS runtime use of NUM_FFN_BLOCKS: the shared MoE path does not consume it, and the new MoE config options are not a compatibility mapping for its token-blocking behavior.

If removing this legacy control is intentional, please also remove its stale handling from modeling_auto.py (export sizing/hash) and update examples/disagg_serving/README.md and if backward compatibility is needed put the warning as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored: QEffGptOssMLP.execute_moe_flavour now overrides the flavour dispatch unconditionally when NUM_FFN_BLOCKS is set (matching the old mixin behaviour), modeling_auto.py hash/sizing logic retained with docstring, and README.md updated to reflect the token-blocking semantics.

module.expert_blocking_packed_chunk_size = expert_parallel_chunk_size
expert_intermediate_block_size = moe_config.get("expert_intermediate_block_size", None)
if expert_intermediate_block_size is not None:
expert_intermediate_block_size = int(expert_intermediate_block_size)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can put a check for negative values and reject non-positive values, as this value is passed through int() before being used as the tile-loop step. This is mainly for robustness.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change done: added if expert_intermediate_block_size <= 0: raise ValueError(...) immediately after the int() cast in OptimizedMoEExportConfigTransform.

Comment thread QEfficient/transformers/moe/flavours.py Outdated
packed_chunk_size = max(1, min(seq_len // num_packed_chunks, seq_len))
matched_idx = build_matched_idx_from_cumsum(T2Ei)
valid_rows = torch.einsum("ij->i", T2Ei.to(torch.int32)).unsqueeze(1)
valid_rows = T2Ei.to(torch.int32).sum(dim=1, keepdim=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.sum() will fail on current mainline, did you validate this?

Comment thread QEfficient/transformers/moe/flavours.py Outdated
f"seq_len={seq_len} must be divisible by num_packed_chunks={num_packed_chunks}"
)
packed_chunk_size = seq_len // num_packed_chunks
packed_chunk_size = max(1, min(seq_len // num_packed_chunks, seq_len))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this pls remove, since num_packed_chunks is computed during the MoE transform and the export sequence length is subsequently rounded to a multiple of it in modeling_auto.py.

@Shrubabati7
Shrubabati7 force-pushed the gpt_oss_moe_transforms branch 3 times, most recently from 516d9fa to 8084de8 Compare September 15, 2026 03:30
Shrubabati Moitra added 2 commits September 15, 2026 11:18
…, EXPERT_PARALLEL) using gptoss_clamped_glu_mlp profile.

Drop bespoke prefill/decode MoE implementations; add expert_intermediate_block_size support and TTFT reporting.
Switch einsum reductions to .sum() following 2dae7e3.

Signed-off-by: Shrubabati Moitra <moitra@qti.qualcomm.com>
- route(): topk raw logits first then softmax over top-k only,
  matching HF GptOssTopKRouter (was softmax-all then topk then
  redundant renorm)
- flavours.py: remove unused chunk_rows variable (ruff F841)
- ruff format pass

Signed-off-by: Shrubabati Moitra <moitra@qti.qualcomm.com>
@vbaddi
vbaddi force-pushed the gpt_oss_moe_transforms branch from d618830 to 439b90d Compare September 15, 2026 05:48
@vbaddi

vbaddi commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

CI-Ready

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants