jkim96 commited on
Commit
00f5c1a
·
verified ·
1 Parent(s): 1a53df5

Add DASH-Q remote-code inference (Triton decode kernel)

Browse files
Files changed (4) hide show
  1. README.md +29 -10
  2. config.json +25 -2
  3. dashq_kernel.py +236 -0
  4. modeling_dashq.py +229 -0
README.md CHANGED
@@ -9,7 +9,6 @@ tags:
9
  - post-training-quantization
10
  - int3
11
  ---
12
-
13
  ![DASH-Q](https://raw.githubusercontent.com/JaeminK/dashq/main/assets/dashq_banner.png)
14
 
15
  # Qwen3.5-35B-A3B-DASHQ-INT3-g128
@@ -17,21 +16,41 @@ tags:
17
  > **DASH-Q** — Diagonal-Aware Shrinkage for Robust PTQ.
18
  > `INT3` · group size 128 · **17.4800 GB** (from 71.9039 GB — **4.1x smaller**)
19
 
20
- DASH-Q checkpoints load with the lightweight DASH-Q runtime linear layers are packed `PackedQuantizedLinear` modules, not plain Transformers weights.
 
 
21
 
22
- ## Install
 
23
 
24
- ```bash
25
- pip install git+https://github.com/JaeminK/dashq.git
 
 
 
 
 
 
 
26
  ```
27
 
28
- ## Load
 
 
 
 
29
 
30
- ```python
31
- from dashq import load_quantized
 
32
 
33
- model, tokenizer = load_quantized("jkim96/Qwen3.5-35B-A3B-DASHQ-INT3-g128", device_map="auto")
34
- ```
 
 
 
 
 
35
 
36
  ## Quantization
37
 
 
9
  - post-training-quantization
10
  - int3
11
  ---
 
12
  ![DASH-Q](https://raw.githubusercontent.com/JaeminK/dashq/main/assets/dashq_banner.png)
13
 
14
  # Qwen3.5-35B-A3B-DASHQ-INT3-g128
 
16
  > **DASH-Q** — Diagonal-Aware Shrinkage for Robust PTQ.
17
  > `INT3` · group size 128 · **17.4800 GB** (from 71.9039 GB — **4.1x smaller**)
18
 
19
+ This checkpoint runs directly with Transformers: the packed quantized layers and a Triton decode kernel are bundled in the repository, so no additional package is needed.
20
+
21
+ ## Usage
22
 
23
+ ```python
24
+ from transformers import AutoModelForImageTextToText, AutoTokenizer
25
 
26
+ model = AutoModelForImageTextToText.from_pretrained(
27
+ "jkim96/Qwen3.5-35B-A3B-DASHQ-INT3-g128", trust_remote_code=True, device_map="cuda", dtype="auto"
28
+ )
29
+ tokenizer = AutoTokenizer.from_pretrained("jkim96/Qwen3.5-35B-A3B-DASHQ-INT3-g128")
30
+
31
+ messages = [{"role": "user", "content": "Explain 2-bit quantization in one sentence."}]
32
+ text = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
33
+ inputs = tokenizer(text, return_tensors="pt").to(model.device)
34
+ print(tokenizer.decode(model.generate(**inputs, max_new_tokens=256)[0]))
35
  ```
36
 
37
+ No extra package is required: this repository carries its own inference code
38
+ (`modeling_dashq.py`) and a Triton weight-only GEMV decode kernel
39
+ (`dashq_kernel.py`). Quantized layers are rebuilt from `dashq_config.json` at load
40
+ time and converted to the kernel automatically; on CPU or without Triton the model
41
+ falls back to a PyTorch dequantize-and-matmul path.
42
 
43
+ Requirements: `transformers`, `torch`, and `triton` (bundled with CUDA builds of
44
+ PyTorch). The [DASH-Q repository](https://github.com/JaeminK/dashq) is only needed
45
+ to quantize your own models.
46
 
47
+ ### Runtime format
48
+
49
+ | Field | Value |
50
+ | --- | --- |
51
+ | Weights | group-wise asymmetric integers (3-bit, group size 128) packed into int32 words |
52
+ | Decode kernel | Triton K-major GEMV (supports 2/3/4/8-bit at any group size) |
53
+ | Prefill | unpack-and-matmul on the same buffers |
54
 
55
  ## Quantization
56
 
config.json CHANGED
@@ -1,7 +1,30 @@
1
  {
2
  "architectures": [
3
- "Qwen3_5MoeForConditionalGeneration"
4
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  "dtype": "bfloat16",
6
  "image_token_id": 248056,
7
  "model_type": "qwen3_5_moe",
@@ -120,4 +143,4 @@
120
  },
121
  "vision_end_token_id": 248054,
122
  "vision_start_token_id": 248053
123
- }
 
1
  {
2
  "architectures": [
3
+ "DashQQwen3_5MoeForConditionalGeneration"
4
  ],
5
+ "auto_map": {
6
+ "AutoModelForCausalLM": "modeling_dashq.DashQQwen3_5MoeForConditionalGeneration",
7
+ "AutoModelForImageTextToText": "modeling_dashq.DashQQwen3_5MoeForConditionalGeneration"
8
+ },
9
+ "dashq": {
10
+ "format": "dashq-packed-linear",
11
+ "format_version": 1,
12
+ "layer_metadata": "dashq_config.json",
13
+ "method": "dashq",
14
+ "n_quantized_modules": 20830,
15
+ "params": {
16
+ "bits": 3,
17
+ "group_size": 128,
18
+ "low_memory_optimization": false,
19
+ "moe_hessian_scope": "shared",
20
+ "n_samples": 128,
21
+ "scale_zero_dtype": "float16",
22
+ "symmetric": false,
23
+ "use_error_compensation": true,
24
+ "use_optimal_shrinkage": true,
25
+ "use_weighted_quantization": true
26
+ }
27
+ },
28
  "dtype": "bfloat16",
29
  "image_token_id": 248056,
30
  "model_type": "qwen3_5_moe",
 
143
  },
144
  "vision_end_token_id": 248054,
145
  "vision_start_token_id": 248053
146
+ }
dashq_kernel.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Triton weight-only GEMV backend for DASH-Q packed checkpoints.
2
+
3
+ Group-wise asymmetric integer weights (the format DASH-Q emits) are stored
4
+ K-major so that a decode-time GEMV reads each packed word exactly once with
5
+ fully coalesced loads:
6
+
7
+ W_q : (K // elements_per_word, N) int32 (packed along K, N contiguous)
8
+ s,z : (K // group_size, N) (one group per program)
9
+
10
+ Supported bit widths: 2, 3, 4, 8 (and 1). 3-bit uses two bit-planes -- a
11
+ 2-bit plane plus a 1-bit plane -- which is exactly 3 bits per weight and is
12
+ not covered by existing kernel libraries.
13
+
14
+ Batched inputs (prefill) fall back to an unpack-and-matmul path that uses the
15
+ same K-major buffers, so the original torch buffers can be released.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+
24
+ try:
25
+ import triton
26
+ import triton.language as tl
27
+
28
+ TRITON_AVAILABLE = True
29
+ except Exception: # pragma: no cover - triton is an optional dependency
30
+ TRITON_AVAILABLE = False
31
+
32
+ SUPPORTED_NBITS = (1, 2, 3, 4, 8)
33
+
34
+
35
+ if TRITON_AVAILABLE:
36
+
37
+ @triton.jit
38
+ def _dashq_gemv_kernel(
39
+ x_ptr, w_ptr, lo_ptr, s_ptr, z_ptr, y_ptr,
40
+ N, K,
41
+ NBITS: tl.constexpr, EPS: tl.constexpr, GS: tl.constexpr,
42
+ BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
43
+ ):
44
+ pid_n = tl.program_id(0)
45
+ pid_k = tl.program_id(1) * 2
46
+ offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
47
+ offs_n = tl.max_contiguous(tl.multiple_of(offs_n, BLOCK_N), BLOCK_N)
48
+
49
+ # one scale/zero group per program (2 * BLOCK_K == GS)
50
+ k_m = (pid_k * BLOCK_K) // GS
51
+ scales = tl.load(s_ptr + k_m * N + offs_n).to(tl.float32)
52
+ zeros = tl.load(z_ptr + k_m * N + offs_n).to(tl.float32)
53
+
54
+ acc = tl.zeros((BLOCK_N,), dtype=tl.float32)
55
+ offs_k = pid_k * BLOCK_K + tl.arange(0, BLOCK_K)
56
+ for _ in tl.static_range(2):
57
+ a = tl.load(x_ptr + offs_k, eviction_policy="evict_last").to(tl.float32)
58
+ if NBITS == 3:
59
+ hw = tl.load(
60
+ w_ptr + (offs_k // 16)[:, None] * N + offs_n[None, :],
61
+ eviction_policy="evict_first",
62
+ )
63
+ lw = tl.load(
64
+ lo_ptr + (offs_k // 32)[:, None] * N + offs_n[None, :],
65
+ eviction_policy="evict_first",
66
+ )
67
+ q = (((hw >> (((offs_k % 16) * 2)[:, None])) & 3) << 1) | (
68
+ (lw >> ((offs_k % 32)[:, None])) & 1
69
+ )
70
+ else:
71
+ wv = tl.load(
72
+ w_ptr + (offs_k // EPS)[:, None] * N + offs_n[None, :],
73
+ eviction_policy="evict_first",
74
+ )
75
+ q = (wv >> (((offs_k % EPS) * NBITS)[:, None])) & ((1 << NBITS) - 1)
76
+ b = (q.to(tl.float32) - zeros[None, :]) * scales[None, :]
77
+ acc += tl.sum(a[:, None] * b, axis=0)
78
+ offs_k += BLOCK_K
79
+
80
+ tl.atomic_add(y_ptr + offs_n, acc, sem="relaxed")
81
+
82
+
83
+ def _pack_kmajor(q_kn: torch.Tensor, bits: int) -> torch.Tensor:
84
+ """(K, N) uint8 codes -> (K // eps, N) int32, value k in word k // eps."""
85
+ K, N = q_kn.shape
86
+ eps = 32 // bits
87
+ v = q_kn.to(torch.int32).reshape(K // eps, eps, N)
88
+ words = torch.zeros(K // eps, N, dtype=torch.int32, device=q_kn.device)
89
+ for j in range(eps):
90
+ words |= v[:, j, :] << (bits * j)
91
+ return words
92
+
93
+
94
+ def _unpack_kmajor(words: torch.Tensor, bits: int, K: int) -> torch.Tensor:
95
+ eps = 32 // bits
96
+ WK, N = words.shape
97
+ shifts = (torch.arange(eps, device=words.device, dtype=torch.int32) * bits).view(1, eps, 1)
98
+ q = (words.view(WK, 1, N) >> shifts) & ((1 << bits) - 1)
99
+ return q.reshape(WK * eps, N)[:K]
100
+
101
+
102
+ class TritonQuantLinear(nn.Module):
103
+ """Decode-optimized replacement for a DASH-Q PackedQuantizedLinear."""
104
+
105
+ def __init__(
106
+ self,
107
+ W_int: torch.Tensor, # (out_features, in_features) integer codes
108
+ scale: torch.Tensor, # (out_features, num_groups)
109
+ zero: torch.Tensor, # (out_features, num_groups)
110
+ nbits: int,
111
+ group_size: int,
112
+ bias: Optional[torch.Tensor] = None,
113
+ out_dtype: torch.dtype = torch.float16,
114
+ block_n: int = 128,
115
+ num_warps: int = 1,
116
+ ) -> None:
117
+ super().__init__()
118
+ if not TRITON_AVAILABLE:
119
+ raise RuntimeError("Triton is not available.")
120
+ if nbits not in SUPPORTED_NBITS:
121
+ raise ValueError(f"Unsupported nbits for the Triton backend: {nbits}")
122
+
123
+ out_features, in_features = W_int.shape
124
+ if in_features % group_size != 0:
125
+ raise ValueError("in_features must be divisible by group_size.")
126
+ if group_size % 2 != 0:
127
+ raise ValueError("group_size must be even.")
128
+
129
+ self.out_features = out_features
130
+ self.in_features = in_features
131
+ self.nbits = int(nbits)
132
+ self.group_size = int(group_size)
133
+ self.out_dtype = out_dtype
134
+ self.block_n = int(block_n)
135
+ self.num_warps = int(num_warps)
136
+ self.block_k = self.group_size // 2
137
+
138
+ q_kn = W_int.t().contiguous().to(torch.uint8)
139
+ if nbits == 3:
140
+ self.register_buffer("W_q", _pack_kmajor(q_kn >> 1, 2))
141
+ self.register_buffer("W_lo", _pack_kmajor(q_kn & 1, 1))
142
+ self.eps = 16
143
+ else:
144
+ self.register_buffer("W_q", _pack_kmajor(q_kn, nbits))
145
+ self.register_buffer("W_lo", torch.zeros(1, dtype=torch.int32, device=q_kn.device))
146
+ self.eps = 32 // nbits
147
+ del q_kn
148
+
149
+ self.register_buffer("scale", scale.t().contiguous().to(out_dtype))
150
+ self.register_buffer("zero", zero.t().contiguous().to(out_dtype))
151
+ if bias is not None:
152
+ self.register_buffer("bias", bias.detach().clone().to(out_dtype))
153
+ else:
154
+ self.bias = None
155
+
156
+ # accumulator is seeded with the bias, so the kernel never adds it
157
+ # (each K-split program contributes once via atomic_add)
158
+ acc_init = torch.zeros(out_features, dtype=torch.float32, device=self.W_q.device)
159
+ if bias is not None:
160
+ acc_init.copy_(self.bias.float())
161
+ self.register_buffer("_acc_init", acc_init)
162
+ self.register_buffer("_acc", acc_init.clone())
163
+ self._grid = (
164
+ (out_features + self.block_n - 1) // self.block_n,
165
+ in_features // self.group_size,
166
+ )
167
+
168
+ @classmethod
169
+ def from_packed(cls, module: nn.Module, **kwargs) -> "TritonQuantLinear":
170
+ """Build from a dashq.quantization.PackedQuantizedLinear instance."""
171
+ from dashq.quantization import _unpack_int_values
172
+
173
+ K = int(getattr(module, "quant_in_features", module.in_features))
174
+ N = int(module.out_features)
175
+ W_int = _unpack_int_values(module.W_q_packed, module.nbits, module.numel).view(N, K)
176
+ num_groups = K // int(module.group_size)
177
+ scale = module.scale.view(N, num_groups)
178
+ zero = module.zero.view(N, num_groups)
179
+ bias = module.bias if getattr(module, "bias", None) is not None else None
180
+ return cls(
181
+ W_int,
182
+ scale,
183
+ zero,
184
+ int(module.nbits),
185
+ int(module.group_size),
186
+ bias=bias,
187
+ out_dtype=getattr(module, "linear_dtype", torch.float16),
188
+ **kwargs,
189
+ )
190
+
191
+ def dequantize_weight(self, dtype: torch.dtype) -> torch.Tensor:
192
+ """Returns W^T as (in_features, out_features), matching the K-major layout."""
193
+ if self.nbits == 3:
194
+ q = (_unpack_kmajor(self.W_q, 2, self.in_features).to(torch.int32) << 1) | (
195
+ _unpack_kmajor(self.W_lo, 1, self.in_features).to(torch.int32)
196
+ )
197
+ else:
198
+ q = _unpack_kmajor(self.W_q, self.nbits, self.in_features)
199
+ s = self.scale.repeat_interleave(self.group_size, dim=0).to(dtype)
200
+ z = self.zero.repeat_interleave(self.group_size, dim=0).to(dtype)
201
+ return (q.to(dtype) - z) * s
202
+
203
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
204
+ shape = x.shape
205
+ tokens = x.numel() // shape[-1]
206
+ if tokens == 1 and x.is_cuda:
207
+ self._acc.copy_(self._acc_init)
208
+ _dashq_gemv_kernel[self._grid](
209
+ x.reshape(-1),
210
+ self.W_q,
211
+ self.W_lo,
212
+ self.scale,
213
+ self.zero,
214
+ self._acc,
215
+ self.out_features,
216
+ self.in_features,
217
+ self.nbits,
218
+ self.eps,
219
+ self.group_size,
220
+ self.block_n,
221
+ self.block_k,
222
+ num_warps=self.num_warps,
223
+ )
224
+ return self._acc.to(x.dtype).reshape(*shape[:-1], self.out_features)
225
+
226
+ w_t = self.dequantize_weight(x.dtype)
227
+ out = x.reshape(tokens, -1) @ w_t
228
+ if self.bias is not None:
229
+ out = out + self.bias.to(x.dtype)
230
+ return out.reshape(*shape[:-1], self.out_features)
231
+
232
+ def extra_repr(self) -> str:
233
+ return (
234
+ f"in_features={self.in_features}, out_features={self.out_features}, "
235
+ f"nbits={self.nbits}, group_size={self.group_size}, backend=triton"
236
+ )
modeling_dashq.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained DASH-Q inference code for this checkpoint.
2
+
3
+ Generated by export_hf_repo.py -- do not edit by hand.
4
+
5
+ Weights are stored as group-wise asymmetric integers packed into int32 words.
6
+ The layout of every quantized layer is described by `dashq_config.json` in this
7
+ repository. At load time each quantized module is converted to the K-major
8
+ layout used by the DASH-Q Triton decode kernel (`dashq_kernel.py`); when Triton
9
+ is unavailable (e.g. CPU) the modules fall back to an unpack-and-matmul path in
10
+ pure PyTorch.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from typing import Any, Dict, Optional
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+
22
+ from transformers import AutoConfig, Qwen3_5MoeForConditionalGeneration
23
+
24
+ try:
25
+ from .dashq_kernel import TRITON_AVAILABLE, SUPPORTED_NBITS, TritonQuantLinear
26
+ except ImportError: # loaded as a flat module by trust_remote_code
27
+ from dashq_kernel import TRITON_AVAILABLE, SUPPORTED_NBITS, TritonQuantLinear
28
+
29
+ DASHQ_CONFIG_FILE = "dashq_config.json"
30
+
31
+
32
+ def _unpack_int_values(packed: torch.Tensor, nbits: int, numel: int) -> torch.Tensor:
33
+ values_per_word = max(1, 32 // nbits)
34
+ mask = (1 << nbits) - 1
35
+ shifts = torch.arange(values_per_word, device=packed.device, dtype=torch.int32) * nbits
36
+ out = (packed.view(-1, 1) >> shifts.view(1, -1)) & mask
37
+ return out.reshape(-1)[:numel]
38
+
39
+
40
+ class DashQPackedLinear(nn.Module):
41
+ """Holds the checkpoint buffers; converted to the Triton kernel after load."""
42
+
43
+ def __init__(self, in_features: int, out_features: int, nbits: int, group_size: int,
44
+ bias: bool, dtype: torch.dtype, quant_in_features: Optional[int] = None) -> None:
45
+ super().__init__()
46
+ self.in_features = int(in_features)
47
+ self.quant_in_features = int(quant_in_features or in_features)
48
+ self.out_features = int(out_features)
49
+ self.nbits = int(nbits)
50
+ self.group_size = int(group_size)
51
+ self.linear_dtype = dtype
52
+ self.numel = self.out_features * self.quant_in_features
53
+ self.num_groups = self.numel // self.group_size
54
+ values_per_word = max(1, 32 // self.nbits)
55
+ n_words = (self.numel + values_per_word - 1) // values_per_word
56
+
57
+ self.register_buffer("W_q_packed", torch.zeros(n_words, dtype=torch.int32))
58
+ self.register_buffer("scale", torch.zeros(self.num_groups, 1, dtype=torch.float16))
59
+ self.register_buffer("zero", torch.zeros(self.num_groups, 1, dtype=torch.float16))
60
+ if bias:
61
+ self.bias = nn.Parameter(torch.zeros(self.out_features, dtype=dtype), requires_grad=False)
62
+ else:
63
+ self.register_parameter("bias", None)
64
+ self.kernel: Optional[nn.Module] = None
65
+
66
+ def dequantize_weight(self, dtype: torch.dtype) -> torch.Tensor:
67
+ W_int = _unpack_int_values(self.W_q_packed, self.nbits, self.numel)
68
+ W_int = W_int.view(self.num_groups, self.group_size).to(dtype)
69
+ W = (W_int - self.zero.to(dtype)) * self.scale.to(dtype)
70
+ return W.view(self.out_features, self.quant_in_features)
71
+
72
+ @torch.no_grad()
73
+ def build_kernel(self) -> bool:
74
+ if self.kernel is not None:
75
+ return True
76
+ if not TRITON_AVAILABLE or self.nbits not in SUPPORTED_NBITS:
77
+ return False
78
+ if self.quant_in_features % self.group_size or self.group_size % 2:
79
+ return False
80
+ if self.W_q_packed is None or not self.W_q_packed.is_cuda:
81
+ return False
82
+ W_int = _unpack_int_values(self.W_q_packed, self.nbits, self.numel)
83
+ W_int = W_int.view(self.out_features, self.quant_in_features)
84
+ ng = self.quant_in_features // self.group_size
85
+ self.kernel = TritonQuantLinear(
86
+ W_int,
87
+ self.scale.view(self.out_features, ng),
88
+ self.zero.view(self.out_features, ng),
89
+ self.nbits,
90
+ self.group_size,
91
+ bias=self.bias.data if self.bias is not None else None,
92
+ out_dtype=self.linear_dtype,
93
+ )
94
+ del W_int
95
+ self._buffers["W_q_packed"] = None
96
+ self._buffers["scale"] = None
97
+ self._buffers["zero"] = None
98
+ return True
99
+
100
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
101
+ if self.kernel is not None:
102
+ return self.kernel(x)
103
+ weight = self.dequantize_weight(x.dtype)
104
+ bias = self.bias.to(x.dtype) if self.bias is not None else None
105
+ return F.linear(x, weight, bias)
106
+
107
+ def extra_repr(self) -> str:
108
+ return (f"in_features={self.in_features}, out_features={self.out_features}, "
109
+ f"nbits={self.nbits}, group_size={self.group_size}")
110
+
111
+
112
+ def _set_module(root: nn.Module, name: str, new_module: nn.Module) -> None:
113
+ parts = name.split(".")
114
+ parent = root
115
+ for part in parts[:-1]:
116
+ parent = getattr(parent, part)
117
+ setattr(parent, parts[-1], new_module)
118
+
119
+
120
+ def _get_module(root: nn.Module, name: str) -> Optional[nn.Module]:
121
+ obj = root
122
+ for part in name.split("."):
123
+ if not hasattr(obj, part):
124
+ return None
125
+ obj = getattr(obj, part)
126
+ return obj
127
+
128
+
129
+ _DTYPES = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32}
130
+
131
+
132
+ def _load_dashq_spec(model_id_or_path, **kwargs) -> Dict[str, Any]:
133
+ """Read dashq_config.json from a local dir or the Hub."""
134
+ path = None
135
+ if model_id_or_path is not None:
136
+ local = os.path.join(str(model_id_or_path), DASHQ_CONFIG_FILE)
137
+ if os.path.isfile(local):
138
+ path = local
139
+ if path is None and model_id_or_path is not None:
140
+ try:
141
+ from huggingface_hub import hf_hub_download
142
+
143
+ path = hf_hub_download(
144
+ repo_id=str(model_id_or_path),
145
+ filename=DASHQ_CONFIG_FILE,
146
+ revision=kwargs.get("revision"),
147
+ token=kwargs.get("token"),
148
+ cache_dir=kwargs.get("cache_dir"),
149
+ )
150
+ except Exception:
151
+ return {}
152
+ if path is None:
153
+ return {}
154
+ with open(path, encoding="utf-8") as f:
155
+ return json.load(f)
156
+
157
+
158
+ def _swap_quantized_modules(model: nn.Module, modules: Dict[str, Any]) -> int:
159
+ count = 0
160
+ for name, meta in modules.items():
161
+ target = _get_module(model, name)
162
+ if target is None or isinstance(target, DashQPackedLinear):
163
+ continue
164
+ module = DashQPackedLinear(
165
+ in_features=meta["in_features"],
166
+ out_features=meta["out_features"],
167
+ nbits=meta["nbits"],
168
+ group_size=meta["group_size"],
169
+ bias=getattr(target, "bias", None) is not None,
170
+ dtype=_DTYPES.get(meta.get("linear_dtype", "float16"), torch.float16),
171
+ quant_in_features=meta.get("quant_in_features"),
172
+ )
173
+ _set_module(model, name, module)
174
+ count += 1
175
+ return count
176
+
177
+
178
+ class DashQQwen3_5MoeForConditionalGeneration(Qwen3_5MoeForConditionalGeneration):
179
+ """Qwen3_5MoeForConditionalGeneration whose linear layers hold DASH-Q packed quantized weights."""
180
+
181
+ def __init__(self, config):
182
+ super().__init__(config)
183
+ modules = (getattr(config, "dashq_modules", None) or {})
184
+ if modules:
185
+ _swap_quantized_modules(self, modules)
186
+
187
+ @classmethod
188
+ def from_pretrained(cls, pretrained_model_name_or_path=None, *args, **kwargs):
189
+ config = kwargs.pop("config", None)
190
+ if config is None and pretrained_model_name_or_path is not None:
191
+ config = AutoConfig.from_pretrained(
192
+ pretrained_model_name_or_path,
193
+ trust_remote_code=True,
194
+ revision=kwargs.get("revision"),
195
+ token=kwargs.get("token"),
196
+ cache_dir=kwargs.get("cache_dir"),
197
+ )
198
+ if config is not None and not getattr(config, "dashq_modules", None):
199
+ spec = _load_dashq_spec(pretrained_model_name_or_path, **kwargs)
200
+ config.dashq_modules = spec.get("quantized_modules", {})
201
+ model = super().from_pretrained(pretrained_model_name_or_path, *args, config=config, **kwargs)
202
+ model.build_dashq_kernels()
203
+ return model
204
+
205
+ def build_dashq_kernels(self, verbose: bool = True) -> "DashQQwen3_5MoeForConditionalGeneration":
206
+ """Convert packed buffers to the Triton decode kernel (no-op off CUDA)."""
207
+ total = built = 0
208
+ for module in self.modules():
209
+ if isinstance(module, DashQPackedLinear):
210
+ total += 1
211
+ built += int(module.build_kernel())
212
+ if verbose and total:
213
+ if built:
214
+ print(f">> DASH-Q: {built}/{total} linear layers using the Triton decode kernel.")
215
+ else:
216
+ print(f">> DASH-Q: {total} quantized layers using the PyTorch fallback path.")
217
+ return self
218
+
219
+ def save_pretrained(self, *args, **kwargs):
220
+ released = any(
221
+ isinstance(m, DashQPackedLinear) and m.kernel is not None for m in self.modules()
222
+ )
223
+ if released:
224
+ raise RuntimeError(
225
+ "This model has been converted to the DASH-Q Triton kernel, so the packed "
226
+ "buffers are no longer materialized and saving would produce an incomplete "
227
+ "checkpoint. Reload with build_dashq_kernels() skipped if you need to re-save."
228
+ )
229
+ return super().save_pretrained(*args, **kwargs)