phanerozoic commited on
Commit
e33875a
·
verified ·
1 Parent(s): 2035283

batched decode; batch-invariance gates

Browse files
README.md CHANGED
@@ -179,6 +179,8 @@ to 1673 at batch 16 (6.5x aggregate).
179
 
180
  One CUDA device, greedy sampling in-kernel (route the fp32 logits to an
181
  external sampler for anything else). Llama-family architectures without
182
- rope attention scaling or attention biases. Prompts prefill in chunks of
183
- at most eight tokens. The program bakes device pointers, so the
184
- `MegaModel` owns its weights and buffers for its lifetime.
 
 
 
179
 
180
  One CUDA device, greedy sampling in-kernel (route the fp32 logits to an
181
  external sampler for anything else). Llama-family architectures without
182
+ rope attention scaling or attention biases. Weights are dense bf16 or
183
+ bitsandbytes nf4 (4-bit checkpoints load with their weights kept packed
184
+ and dequantized in the kernel). Prompts prefill in chunks of at most eight
185
+ tokens. The program bakes device pointers, so the `MegaModel` owns its
186
+ weights and buffers for its lifetime.
mak_cuda/megakernel.cu CHANGED
@@ -55,8 +55,20 @@ enum PhaseOp : int {
55
  OP_NORMB = 10, // RMSNorm (llama or gemma) [B][K] -> scratch
56
  OP_GLUB = 11, // SwiGLU / gelu-glu gating [B][2K] -> scratch [B][K]
57
  OP_ATTNFINB = 12, // finalize split-S attention [B] -> scratch [B][qdim]
 
58
  };
59
 
 
 
 
 
 
 
 
 
 
 
 
60
  enum InputTransform : int {
61
  IT_NONE = 0,
62
  IT_RMSNORM = 1,
@@ -1326,6 +1338,66 @@ __device__ void ph_gemv_plain(const long long* P, int M, int rs) {
1326
  }
1327
  }
1328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1329
  template <int NB>
1330
  __device__ __forceinline__ void dispatch_phase(const long long* P, int pos,
1331
  int M, int step_slot,
@@ -1343,6 +1415,7 @@ __device__ __forceinline__ void dispatch_phase(const long long* P, int pos,
1343
  case OP_NORMRES: ph_normres(P); break;
1344
  case OP_PLEMIX: ph_plemix(P); break;
1345
  case OP_GEMV_PLAIN: ph_gemv_plain<NB>(P, M, ring_stages); break;
 
1346
  case OP_NORMB: ph_normb(P, M); break;
1347
  case OP_GLUB: ph_glub(P, M); break;
1348
  case OP_ATTNFINB: ph_attnfinb(P, M, pos_b, step); break;
 
55
  OP_NORMB = 10, // RMSNorm (llama or gemma) [B][K] -> scratch
56
  OP_GLUB = 11, // SwiGLU / gelu-glu gating [B][2K] -> scratch [B][K]
57
  OP_ATTNFINB = 12, // finalize split-S attention [B] -> scratch [B][qdim]
58
+ OP_GEMV_Q4 = 13, // GEMV with nf4-packed weights, dequantized in-kernel
59
  };
60
 
61
+ // NF4 codebook (bitsandbytes): the 16 quantile levels a 4-bit index maps
62
+ // to. Dequantized weight = code[nibble] * per-block absmax, rounded to
63
+ // bf16; the lookup is exact and the multiply is IEEE, so it reproduces on
64
+ // every card.
65
+ __device__ __constant__ float NF4_CODE[16] = {
66
+ -1.0f, -0.6961928009986877f, -0.5250730514526367f,
67
+ -0.39491748809814453f, -0.28444138169288635f, -0.18477343022823334f,
68
+ -0.09105003625154495f, 0.0f, 0.07958029955625534f, 0.16093020141124725f,
69
+ 0.24611230194568634f, 0.33791524171829224f, 0.44070982933044434f,
70
+ 0.5626170039176941f, 0.7229568362236023f, 1.0f};
71
+
72
  enum InputTransform : int {
73
  IT_NONE = 0,
74
  IT_RMSNORM = 1,
 
1338
  }
1339
  }
1340
 
