1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Fused SwiGLU/quantization within one grouped-expert autograd boundary."""
16
17from typing import TYPE_CHECKING, Optional
18
19import torch # pylint: disable=forbidden-backend-import
20from torch.autograd.function import once_differentiable # pylint: disable=forbidden-backend-import
21
22from hyper_parallel.components.quantization.ops.npu_mxfp8 import mxfp8_grouped_matmul
23from hyper_parallel.components.quantization.tensor import MXFP8Tensor
24
25if TYPE_CHECKING:
26 from hyper_parallel.components.quantization.quantizers.mxfp8 import MXFP8Quantizer
27
28
29def _save_operands(ctx, counts, projected, operands):
30 """Expose physical tensors to saved-tensor hooks, never carrier wrappers."""
31 ctx.operand_metadata = tuple((tuple(q.shape), q.dtype, q.quantizer) for q in operands)
32 buffers = tuple(buffer for q in operands for buffer in (q.row_data, q.row_scale, q.col_data, q.col_scale))
33 ctx.save_for_backward(counts, projected, *buffers)
34
35
36def _restore_operands(ctx):
37 """Use the same physical-buffer reconstruction as the upstream Dense/GMM path."""
38 counts, projected, *buffers = ctx.saved_tensors
39 operands = []
40 for index, (shape, dtype, quantizer) in enumerate(ctx.operand_metadata):
41 row, row_scale, col, col_scale = buffers[4 * index:4 * index + 4]
42 operands.append(MXFP8Tensor(shape, dtype, quantizer=quantizer, row_data=row,
43 row_scale=row_scale, col_data=col, col_scale=col_scale))
44 return counts, projected, operands
45
46
47def _fused_quant(projected, quantizer, counts, gradient=None):
48 """Keep prequantized operands internal to this Function's GMM calls."""
49 options = {"quant_dtype": quantizer.quant_dtype, "group_index": counts.cumsum(0).to(torch.int64)}
50 if gradient is None:
51 buffers = quantizer.npu_ops.swiglu_mx_quant_dual_axis(projected, **options)
52 shape = (projected.shape[0], projected.shape[1] // 2)
53 else:
54 buffers = quantizer.npu_ops.swiglu_backward_mx_quant_dual_axis(
55 projected, gradient.contiguous(), **options)
56 shape = projected.shape
57 row, row_scale, col, col_scale = buffers
58 return MXFP8Tensor(shape, projected.dtype, quantizer=quantizer,
59 row_data=row, row_scale=row_scale, col_data=col, col_scale=col_scale)
60
61
62def _gmm(left, right, counts, dtype, layout="NN"):
63 """Use the existing MXFP8 adapter and count-based expert partitioning."""
64 return mxfp8_grouped_matmul(left, right, layout=layout, group_list=counts,
65 group_type=2 if layout == "TN" else 0,
66 group_list_type=1, output_dtype=dtype)
67
68
69class _FusedGroupedSwiGLU(torch.autograd.Function):
70 """Own both GMMs and fusion so external autograd edges remain ordinary tensors."""
71
72 @staticmethod
73 def forward(
74 ctx: torch.autograd.function.FunctionCtx, inputs: torch.Tensor,
75 gate_up: torch.Tensor, down: torch.Tensor, counts: torch.Tensor, quantizer: "MXFP8Quantizer",
76 ) -> torch.Tensor:
77 """Compute two MXFP8 GMMs with a fused activation/quantization boundary.
78
79 Args:
80 ctx: Autograd save context.
81 inputs: Expert-major token matrix.
82 gate_up: Canonical gate/up weights.
83 down: Canonical down weights.
84 counts: Per-expert token counts.
85 quantizer: MXFP8 quantizer.
86 """
87 ctx.shapes = (inputs.shape, gate_up.shape, down.shape)
88 ctx.dtype, ctx.device, ctx.quantizer = inputs.dtype, inputs.device, quantizer
89 if inputs.shape[0] == 0:
90 ctx.save_for_backward()
91 return inputs.new_empty((0, down.shape[-1]))
92 input_q = quantizer.quantize(inputs, group_list=counts, group_list_type=1, rowwise=True, colwise=True)
93 gate_up_q = quantizer.quantize(gate_up.contiguous(), rowwise=True, colwise=True)
94 down_q = quantizer.quantize(down.contiguous(), rowwise=True, colwise=True)
95 projected = _gmm(input_q, gate_up_q, counts, inputs.dtype)
96 activated_q = _fused_quant(projected, quantizer, counts)
97 output = _gmm(activated_q, down_q, counts, inputs.dtype)
98 _save_operands(ctx, counts, projected, (input_q, gate_up_q, activated_q, down_q))
99 return output
100
101 @staticmethod
102 @once_differentiable
103 def backward(
104 ctx: torch.autograd.function.FunctionCtx, gradient: torch.Tensor,
105 ) -> tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor], None, None]:
106 """Fuse SwiGLU derivative/quantization and compute requested first gradients.
107
108 Args:
109 ctx: Saved forward context.
110 gradient: Output gradient.
111 """
112 needs_input, needs_gate, needs_down = ctx.needs_input_grad[:3]
113 if ctx.shapes[0][0] == 0:
114 grads = [torch.zeros(shape, device=ctx.device, dtype=ctx.dtype) if needed else None
115 for shape, needed in zip(ctx.shapes, (needs_input, needs_gate, needs_down))]
116 return *grads, None, None
117 counts, projected, (input_q, gate_up_q, activated_q, down_q) = _restore_operands(ctx)
118 grad_q = ctx.quantizer.quantize(
119 gradient, group_list=counts if needs_down else None, group_list_type=1,
120 rowwise=needs_input or needs_gate, colwise=needs_down)
121 grad_down = _gmm(activated_q, grad_q, counts, ctx.dtype, "TN") if needs_down else None
122 grad_input = grad_gate = None
123 if needs_input or needs_gate:
124 grad_activated = _gmm(grad_q, down_q, counts, ctx.dtype, "NT")
125 grad_projected_q = _fused_quant(projected, ctx.quantizer, counts, grad_activated)
126 if needs_input:
127 grad_input = _gmm(grad_projected_q, gate_up_q, counts, ctx.dtype, "NT")
128 if needs_gate:
129 grad_gate = _gmm(input_q, grad_projected_q, counts, ctx.dtype, "TN")
130 return grad_input, grad_gate, grad_down, None, None
131
132
133def fused_grouped_swiglu(
134 inputs: torch.Tensor, gate_up: torch.Tensor, down: torch.Tensor,
135 counts: torch.Tensor, quantizer: "MXFP8Quantizer",
136) -> torch.Tensor:
137 """Compute expert-major outputs with canonical [E,H,2I]/[E,I,H] weights.
138
139 Args:
140 inputs: BF16 expert-major token matrix.
141 gate_up: Canonical packed gate/up weights.
142 down: Canonical down weights.
143 counts: Per-local-expert token counts, including zero-token experts.
144 quantizer: MXFP8 recipe with both fused NPU kernels available.
145
146 Returns:
147 Ordinary BF16 outputs before outer routing weights are applied.
148 """
149 if inputs.dtype not in (torch.bfloat16, torch.float16):
150 raise ValueError("Fused MXFP8 SwiGLU requires BF16 or FP16 inputs")
151 if gate_up.dtype != inputs.dtype or down.dtype != inputs.dtype:
152 raise ValueError("Fused MXFP8 SwiGLU requires matching input and weight dtypes")
153 if inputs.ndim != 2 or gate_up.ndim != 3 or down.ndim != 3:
154 raise ValueError("Fused MXFP8 SwiGLU requires 2D inputs and 3D expert weights")
155 if (gate_up.shape[0] != down.shape[0] or gate_up.shape[1] != inputs.shape[1]
156 or gate_up.shape[2] != 2 * down.shape[1] or down.shape[2] != inputs.shape[1]):
157 raise ValueError("Fused MXFP8 SwiGLU weight dimensions do not match inputs")
158 if counts.ndim != 1 or counts.numel() != gate_up.shape[0] or counts.dtype not in (torch.int32, torch.int64):
159 raise ValueError("Fused MXFP8 SwiGLU requires one integer count per expert")
160 return _FusedGroupedSwiGLU.apply(inputs, gate_up, down, counts, quantizer)