Skip to content

Commit 2b00980

Browse files
quic-mamtamamtsing
andauthored
Add Support for Kimi K2.5 Vision (quic#1108)
Summary This PR adds QEff support for Kimi K2.5 Vision using a `ConditionalGeneration` flow designed for Dual-QPC execution, and updates our Transformers dependency to align with the required upstream APIs. What’s included • Added QEff Kimi K2.5 multimodal model support: • New Kimi K2.5 config classes (text + vision). • New Kimi K2.5 model implementation with: • vision tower + projector integration, • language decoder integration via DeepSeek-V3, • multimodal input merging and generation path, • export/runtime helpers (dummy inputs, dynamic axes, specializations, output naming). • Added a Kimi K2.5 vision export example for Dual-QPC • Added parity checks for vision and language models. Validation • Verified model wiring and transform registration are in place for Kimi K2.5 conditional generation flow. • Verified export example script supports reduced-layer / expert-subset setup for iterative validation. Example Script - examples/kimi_k2/export_kimi_k25_vision.py --------- Signed-off-by: Mamta Singh <mamtsing@qti.qualcomm.com> Signed-off-by: Mamta Singh <168400541+quic-mamta@users.noreply.github.com> Co-authored-by: Mamta Singh <mamtsing@qti.qualcomm.com>
1 parent 5b1b484 commit 2b00980

32 files changed

Lines changed: 4175 additions & 433 deletions

QEfficient/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import warnings # noqa: I001
2020
import transformers
2121
import transformers.utils as transformers_utils
22+
from transformers.utils import import_utils as hf_import_utils
2223

2324
try:
2425
from transformers import HybridCache as _TransformersHybridCache # noqa: F401
@@ -117,3 +118,23 @@ def check_qaic_sdk():
117118

118119
if not check_qaic_sdk():
119120
logger.warning("QAIC SDK is not installed, eager mode features won't be available!")
121+
122+
123+
def ensure_torch_fx_import_compatibility():
124+
if hasattr(hf_import_utils, "is_torch_fx_available"):
125+
return
126+
127+
def _is_torch_fx_available() -> bool:
128+
if not hf_import_utils.is_torch_available():
129+
return False
130+
try:
131+
import torch.fx # noqa: F401
132+
133+
return True
134+
except Exception:
135+
return False
136+
137+
hf_import_utils.is_torch_fx_available = _is_torch_fx_available
138+
139+
140+
ensure_torch_fx_import_compatibility()

QEfficient/base/onnx_transforms.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@
4444
CtxScatterFuncCB,
4545
CtxScatterFuncCB3D,
4646
)
47-
48-
# from QEfficient.customop.quantization_ops import CastToUInt4, CastToUInt4Func
4947
from QEfficient.customop.onnxscript_utils import get_onnxscript_func
48+
from QEfficient.customop.quantization_ops import CastToUInt4, CastToUInt4Func
5049
from QEfficient.customop.rms_norm import CustomRMSNorm, CustomRMSNormFunc
5150
from QEfficient.utils import constants
5251
from QEfficient.utils.constants import FILE_CHUNK_SIZE_DEFAULT, SIZE_THRESHOLD_DEFAULT
@@ -112,7 +111,7 @@ class CustomOpTransform(BaseOnnxTransform):
112111
"CtxGatherFuncBlockedKVCB": (CtxGatherFuncBlockedKVCB, CtxGatherBlockedKVCB),
113112
"CtxScatterFuncCB": (CtxScatterFuncCB, CtxScatterCB),
114113
"CtxGatherFuncCB": (CtxGatherFuncCB, CtxGatherCB),
115-
# "CastToUInt4": (CastToUInt4Func, CastToUInt4),
114+
"CastToUInt4": (CastToUInt4Func, CastToUInt4),
116115
}
117116

118117
@classmethod

QEfficient/base/pytorch_transforms.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
# ----------------------------------------------------------------------------
77
from types import MethodType
8-
from typing import Callable, Dict, Tuple, Type
8+
from typing import Callable, Dict, Optional, Tuple, Type
99

1010
from torch import nn
1111