1341
+ // GEMV with nf4-packed weights [N][K/2] (two 4-bit indices per byte, row
1342
+ // major) and per-64-element fp32 absmax. Each lane dequantizes its own
1343
+ // weight tile (code[nibble] * absmax, rounded to bf16) and accumulates in
1344
+ // the exact order of the dense path, so the result is bit-identical to a
1345
+ // dense GEMV over the same dequantized weights and reproduces on every
1346
+ // card. Input is dense bf16 in a global buffer (a transform phase writes
1347
+ // it). Weights stay packed, so the quantized memory footprint is kept.
1348
+ template <int NB>
1349
+ __device__ void ph_gemv_q4(const long long* P, int M) {
1350
+ const unsigned char* W = reinterpret_cast<const unsigned char*>(P[2]);
1351
+ const float* absmax = reinterpret_cast<const float*>(P[6]);
1352
+ const bf16* resid = reinterpret_cast<const bf16*>(P[5]);
1353
+ const bf16* xin = reinterpret_cast<const bf16*>(P[1]);
1354
+ const long long N = P[7], K = P[8];
1355
+ const int epi = (int)P[10];
1356
+ const int lane = threadIdx.x & 31;
1357
+ const long long gwarp =
1358
+ ((long long)blockIdx.x * blockDim.x + threadIdx.x) >> 5;
1359
+ const long long nwarps = ((long long)gridDim.x * blockDim.x) >> 5;
1360
+ const long long kbase = (long long)lane * 8;
1361
+ const long long ntiles = (kbase < K) ? (K - kbase + 255) / 256 : 0;
1362
+ const long long Kh = K >> 1; // packed bytes per row
1363
+
1364
+ for (long long row = gwarp; row < N; row += nwarps) {
1365
+ float acc[NB];
1366
+ #pragma unroll
1367
+ for (int m = 0; m < NB; ++m) acc[m] = 0.f;
1368
+ for (long long t = 0; t < ntiles; ++t) {
1369
+ const long long k = kbase + t * 256;
1370
+ const unsigned int pw =
1371
+ *reinterpret_cast<const unsigned int*>(W + row * Kh + (k >> 1));
1372
+ const float am = absmax[(row * K + k) >> 6];
1373
+ float w8[8];
1374
+ #pragma unroll
1375
+ for (int b = 0; b < 4; ++b) {
1376
+ const unsigned int by = (pw >> (b * 8)) & 0xFFu;
1377
+ // bitsandbytes packs the earlier element in the high nibble
1378
+ w8[2 * b] = bf2f(f2bf(NF4_CODE[by >> 4] * am));
1379
+ w8[2 * b + 1] = bf2f(f2bf(NF4_CODE[by & 0xF] * am));
1380
+ }
1381
+ #pragma unroll
1382
+ for (int m = 0; m < NB; ++m) {
1383
+ if (m < M) {
1384
+ const bf16* xp = xin + (long long)m * K + k;
1385
+ #pragma unroll
1386
+ for (int j = 0; j < 8; ++j)
1387
+ acc[m] = fmaf(w8[j], bf2f(xp[j]), acc[m]);
1388
+ }
1389
+ }
1390
+ }
1391
+ #pragma unroll
1392
+ for (int m = 0; m < NB; ++m) {
1393
+ if (m < M) {
1394
+ const float a = warp_sum(acc[m]);
1395
+ if (lane == 0) gemv_store(P, epi, N, resid, m, row, a);
1396
+ }
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
  template <int NB>
1402
  __device__ __forceinline__ void dispatch_phase(const long long* P, int pos,
1403
  int M, int step_slot,
 
1415
  case OP_NORMRES: ph_normres(P); break;
1416
  case OP_PLEMIX: ph_plemix(P); break;
1417
  case OP_GEMV_PLAIN: ph_gemv_plain<NB>(P, M, ring_stages); break;
1418
+ case OP_GEMV_Q4: ph_gemv_q4<NB>(P, M); break;
1419
  case OP_NORMB: ph_normb(P, M); break;
1420
  case OP_GLUB: ph_glub(P, M); break;
1421
  case OP_ATTNFINB: ph_attnfinb(P, M, pos_b, step); break;
tests/test_mak.py CHANGED
@@ -212,6 +212,41 @@ def test_batch_invariance(models):
212
  assert all(perm[j] == solo[order[j]] for j in range(B))
213
 
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  @requires_cuda
216
  @pytest.mark.parametrize("model_id", ["Qwen/Qwen3-0.6B",
217
  "TinyLlama/TinyLlama-1.1B-Chat-v1.0"])
 
212
  assert all(perm[j] == solo[order[j]] for j in range(B))
213
 
214
 
215
+ @requires_cuda
216
+ def test_nf4_packed_matches_dequant():
217
+ """A bitsandbytes nf4 checkpoint loads with weights kept packed and
218
+ dequantized in the kernel, bitwise identical to the same weights
219
+ dequantized to bf16, and deterministic."""
220
+ bnb = pytest.importorskip("bitsandbytes")
221
+ transformers = pytest.importorskip("transformers")
222
+ from transformers import BitsAndBytesConfig, AutoModelForCausalLM
223
+ mid = "HuggingFaceTB/SmolLM2-135M"
224
+ try:
225
+ q = AutoModelForCausalLM.from_pretrained(
226
+ mid, quantization_config=BitsAndBytesConfig(
227
+ load_in_4bit=True, bnb_4bit_quant_type="nf4",
228
+ bnb_4bit_compute_dtype=torch.bfloat16,
229
+ bnb_4bit_use_double_quant=False))
230
+ except Exception as e:
231
+ pytest.skip(f"bitsandbytes 4-bit unavailable here: {e}")
232
+ mm = mak.MegaModel.from_pretrained(q, max_seq=512, max_gen=64)
233
+ assert mm._has_q4 and mm._layers[0]["wdown"]["packed"].dtype == torch.uint8
234
+ ref = AutoModelForCausalLM.from_pretrained(mid, dtype=torch.bfloat16)
235
+ for name, mod in q.named_modules():
236
+ if isinstance(mod, bnb.nn.Linear4bit):
237
+ w = bnb.functional.dequantize_4bit(mod.weight.data,
238
+ mod.weight.quant_state)
239
+ ref.get_submodule(name).weight.data = \
240
+ w.to(torch.bfloat16).cpu().clone()
241
+ mr = mak.MegaModel.from_pretrained(ref.cuda().eval(), max_seq=512,
242
+ max_gen=64)
243
+ prompt = [5, 9, 13, 21, 33, 41, 40, 32]
244
+ assert torch.equal(mm.decode_step(prompt[0], 0),
245
+ mr.decode_step(prompt[0], 0))
246
+ g = mm.generate(prompt, 32)
247
+ assert g == mr.generate(prompt, 32) == mm.generate(prompt, 32)
248
+
249
+
250
  @requires_cuda
251
  @pytest.mark.parametrize("model_id", ["Qwen/Qwen3-0.6B",
252
  "TinyLlama/TinyLlama-1.1B-Chat-v1.0"])
torch-ext/model_as_a_kernel/__init__.py CHANGED
@@ -28,6 +28,7 @@ _OP_EMBED, _OP_GEMV, _OP_QKV_POST, _OP_ATTN = 0, 1, 2, 3
28
  _OP_ARGMAX_PART, _OP_ARGMAX_FIN = 4, 5
29
  _OP_KV_APPEND, _OP_NORMRES, _OP_PLEMIX = 6, 7, 8
30
  _OP_GEMV_PLAIN, _OP_NORMB, _OP_GLUB, _OP_ATTNFINB = 9, 10, 11, 12
 
31
  _IT_NONE, _IT_RMSNORM, _IT_SWIGLU, _IT_ATTNFIN = 0, 1, 2, 3
32
  _IT_RMSNORM_G, _IT_GELU_GLU = 4, 5
33
  _EP_GELU_PLE, _EP_F32_AMAX_CAP = 5, 6
@@ -124,7 +125,7 @@ class MegaModel:
124
  self._keep: List[torch.Tensor] = []
125
  embed = pack(weights["embed"])
126
  final_norm = pack(weights["norm"])
127
- lm_head = pack_tiled(weights["lm_head"])
128
  inv_freq = weights.get("inv_freq")
129
  if inv_freq is None:
130
  d_idx = torch.arange(0, self.D, 2, dtype=torch.float32)
@@ -137,24 +138,49 @@ class MegaModel:
137
  self._invf = inv_freq
138
 
139
  qdim, kvdim = self.Hq * self.D, self.Hkv * self.D
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  self._layers = []
141
  for lw in weights["layers"]:
142
- assert lw["wqkv"].shape == (qdim + 2 * kvdim, self.hidden)
143
- assert lw["wo"].shape == (self.hidden, qdim)
144
- assert lw["wgu"].shape == (2 * self.I, self.hidden)
145
- assert lw["wdown"].shape == (self.hidden, self.I)
146
  layer = {
147
- "wqkv": pack_tiled(lw["wqkv"]), "wo": pack_tiled(lw["wo"]),
148
- "wgu": pack_tiled(lw["wgu"]),
149
- "wdown": pack_tiled(lw["wdown"]),
150
  "ln1": pack(lw["ln1"]), "ln2": pack(lw["ln2"]),
151
  "qn": pack(lw["qn"]) if self.qk_norm else None,
152
  "kn": pack(lw["kn"]) if self.qk_norm else None,
153
  }
 
 
154
  self._layers.append(layer)
155
  assert len(self._layers) == self.L
156
  assert embed.shape == (self.V, self.hidden)
157
- assert lm_head.numel() >= self.V * self.hidden # row-tiled, padded
 
 
 
158
  self._w_embed, self._w_fnorm, self._w_lmhead = (embed, final_norm,
159
  lm_head)
160
  self._qdim, self._kvdim = qdim, kvdim
@@ -202,6 +228,10 @@ class MegaModel:
202
  self._pos_b = None
203
  self._kv_bstride = 0
204
  self._gemma = False
 
 
 
 
205
  self._build_programs()
206
 
207
  # ------------------------------------------------------------------
@@ -222,7 +252,7 @@ class MegaModel:
222
  qdim, kvdim = self._qdim, self._kvdim
223
  embed, lm_head = self._w_embed, self._w_lmhead
224
  final_norm = self._w_fnorm
225
- xg = self._xg.data_ptr() if big else 0
226
  B = self._batch if batch else 1
227
  rows, names = [], []
228
  staged_elems = [0] # widest [B][K] panel any fused projection stages
@@ -233,52 +263,63 @@ class MegaModel:
233
  def note(K):
234
  staged_elems[0] = max(staged_elems[0], B * K)
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  def norm_gemv(name, gamma, w, out, N, K, epi, inp, resid=0,
237
  variant=_IT_RMSNORM):
238
- if big and not fits(K):
239
  rows.append(_row(_OP_NORMB, p1=inp, p2=gamma, p3=xg, k=K,
240
  it=variant, f0=self.eps))
241
  names.append(name + ".norm")
242
- rows.append(_row(_OP_GEMV_PLAIN, p1=xg, p2=w, p3=out, p5=resid,
243
- n=N, k=K, it=_tbit, epi=epi))
244
  names.append(name)
245
  else:
246
  note(K)
247
- rows.append(_row(_OP_GEMV, p1=inp, p2=w, p3=out, p4=gamma,
248
- p5=resid, n=N, k=K, it=variant | _tbit,
249
- epi=epi, f0=self.eps))
250
  names.append(name)
251
 
252
  def glu_gemv(name, gu, w, out, resid, variant=_IT_SWIGLU):
253
- if big and not fits(self.I):
254
  rows.append(_row(_OP_GLUB, p1=gu, p3=xg, k=self.I, it=variant))
255
  names.append(name + ".glu")
256
- rows.append(_row(_OP_GEMV_PLAIN, p1=xg, p2=w, p3=out, p5=resid,
257
- n=self.hidden, k=self.I, it=_tbit,
258
- epi=_EP_RESID))
259
  names.append(name)
260
  else:
261
  note(self.I)
262
- rows.append(_row(_OP_GEMV, p1=gu, p2=w, p3=out, p5=resid,
263
- n=self.hidden, k=self.I,
264
  it=variant | _tbit, epi=_EP_RESID))
265
  names.append(name)
266
 
267
  def attn_gemv(name, w, out, resid):
268
- if big and not fits(qdim):
269
  rows.append(_row(_OP_ATTNFINB, p1=self._partials.data_ptr(),
270
  p3=xg, k=qdim, hq=self.Hq,
271
  hkv=self._chunk << 16, d=self.D,
272
  i0=self._maxch))
273
  names.append(name + ".fin")
274
- rows.append(_row(_OP_GEMV_PLAIN, p1=xg, p2=w, p3=out, p5=resid,
275
- n=self.hidden, k=qdim, it=_tbit,
276
- epi=_EP_RESID))
277
  names.append(name)
278
  else:
279
  note(qdim)
280
  rows.append(_row(_OP_GEMV, p1=self._partials.data_ptr(),
281
- p2=w, p3=out, p5=resid, n=self.hidden, k=qdim,
 
282
  it=_IT_ATTNFIN | _tbit, epi=_EP_RESID,
283
  hq=self.Hq, hkv=self._chunk << 16, d=self.D,
284
  i0=self._maxch))
@@ -297,7 +338,7 @@ class MegaModel:
297
  kc = self._kvp(self._kcache, li, kv_slice)
298
  vc = self._kvp(self._vcache, li, kv_slice)
299
  norm_gemv(f"L{li}.qkv", lw["ln1"].data_ptr(),
300
- lw["wqkv"].data_ptr(), self._qkv.data_ptr(),
301
  qdim + 2 * kvdim, self.hidden, _EP_STORE, hid)
302
  rows.append(_row(6, p1=self._qkv.data_ptr(), p2=kc, p3=vc,
303
  p6=lw["kn"].data_ptr() if self.qk_norm else 0,
@@ -317,14 +358,13 @@ class MegaModel:
317
  hkv=self.Hkv | (self._chunk << 16),
318
  d=self.D, f0=scale, i0=self._maxch))
319
  names.append(f"L{li}.attn")
320
- attn_gemv(f"L{li}.o", lw["wo"].data_ptr(), hid, hid)
321
  norm_gemv(f"L{li}.gateup", lw["ln2"].data_ptr(),
322
- lw["wgu"].data_ptr(), self._gu.data_ptr(), 2 * self.I,
323
  self.hidden, _EP_STORE, hid)
324
- glu_gemv(f"L{li}.down", self._gu.data_ptr(),
325
- lw["wdown"].data_ptr(), hid, hid)
326
- if batch:
327
- norm_gemv("lm_head", final_norm.data_ptr(), lm_head.data_ptr(),
328
  self._logits.data_ptr(), self.V, self.hidden, _EP_F32,
329
  hid)
330
  rows.append(_row(_OP_ARGMAX_PART, p1=self._logits.data_ptr(),
@@ -402,8 +442,9 @@ class MegaModel:
402
  self._parts = torch.zeros(B * self._nblocks, dtype=torch.int64,
403
  device=dev)
404
  self._pos_b = torch.zeros(B, dtype=torch.int32, device=dev)
405
- # transformed-input scratch for the global-scratch path (B > 8)
406
- self._xg = torch.empty(B * self._maxk, dtype=dt, device=dev)
 
407
  # working buffers hold one row per active sequence; prefill chunks
408
  # still use up to chunk_m rows, so keep at least the fused capacity
409
  nb = max(8, B)
@@ -476,10 +517,67 @@ class MegaModel:
476
  return [[firsts[b]] + rest[b] for b in range(B)]
477
 
478
  # ------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  @classmethod
480
  def from_pretrained(cls, model, device="cuda", max_seq: int = 4096,
481
  max_gen: int = 4096):
482
- """Build from a transformers model (object or repo id)."""
 
 
483
  if isinstance(model, str):
484
  from transformers import AutoModelForCausalLM
485
  model = AutoModelForCausalLM.from_pretrained(
@@ -513,22 +611,22 @@ class MegaModel:
513
  raise ValueError("rope attention_scaling != 1 is not supported")
514
  inv_freq = rot.inv_freq.detach().float()
515
 
 
516
  layers = []
517
  for lyr in dec.layers:
518
  a, m = lyr.self_attn, lyr.mlp
519
  layers.append({
520
- "wqkv": torch.cat([a.q_proj.weight, a.k_proj.weight,
521
- a.v_proj.weight], 0),
522
- "wo": a.o_proj.weight,
523
- "wgu": torch.cat([m.gate_proj.weight, m.up_proj.weight], 0),
524
- "wdown": m.down_proj.weight,
525
  "ln1": lyr.input_layernorm.weight,
526
  "ln2": lyr.post_attention_layernorm.weight,
527
  "qn": a.q_norm.weight if qk_norm else None,
528
  "kn": a.k_norm.weight if qk_norm else None,
529
  })
530
  weights = {"embed": dec.embed_tokens.weight, "norm": dec.norm.weight,
531
- "lm_head": model.lm_head.weight, "layers": layers,
532
  "inv_freq": inv_freq}
533
  config = {
534
  "num_hidden_layers": hf.num_hidden_layers,
 
28
  _OP_ARGMAX_PART, _OP_ARGMAX_FIN = 4, 5
29
  _OP_KV_APPEND, _OP_NORMRES, _OP_PLEMIX = 6, 7, 8
30
  _OP_GEMV_PLAIN, _OP_NORMB, _OP_GLUB, _OP_ATTNFINB = 9, 10, 11, 12
31
+ _OP_GEMV_Q4 = 13
32
  _IT_NONE, _IT_RMSNORM, _IT_SWIGLU, _IT_ATTNFIN = 0, 1, 2, 3
33
  _IT_RMSNORM_G, _IT_GELU_GLU = 4, 5
34
  _EP_GELU_PLE, _EP_F32_AMAX_CAP = 5, 6
 
125
  self._keep: List[torch.Tensor] = []
126
  embed = pack(weights["embed"])
127
  final_norm = pack(weights["norm"])
128
+ lm_head = weights["lm_head"] # packed below (dense-tiled or nf4)
129
  inv_freq = weights.get("inv_freq")
130
  if inv_freq is None:
131
  d_idx = torch.arange(0, self.D, 2, dtype=torch.float32)
 
138
  self._invf = inv_freq
139
 
140
  qdim, kvdim = self.Hq * self.D, self.Hkv * self.D
141
+
142
+ def _is_q4(w):
143
+ return isinstance(w, dict) and w.get("q4")
144
+
145
+ def pack_w(w):
146
+ # nf4-packed weights move to the device intact; dense weights take
147
+ # the usual (optionally row-tiled) bf16 packing.
148
+ if _is_q4(w):
149
+ pk = w["packed"].to(dev).contiguous()
150
+ am = w["absmax"].to(device=dev, dtype=torch.float32).contiguous()
151
+ self._keep.append(pk)
152
+ self._keep.append(am)
153
+ return {"q4": True, "packed": pk, "absmax": am,
154
+ "N": int(w["N"]), "K": int(w["K"])}
155
+ return pack_tiled(w)
156
+
157
+ def _wshape(w, expected):
158
+ got = (int(w["N"]), int(w["K"])) if _is_q4(w) else tuple(w.shape)
159
+ assert got == expected, (got, expected)
160
+
161
+ self._has_q4 = False
162
  self._layers = []
163
  for lw in weights["layers"]:
164
+ _wshape(lw["wqkv"], (qdim + 2 * kvdim, self.hidden))
165
+ _wshape(lw["wo"], (self.hidden, qdim))
166
+ _wshape(lw["wgu"], (2 * self.I, self.hidden))
167
+ _wshape(lw["wdown"], (self.hidden, self.I))
168
  layer = {
169
+ "wqkv": pack_w(lw["wqkv"]), "wo": pack_w(lw["wo"]),
170
+ "wgu": pack_w(lw["wgu"]), "wdown": pack_w(lw["wdown"]),
 
171
  "ln1": pack(lw["ln1"]), "ln2": pack(lw["ln2"]),
172
  "qn": pack(lw["qn"]) if self.qk_norm else None,
173
  "kn": pack(lw["kn"]) if self.qk_norm else None,
174
  }
175
+ self._has_q4 |= any(_is_q4(layer[n]) for n in
176
+ ("wqkv", "wo", "wgu", "wdown"))
177
  self._layers.append(layer)
178
  assert len(self._layers) == self.L
179
  assert embed.shape == (self.V, self.hidden)
180
+ lm_head = pack_w(lm_head)
181
+ self._has_q4 |= _is_q4(lm_head)
182
+ if not _is_q4(lm_head):
183
+ assert lm_head.numel() >= self.V * self.hidden # row-tiled, padded
184
  self._w_embed, self._w_fnorm, self._w_lmhead = (embed, final_norm,
185
  lm_head)
186
  self._qdim, self._kvdim = qdim, kvdim
 
228
  self._pos_b = None
229
  self._kv_bstride = 0
230
  self._gemma = False
231
+ # transformed-input scratch: the nf4 and wide-batch paths write the
232
+ # dense bf16 input here (one row per active token) for the following
233
+ # plain/quant GEMV; sized for a prefill chunk (up to 8 rows)
234
+ self._xg = torch.empty(cm * self._maxk, dtype=dt, device=dev)
235
  self._build_programs()
236
 
237
  # ------------------------------------------------------------------
 
252
  qdim, kvdim = self._qdim, self._kvdim
253
  embed, lm_head = self._w_embed, self._w_lmhead
254
  final_norm = self._w_fnorm
255
+ xg = self._xg.data_ptr()
256
  B = self._batch if batch else 1
257
  rows, names = [], []
258
  staged_elems = [0] # widest [B][K] panel any fused projection stages
 
263
  def note(K):
264
  staged_elems[0] = max(staged_elems[0], B * K)
265
 
266
+ def _q4(w):
267
+ return isinstance(w, dict) and w.get("q4")
268
+
269
+ def _wgemv(out, w, N, K, epi, resid):
270
+ # the GEMV after a transform-to-scratch phase: nf4-dequant when
271
+ # the weight is packed, otherwise a plain bf16 GEMV
272
+ if _q4(w):
273
+ rows.append(_row(_OP_GEMV_Q4, p1=xg, p2=w["packed"].data_ptr(),
274
+ p3=out, p5=resid, p6=w["absmax"].data_ptr(),
275
+ n=N, k=K, epi=epi))
276
+ else:
277
+ rows.append(_row(_OP_GEMV_PLAIN, p1=xg, p2=w.data_ptr(),
278
+ p3=out, p5=resid, n=N, k=K, it=_tbit,
279
+ epi=epi))
280
+
281
  def norm_gemv(name, gamma, w, out, N, K, epi, inp, resid=0,
282
  variant=_IT_RMSNORM):
283
+ if _q4(w) or (big and not fits(K)):
284
  rows.append(_row(_OP_NORMB, p1=inp, p2=gamma, p3=xg, k=K,
285
  it=variant, f0=self.eps))
286
  names.append(name + ".norm")
287
+ _wgemv(out, w, N, K, epi, resid)
 
288
  names.append(name)
289
  else:
290
  note(K)
291
+ rows.append(_row(_OP_GEMV, p1=inp, p2=w.data_ptr(), p3=out,
292
+ p4=gamma, p5=resid, n=N, k=K,
293
+ it=variant | _tbit, epi=epi, f0=self.eps))
294
  names.append(name)
295
 
296
  def glu_gemv(name, gu, w, out, resid, variant=_IT_SWIGLU):
297
+ if _q4(w) or (big and not fits(self.I)):
298
  rows.append(_row(_OP_GLUB, p1=gu, p3=xg, k=self.I, it=variant))
299
  names.append(name + ".glu")
300
+ _wgemv(out, w, self.hidden, self.I, _EP_RESID, resid)
 
 
301
  names.append(name)
302
  else:
303
  note(self.I)
304
+ rows.append(_row(_OP_GEMV, p1=gu, p2=w.data_ptr(), p3=out,
305
+ p5=resid, n=self.hidden, k=self.I,
306
  it=variant | _tbit, epi=_EP_RESID))
307
  names.append(name)
308
 
309
  def attn_gemv(name, w, out, resid):
310
+ if _q4(w) or (big and not fits(qdim)):
311
  rows.append(_row(_OP_ATTNFINB, p1=self._partials.data_ptr(),
312
  p3=xg, k=qdim, hq=self.Hq,
313
  hkv=self._chunk << 16, d=self.D,
314
  i0=self._maxch))
315
  names.append(name + ".fin")
316
+ _wgemv(out, w, self.hidden, qdim, _EP_RESID, resid)
 
 
317
  names.append(name)
318
  else:
319
  note(qdim)
320
  rows.append(_row(_OP_GEMV, p1=self._partials.data_ptr(),
321
+ p2=w.data_ptr(), p3=out, p5=resid,
322
+ n=self.hidden, k=qdim,
323
  it=_IT_ATTNFIN | _tbit, epi=_EP_RESID,
324
  hq=self.Hq, hkv=self._chunk << 16, d=self.D,
325
  i0=self._maxch))
 
338
  kc = self._kvp(self._kcache, li, kv_slice)
339
  vc = self._kvp(self._vcache, li, kv_slice)
340
  norm_gemv(f"L{li}.qkv", lw["ln1"].data_ptr(),
341
+ lw["wqkv"], self._qkv.data_ptr(),
342
  qdim + 2 * kvdim, self.hidden, _EP_STORE, hid)
343
  rows.append(_row(6, p1=self._qkv.data_ptr(), p2=kc, p3=vc,
344
  p6=lw["kn"].data_ptr() if self.qk_norm else 0,
 
358
  hkv=self.Hkv | (self._chunk << 16),
359
  d=self.D, f0=scale, i0=self._maxch))
360
  names.append(f"L{li}.attn")
361
+ attn_gemv(f"L{li}.o", lw["wo"], hid, hid)
362
  norm_gemv(f"L{li}.gateup", lw["ln2"].data_ptr(),
363
+ lw["wgu"], self._gu.data_ptr(), 2 * self.I,
364
  self.hidden, _EP_STORE, hid)
365
+ glu_gemv(f"L{li}.down", self._gu.data_ptr(), lw["wdown"], hid, hid)
366
+ if batch or _q4(lm_head):
367
+ norm_gemv("lm_head", final_norm.data_ptr(), lm_head,
 
368
  self._logits.data_ptr(), self.V, self.hidden, _EP_F32,
369
  hid)
370
  rows.append(_row(_OP_ARGMAX_PART, p1=self._logits.data_ptr(),
 
442
  self._parts = torch.zeros(B * self._nblocks, dtype=torch.int64,
443
  device=dev)
444
  self._pos_b = torch.zeros(B, dtype=torch.int32, device=dev)
445
+ # transformed-input scratch: one row per active token, and prefill
446
+ # (during generate_batch) still uses up to chunk_m rows
447
+ self._xg = torch.empty(max(8, B) * self._maxk, dtype=dt, device=dev)
448
  # working buffers hold one row per active sequence; prefill chunks
449
  # still use up to chunk_m rows, so keep at least the fused capacity
450
  nb = max(8, B)
 
517
  return [[firsts[b]] + rest[b] for b in range(B)]
518
 
519
  # ------------------------------------------------------------------
520
+ @staticmethod
521
+ def _dense_weight(mod):
522
+ """The effective bf16 weight of a linear layer, dequantizing packed
523
+ quantized layers (bitsandbytes 4-bit and 8-bit) so a quantized
524
+ checkpoint loads directly. Dequantization is exact and does not
525
+ depend on the card, so inference on the result keeps every
526
+ guarantee; the weights materialize to bf16, so the quantized memory
527
+ footprint is not preserved (that needs the in-kernel packed path)."""
528
+ w = mod.weight
529
+ qs = getattr(w, "quant_state", None)
530
+ if qs is not None: # bitsandbytes 4-bit (nf4 / fp4)
531
+ import bitsandbytes as bnb
532
+ return bnb.functional.dequantize_4bit(w.data, qs).to(torch.bfloat16)
533
+ if getattr(w, "SCB", None) is not None or hasattr(mod, "SCB"):
534
+ # bitsandbytes 8-bit (LLM.int8): row scales in SCB, int8 in CB
535
+ scb = w.SCB if getattr(w, "SCB", None) is not None else mod.SCB
536
+ cb = w.data if w.data.dtype == torch.int8 else mod.CB
537
+ return (cb.to(torch.float32)
538
+ * (scb.to(torch.float32) / 127.0).unsqueeze(1)
539
+ ).to(torch.bfloat16)
540
+ return w
541
+
542
+ @staticmethod
543
+ def _q4_parts(mod):
544
+ """(packed [N, K/2] uint8, absmax [N*K/64] fp32) for a bitsandbytes
545
+ nf4 layer whose weights stay packed, else None. Double-quantized
546
+ absmax is materialized to fp32 so the kernel needs one scale array."""
547
+ w = getattr(mod, "weight", None)
548
+ qs = getattr(w, "quant_state", None)
549
+ if qs is None or getattr(qs, "quant_type", None) != "nf4":
550
+ return None
551
+ import bitsandbytes as bnb
552
+ n, k = int(qs.shape[0]), int(qs.shape[1])
553
+ if k % 64:
554
+ return None # kernel assumes 64 | K for per-block absmax
555
+ packed = w.data.reshape(n, k // 2).contiguous()
556
+ if getattr(qs, "nested", False):
557
+ absmax = bnb.functional.dequantize_blockwise(
558
+ qs.absmax, qs.state2) + qs.offset
559
+ else:
560
+ absmax = qs.absmax
561
+ return packed, absmax.float().reshape(-1).contiguous()
562
+
563
+ @classmethod
564
+ def _proj(cls, mods):
565
+ """A projection, concatenated across `mods` (q/k/v or gate/up). Stays
566
+ nf4-packed when every part is nf4, else a dense bf16 tensor."""
567
+ parts = [cls._q4_parts(m) for m in mods]
568
+ if all(p is not None for p in parts):
569
+ packed = torch.cat([p[0] for p in parts], 0)
570
+ absmax = torch.cat([p[1] for p in parts], 0)
571
+ return {"q4": True, "packed": packed, "absmax": absmax,
572
+ "N": packed.shape[0], "K": packed.shape[1] * 2}
573
+ return torch.cat([cls._dense_weight(m) for m in mods], 0)
574
+
575
  @classmethod
576
  def from_pretrained(cls, model, device="cuda", max_seq: int = 4096,
577
  max_gen: int = 4096):
578
+ """Build from a transformers model (object or repo id). A quantized
579
+ checkpoint (bitsandbytes 4-bit/8-bit) is accepted directly; its
580
+ weights are dequantized to bf16 at load."""
581
  if isinstance(model, str):
582
  from transformers import AutoModelForCausalLM
583
  model = AutoModelForCausalLM.from_pretrained(
 
611
  raise ValueError("rope attention_scaling != 1 is not supported")
612
  inv_freq = rot.inv_freq.detach().float()
613
 
614
+ pj = cls._proj
615
  layers = []
616
  for lyr in dec.layers:
617
  a, m = lyr.self_attn, lyr.mlp
618
  layers.append({
619
+ "wqkv": pj([a.q_proj, a.k_proj, a.v_proj]),
620
+ "wo": pj([a.o_proj]),
621
+ "wgu": pj([m.gate_proj, m.up_proj]),
622
+ "wdown": pj([m.down_proj]),
 
623
  "ln1": lyr.input_layernorm.weight,
624
  "ln2": lyr.post_attention_layernorm.weight,
625
  "qn": a.q_norm.weight if qk_norm else None,
626
  "kn": a.k_norm.weight if qk_norm else None,
627
  })
628
  weights = {"embed": dec.embed_tokens.weight, "norm": dec.norm.weight,
629
+ "lm_head": pj([model.lm_head]), "layers": layers,
630
  "inv_freq": inv_freq}
631
  config = {
632
  "num_hidden_layers": hf.num_hidden_layers,