Files
wehub-resource-sync ec436095dd
Book-CI / test (macos-latest) (push) Waiting to run
Deploy / deploy (macos-latest) (push) Waiting to run
Deploy / deploy (ubuntu-latest) (push) Waiting to run
Deploy / deploy (windows-latest) (push) Waiting to run
Release to PyPI / Build & publish sglang-kt (push) Waiting to run
Release to PyPI / Build kt-kernel (Python 3.11) (push) Waiting to run
Release to PyPI / Build kt-kernel (Python 3.12) (push) Waiting to run
Release to PyPI / Publish kt-kernel to PyPI (push) Blocked by required conditions
Book-CI / test (ubuntu-latest) (push) Waiting to run
Book-CI / test (windows-latest) (push) Waiting to run
Release Fake Tag / publish (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:30:03 +08:00

355 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import math
import os
import sys
from typing import Dict
sys.path.insert(0, os.path.dirname(__file__) + "/../build")
import torch
from kt_kernel import kt_kernel_ext
torch.manual_seed(42)
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
expert_num = 16
num_experts_per_tok = 8
layer_num = 1
CPUInfer = kt_kernel_ext.CPUInfer(40)
validation_iter = 3
k_group_size = 32
debug_print_count = 16
# Forward dispatch in do_gate_up_gemm uses `qlen > 4 * expert_num / top_k`
# (= 8 with these constants), so qlen=1 hits mat-vec and qlen=32 hits the
# mat-mat 4×4 register tile (per-expert avg m = qlen*top_k/expert_num = 16).
QLEN_LIST = [1, 32]
DISPATCH_THRESHOLD = 4 * expert_num / num_experts_per_tok
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
# E2M1 values: {0, ±0.5, ±1.0, ±1.5, ±2.0, ±3.0, ±4.0, ±6.0}
E2M1_VALUES = torch.tensor([
0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
-0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0,
], dtype=torch.float32)
# Nibble encoding: index = 4-bit value
# 0b0000..0b0111 = positive, 0b1000..0b1111 = negative
E2M1_NIBBLE_MAP = torch.tensor([
0, # 0b0000 = 0.0
1, # 0b0001 = 0.5
2, # 0b0010 = 1.0
3, # 0b0011 = 1.5
4, # 0b0100 = 2.0
5, # 0b0101 = 3.0
6, # 0b0110 = 4.0
7, # 0b0111 = 6.0
8, # 0b1000 = -0.0
9, # 0b1001 = -0.5
10, # 0b1010 = -1.0
11, # 0b1011 = -1.5
12, # 0b1100 = -2.0
13, # 0b1101 = -3.0
14, # 0b1110 = -4.0
15, # 0b1111 = -6.0
], dtype=torch.int32)
def _pattern_uniform(groups: int) -> torch.Tensor:
return torch.full((groups,), 0.02, dtype=torch.float32)
def _pattern_alternating(groups: int) -> torch.Tensor:
vals = torch.full((groups,), 0.015, dtype=torch.float32)
vals[1::2] = 0.03
return vals
def _pattern_ramp(groups: int) -> torch.Tensor:
return torch.linspace(0.005, 0.04, steps=groups, dtype=torch.float32)
WEIGHT_PATTERNS = {
"uniform_scale": ("All k-groups share the same abs max / scale", _pattern_uniform),
"alternating_scale": ("Alternate small / large abs max per k-group", _pattern_alternating),
"ramp_scale": ("Linearly increasing abs max per k-group", _pattern_ramp),
"random": ("Random bf16 weights (baseline)", None),
}
def act_fn(x):
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
ret = torch.mm(intermediate, down_proj.t())
return ret
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
def quantize_mxfp4_tensor(weights: torch.Tensor, group_size: int):
"""
MXFP4 E2M1 quantization per k-group.
For each block of group_size (32) elements in K dimension:
scale = max_abs / 6.0
quantized = round(value / scale) to nearest E2M1 value
Args:
weights: [expert_num, rows (N), cols (K)] in bf16
Returns:
packed: int32 tensor storing 8 FP4 nibbles per int32, shape [expert_num, rows * (cols // 8)]
scales: bfloat16 tensor with shape [expert_num, rows * (cols // group_size)]
"""
weights_f32 = weights.to(torch.float32)
e, rows, cols = weights_f32.shape
if cols % group_size != 0 or cols % 2 != 0:
raise ValueError(f"cols ({cols}) must be divisible by group_size ({group_size}) and 2")
reshaped = weights_f32.view(e, rows, cols // group_size, group_size)
max_abs = reshaped.abs().amax(dim=-1, keepdim=True)
max_abs = torch.clamp(max_abs, min=1e-8)
scales = (max_abs / 6.0).squeeze(-1)
# Quantize: round(value / scale) to nearest E2M1 value
normalized = reshaped / scales.unsqueeze(-1)
# For each normalized value, find the closest E2M1 value
# E2M1_VALUES shape: [16]
e2m1_vals = E2M1_VALUES.view(1, 1, 1, 1, 16) # broadcast over (e, rows, groups, group_size, 16)
normalized_expanded = normalized.unsqueeze(-1) # (e, rows, groups, group_size, 1)
distances = torch.abs(normalized_expanded - e2m1_vals)
closest_indices = distances.argmin(dim=-1) # (e, rows, groups, group_size) — indices 0..15
# Dequantized values for reference
dequant = E2M1_VALUES[closest_indices].to(torch.float32) * scales.unsqueeze(-1)
dequant = dequant.view(e, rows, cols)
# Pack nibbles: each byte = (hi_nibble << 4) | lo_nibble
# Column-major: consecutive K elements are consecutive nibbles
# nibble at even K index goes to low nibble, odd K index goes to high nibble
# But wait — looking at the kernel's mxfp4_to_bf16_32:
# lo = packed & 0x0F, hi = (packed >> 4) & 0x0F
# And the interleaving is: [lo[0],hi[0], lo[1],hi[1], ...] = column order
# So for column-major layout: each byte has lo nibble = element at col c, hi nibble = element at col c+1
# But the weight buffer layout is: b_row[k_block] = 16 packed bytes covering 32 K elements
# With column-major: K is the innermost dimension
# For 32 elements per k_group: byte 0 = [K_0 | K_1], byte 1 = [K_2 | K_3], ..., byte 15 = [K_30 | K_31]
# low nibble = even K index, high nibble = odd K index
nibbles = closest_indices.to(torch.uint8) # 0..15, each is already a 4-bit value
nibbles = nibbles.view(e, rows, cols // 2, 2)
lo = nibbles[..., 0] # even K indices
hi = nibbles[..., 1] # odd K indices
packed_bytes = (hi << 4) | lo # low nibble first in memory (little-endian style)
# Pack 4 bytes into int32
bytes_view = packed_bytes.view(e, rows, cols // 8, 4)
packed_int32 = (
bytes_view[..., 0].to(torch.int32) |
(bytes_view[..., 1].to(torch.int32) << 8) |
(bytes_view[..., 2].to(torch.int32) << 16) |
(bytes_view[..., 3].to(torch.int32) << 24)
)
packed_int32 = packed_int32.view(e, rows, cols // 8).contiguous()
scales = scales.to(torch.bfloat16).contiguous().view(e, rows, cols // group_size).contiguous()
return packed_int32, scales, dequant
def build_structured_tensor(shape: torch.Size, pattern: str) -> torch.Tensor:
if pattern == "random":
torch.manual_seed(42)
return (torch.randn(shape, dtype=torch.bfloat16, device="cpu") / 100.0).contiguous()
e, rows, cols = shape
groups = cols // k_group_size
group_builder = WEIGHT_PATTERNS[pattern][1]
group_vals = group_builder(groups).to(torch.float32)
block = group_vals.view(1, 1, groups, 1).expand(e, rows, groups, k_group_size).clone()
row_signs = torch.where(
(torch.arange(rows) % 2 == 0),
torch.ones(rows, dtype=torch.float32),
-torch.ones(rows, dtype=torch.float32),
).view(1, rows, 1, 1)
col_offsets = torch.linspace(-0.0005, 0.0005, steps=k_group_size, dtype=torch.float32).view(1, 1, 1, k_group_size)
block = block * row_signs + col_offsets
return block.reshape(shape).to(torch.bfloat16).contiguous()
def prepare_mxfp4_quantized_weights(pattern: str) -> Dict[str, torch.Tensor]:
if pattern not in WEIGHT_PATTERNS:
raise ValueError(f"Unknown weight pattern: {pattern}")
gate_proj = build_structured_tensor((expert_num, intermediate_size, hidden_size), pattern)
up_proj = build_structured_tensor((expert_num, intermediate_size, hidden_size), pattern)
down_proj = build_structured_tensor((expert_num, hidden_size, intermediate_size), pattern)
gate_q, gate_scales, gate_dequant = quantize_mxfp4_tensor(gate_proj, k_group_size)
up_q, up_scales, up_dequant = quantize_mxfp4_tensor(up_proj, k_group_size)
down_q, down_scales, down_dequant = quantize_mxfp4_tensor(down_proj, k_group_size)
return {
"gate_qweight": gate_q.contiguous(),
"up_qweight": up_q.contiguous(),
"down_qweight": down_q.contiguous(),
"gate_scales": gate_scales.contiguous(),
"up_scales": up_scales.contiguous(),
"down_scales": down_scales.contiguous(),
"dequantized": {
"gate_proj": gate_dequant.to(torch.bfloat16).contiguous(),
"up_proj": up_dequant.to(torch.bfloat16).contiguous(),
"down_proj": down_dequant.to(torch.bfloat16).contiguous(),
},
}
def build_moes_from_quantized_data(quant_data: Dict[str, torch.Tensor]):
moes = []
with torch.inference_mode(mode=True):
for _ in range(layer_num):
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.quant_config.bits = 4
config.quant_config.group_size = k_group_size
config.quant_config.zero_point = False
config.gate_proj = quant_data["gate_qweight"].data_ptr()
config.up_proj = quant_data["up_qweight"].data_ptr()
config.down_proj = quant_data["down_qweight"].data_ptr()
config.gate_scale = quant_data["gate_scales"].data_ptr()
config.up_scale = quant_data["up_scales"].data_ptr()
config.down_scale = quant_data["down_scales"].data_ptr()
config.pool = CPUInfer.backend_
moe = kt_kernel_ext.moe.AMXFP4_KGroup_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
moes.append(moe)
return moes
def run_case(pattern: str, qlen: int) -> Dict[str, float]:
print("\n" + "=" * 70)
desc = WEIGHT_PATTERNS[pattern][0]
path = "mat-vec" if qlen <= DISPATCH_THRESHOLD else "mat-mat"
print(f"Running case: {pattern} -> {desc} (qlen={qlen}, path={path})")
print("=" * 70)
quant_data = prepare_mxfp4_quantized_weights(pattern)
moes = build_moes_from_quantized_data(quant_data)
dequant_weights = quant_data["dequantized"]
gate_bf16 = dequant_weights["gate_proj"]
up_bf16 = dequant_weights["up_proj"]
down_bf16 = dequant_weights["down_proj"]
diffs = []
with torch.inference_mode(mode=True):
for i in range(validation_iter):
torch.manual_seed(100 + i)
bsz_tensor = torch.tensor([qlen], device="cpu")
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.randn((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_tensor = torch.randn((qlen, hidden_size), dtype=torch.bfloat16).contiguous() / 100
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
moe = moes[i % layer_num]
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_tensor.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Torch reference: use dequantized weights
input_tensor_bf16 = input_tensor.to(torch.bfloat16)
t_output = moe_torch(input_tensor_bf16, expert_ids, weights, gate_bf16, up_bf16, down_bf16).to(
torch.bfloat16
)
t_output = t_output.flatten()
output = output.flatten()
diff = torch.mean(torch.abs(output.float() - t_output.float())) / (
torch.mean(torch.abs(t_output.float())) + 1e-12
)
diffs.append(diff.item())
print(f"[{pattern}] Iteration {i}: relative L1 diff = {diff:.4f}")
print(f" output {output[:debug_print_count]}")
print(f" t_output {t_output[:debug_print_count]}")
mean_diff = float(sum(diffs) / len(diffs))
max_diff = float(max(diffs))
min_diff = float(min(diffs))
return {"case": pattern, "description": desc, "mean": mean_diff, "max": max_diff, "min": min_diff}
def run_fp4_moe_test():
summary_rows = []
for qlen in QLEN_LIST:
path = "mat-vec" if qlen <= DISPATCH_THRESHOLD else "mat-mat"
print(f"\n##### qlen={qlen} path={path} #####")
for case_name in WEIGHT_PATTERNS.keys():
results = run_case(case_name, qlen)
results["qlen"] = qlen
results["path"] = path
summary_rows.append(results)
print("\n=== Case vs. Relative Error Summary ===")
print(f"{'Case':<20} {'qlen':>5} {'path':<8} {'Mean':>10} {'Max':>10} {'Min':>10}")
for row in summary_rows:
print(f"{row['case']:<20} {row['qlen']:>5} {row['path']:<8} "
f"{row['mean']*100:9.2f}% {row['max']*100:9.2f}% {row['min']*100:9.2f}%")
if __name__ == "__main__":
run_fp4_moe_test()