@@ -97,6 +97,7 @@ class ModuleMutatorTransform(PytorchTransform):
9797
"""
9898

9999
_match_class: nn.Module
100+
_match_string: Optional[str] = None
100101

101102
@classmethod
102103
def apply(cls, model: nn.Module) -> Tuple[nn.Module, bool]:
@@ -135,7 +136,18 @@ def apply(cls, model: nn.Module) -> Tuple[nn.Module, bool]:
135136
repl_method_map := cls._match_string_replace_method.get(module.__class__.__name__)
136137
):
137138
for orig_method_name, mapped_method in repl_method_map.items():
138-
setattr(module, orig_method_name, MethodType(mapped_method, module))
139+
parts = orig_method_name.split(".")
140+
if len(parts) > 1:
141+
target = module
142+
for part in parts[:-1]:
143+
target = getattr(target, part)
144+
if callable(mapped_method):
145+
mapped_method = MethodType(mapped_method, target)
146+
setattr(target, parts[-1], mapped_method)
147+
else:
148+
if callable(mapped_method):
149+
mapped_method = MethodType(mapped_method, module)
150+
setattr(module, orig_method_name, mapped_method)
139151

140152
if hasattr(module, "__qeff_init__"):
141153
module.__qeff_init__()

QEfficient/customop/matmulnbits.py

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
class QuantLinearTorchFunction(torch.autograd.Function):
1515
@staticmethod
1616
def symbolic(g, x, qself_qweight, qself_scales, qself_qzeros, g_idx, bits, group_size, in_features, out_features):
17-
input_tuple = (x, qself_qweight, qself_scales, qself_qzeros)
17+
if qself_qzeros is None:
18+
input_tuple = (x, qself_qweight, qself_scales)
19+
else:
20+
input_tuple = (x, qself_qweight, qself_scales, qself_qzeros)
1821
input_tuple += (g_idx,) if g_idx is not None else ()
1922
return g.op(
2023
"com.microsoft::MatMulNBits",
@@ -28,7 +31,10 @@ def symbolic(g, x, qself_qweight, qself_scales, qself_qzeros, g_idx, bits, group
2831

2932
@staticmethod
3033
def forward(ctx, x, qself_qweight, qself_scales, qself_qzeros, g_idx, bits, group_size, in_features, out_features):
34+
if qself_qzeros is None:
35+
qself_qzeros = 2 ^ (bits - 1)
3136
if torch.onnx.is_in_onnx_export():
37+
# For faster export
3238
return torch.zeros(x.shape[:-1] + (out_features,), dtype=x.dtype).float()
3339
fp_weight = dequantize_blockwise_bits(
3440
qself_qweight, qself_scales, qself_qzeros, bits, group_size, g_idx, in_features, out_features
@@ -40,8 +46,7 @@ def forward(ctx, x, qself_qweight, qself_scales, qself_qzeros, g_idx, bits, grou
4046
def dequantize_blockwise_bits(quant_values, scale, zero_point, bits, group_size, g_idx, rows, cols):
4147
if bits != 4:
4248
raise ValueError("Only bits=4 is supported for executing quantized model")
43-
if group_size != 128:
44-
raise ValueError("Only group_size=128 is supported for executing quantized model")
49+
4550
expand_quant_value = (quant_values.unsqueeze(-1) >> torch.tensor([[[[0, 4]]]], dtype=torch.int32)) & 0x0F
4651
expand_quant_value = expand_quant_value.reshape(*quant_values.shape[:-1], -1)
4752
aligned_scale = scale.reshape(*quant_values.shape[:-1], 1)
@@ -88,20 +93,20 @@ def __init__(self, bits, group_size, in_features, out_features, bias):
8893
q_rows = in_features // self.group_size
8994
self.register_buffer(
9095
"qweight",
91-
torch.zeros((out_features, q_rows, self.group_size // (8 // bits)), dtype=torch.uint8),
96+
torch.empty((out_features, q_rows, self.group_size // (8 // bits)), dtype=torch.uint8),
9297
)
9398
self.register_buffer(
9499
"qzeros",
95-
torch.zeros((q_rows + (q_rows & 1)) * (out_features // 8 * self.bits), dtype=torch.uint8),
100+
torch.empty((q_rows + (q_rows & 1)) * (out_features // 8 * self.bits), dtype=torch.uint8),
96101
)
97102
self.register_buffer(
98-
"scales", torch.zeros((math.ceil(in_features / self.group_size) * out_features), dtype=torch.float16)
103+
"scales", torch.empty((math.ceil(in_features / self.group_size) * out_features), dtype=torch.float16)
99104
)
100105
self.register_buffer(
101106
"g_idx", torch.tensor([i // self.group_size for i in range(in_features)], dtype=torch.int32)
102107
)
103108
if bias:
104-
self.register_buffer("bias", torch.zeros((out_features), dtype=torch.float16))
109+
self.register_buffer("bias", torch.empty((out_features), dtype=torch.float16))
105110
else:
106111
self.bias = None
107112

@@ -183,3 +188,72 @@ def forward(self, inputs):
183188
)
184189
out = out + self.bias if self.bias is not None else out
185190
return out
191+
192+
193+
class QMOE(torch.autograd.Function):
194+
@staticmethod
195+
def symbolic(
196+
g,
197+
x,
198+
router_weights,
199+
fc1_experts_weights,
200+
fc1_scales,
201+
fc2_experts_weights,
202+
fc2_scales,
203+
fc3_experts_weights,
204+
fc3_scales,
205+
router_probs,
206+
activation_type,
207+
block_size,
208+
expert_weight_bits,
209+
k,
210+
):
211+
qmoe_out = g.op(
212+
"com.microsoft::QMoE",
213+
x,
214+
router_weights,
215+
router_probs,
216+
fc1_experts_weights,
217+
fc1_scales,
218+
fc2_experts_weights,
219+
fc2_scales,
220+
fc3_experts_weights,
221+
fc3_scales,
222+
outputs=1,
223+
activation_type_s=activation_type, # <-- _s suffix for string
224+
block_size_i=block_size,
225+
expert_weight_bits_i=expert_weight_bits,
226+
k_i=k,
227+
)
228+
229+
# # Create axes=-1 as an explicit int64 constant tensor
230+
# axes = g.op("Constant", value_t=torch.tensor([-1], dtype=torch.int64))
231+
232+
# # Compute mean of router_probs along the last axis, keepdims for broadcasting
233+
# router_probs_mean = g.op("ReduceMean", router_probs, axes, keepdims_i=1)
234+
235+
# Multiply qmoe_out with the averaged router_probs
236+
return qmoe_out
237+
# return g.op("Mul", qmoe_out, router_probs_mean))
238+
239+
@staticmethod
240+
def forward(
241+
ctx,
242+
x,
243+
router_weights,
244+
fc1_experts_weights,
245+
fc1_scales,
246+
fc2_experts_weights,
247+
fc2_scales,
248+
fc3_experts_weights,
249+
fc3_scales,
250+
router_probs,
251+
activation_type,
252+
block_size,
253+
expert_weight_bits,
254+
k,
255+
):
256+
# Dummy forward: simulate qmoe_out as zeros_like(x), then apply ReduceMean * Mul
257+
qmoe_out = torch.zeros_like(x)
258+
router_probs_mean = router_probs.mean(dim=-1, keepdim=True)
259+
return qmoe_out * router_probs_mean
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# -----------------------------------------------------------------------------
2+
#
3+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
#
6+
# -----------------------------------------------------------------------------
7+
8+
import onnxscript
9+
import torch
10+
from onnx import TensorProto
11+
12+
from QEfficient.utils import constants
13+
14+
ops = getattr(onnxscript, "opset" + str(constants.ONNX_EXPORT_OPSET))
15+
16+
17+
@onnxscript.script(onnxscript.values.Opset("com.qti.aisw.onnx", 1))
18+
def CastToUInt4(weight_packed: onnxscript.UINT8) -> onnxscript.UINT8:
19+
"""
20+
Unpack packed uint8 weights into uint4 values and cast output to UINT4.
21+
Supports N-D input: all leading dimensions are preserved; only the last
22+
dimension (in_features // 2) is doubled to (in_features).
23+
24+
Input: (..., in_features // 2) UINT8
25+
Each byte holds two nibbles: byte = (w_y << 4) | (w_x & 0x0F)
26+
Output: (..., in_features) UINT4, values in [0, 15]
27+
28+
Operations:
29+
w_x = weight_packed % 16 (lower nibble)
30+
w_y = (weight_packed >> 4) % 16 (upper nibble)
31+
stacked = concat([w_x, w_y], axis=-1) after unsqueeze
32+
→ (..., in//2, 2)
33+
leading_dims = shape[:-1]
34+
new_shape = [...leading_dims, last_dim * 2]
35+
reshaped = reshape(stacked, new_shape)
36+
output = Cast(reshaped, to=UINT4)
37+
"""
38+
sixteen = ops.CastLike(ops.Constant(value_ints=[16]), weight_packed)
39+
40+
# Lower nibble: weight_packed & 0x0F = weight_packed % 16
41+
w_x = ops.Mod(weight_packed, sixteen)
42+
43+
# Upper nibble: (weight_packed >> 4) & 0x0F
44+
shift = ops.CastLike(ops.Constant(value_ints=[4]), weight_packed)
45+
w_shifted = ops.BitShift(weight_packed, shift, direction="RIGHT")
46+
w_y = ops.Mod(w_shifted, sixteen)
47+
48+
# Stack along a new last dim → (..., in_features//2, 2)
49+
w_x_unsq = ops.Unsqueeze(w_x, [-1])
50+
w_y_unsq = ops.Unsqueeze(w_y, [-1])
51+
stacked = ops.Concat(w_x_unsq, w_y_unsq, axis=-1)
52+
53+
# N-D aware reshape: preserve all leading dims, double the last dim.
54+
# packed_shape = [d0, d1, ..., last_dim]
55+
packed_shape = ops.Shape(weight_packed)
56+
# All dims except the last: [d0, d1, ...]
57+
leading_dims = ops.Slice(packed_shape, starts=[0], ends=[-1], axes=[0])
58+
# Last dim only: [last_dim]
59+
last_dim = ops.Slice(packed_shape, starts=[-1], ends=[2147483647], axes=[0])
60+
# Double the last dim: [last_dim * 2]
61+
last_dim_doubled = ops.Mul(last_dim, ops.Constant(value_ints=[2]))
62+
# New shape: [d0, d1, ..., last_dim * 2]
63+
new_shape = ops.Concat(leading_dims, last_dim_doubled, axis=0)
64+
reshaped = ops.Reshape(stacked, new_shape)
65+
66+
# Cast to UINT4 — data_type value is version-dependent (21 in ONNX 1.18, 23 in newer)
67+
return ops.Cast(reshaped, to=int(TensorProto.UINT4))
68+
69+
70+
class CastToUInt4Func(torch.autograd.Function):
71+
"""
72+
Custom op: unpacks packed uint8 → uint8 (values 0-15) in PyTorch.
73+
In ONNX the custom op subgraph includes a Cast → UINT4 as its last step.
74+
Supports N-D input: all leading dimensions are preserved.
75+
76+
PyTorch forward : packed uint8 (..., in//2) → uint8 (..., in), values [0, 15]
77+
ONNX symbolic : emits CastToUInt4 node (com.qti.aisw.onnx)
78+
The subgraph ends with Cast → UINT4.
79+
"""
80+
81+
@staticmethod
82+
def forward(weight_packed: torch.Tensor) -> torch.Tensor:
83+
w_x = weight_packed & 0x0F # lower nibble, (..., in//2), range [0, 15]
84+
w_y = (weight_packed >> 4) & 0x0F # upper nibble, (..., in//2), range [0, 15]
85+
# New shape: all leading dims unchanged, last dim doubled
86+
new_shape = list(weight_packed.shape[:-1]) + [weight_packed.shape[-1] * 2]
87+
return torch.stack(
88+
[w_x, w_y], dim=-1
89+
).reshape(
90+
new_shape
91+
) # Can't add a cast operation to uint4 here, as its not supported in pytorch; The ONNX export will handle the cast to IINT4 in the symbolic method.
92+
93+
@staticmethod
94+
def setup_context(ctx, inputs, outputs):
95+
pass
96+
97+
@staticmethod
98+
def symbolic(g: torch.Graph, weight_packed: torch.Value) -> torch.Value:
99+
output = g.onnxscript_op(CastToUInt4, weight_packed)
100+
return output
101+
102+
103+
class DequantizeLinearFunc(torch.autograd.Function):
104+
"""
105+
Emits a standard ONNX DequantizeLinear node (ai.onnx domain, not custom).
106+
107+
Symmetric blockwise quantization — no zero_point:
108+
output = x * scale (per block along the last axis)
109+
110+
Supports N-D input:
111+
weight_unpacked : (..., in_features) — quantized values
112+
scale : (..., num_blocks) — per-block scales
113+
block_size : int — elements per block
114+
115+
PyTorch forward : expand blockwise scale along last dim, multiply
116+
ONNX symbolic : DequantizeLinear(weight_unpacked, scale,
117+
axis=2, block_size=block_size)
118+
axis=2 for 3D input (2, out_features, in_features).
119+
No zero_point input (symmetric).
120+
"""
121+
122+
@staticmethod
123+
def forward(
124+
weight_unpacked: torch.Tensor, scale: torch.Tensor, zeros: torch.Tensor, block_size: int
125+
) -> torch.Tensor:
126+
# Expand per-block scale → per-element scale along last dim
127+
scale_expanded = scale.repeat_interleave(block_size, dim=-1)
128+
zeros_expanded = zeros.repeat_interleave(block_size, dim=-1)
129+
return (weight_unpacked.to(torch.int8) - zeros_expanded.to(torch.int8)) * scale_expanded
130+
131+
@staticmethod
132+
def setup_context(ctx, inputs, outputs):
133+
pass
134+
135+
@staticmethod
136+
def symbolic(
137+
g: torch.Graph, weight_unpacked: torch.Value, scale: torch.Value, zeros: torch.Value, block_size: int
138+
) -> torch.Value:
139+
# Standard DequantizeLinear: symmetric (no zero_point), blockwise.
140+
# Input is 3D: (2, out_features, in_features) → axis=2 (last dim).
141+
# DequantizeLinear natively supports batch dimensions.
142+
return g.op(
143+
"DequantizeLinear",
144+
weight_unpacked,
145+
scale,
146+
zeros,
147+
axis_i=2,
148+
block_size_i=block_size,
149+
)

0 commit comments

Comments
 (0)