ArthurZ HF Staff commited on
Commit
5d7f3dc
·
verified ·
1 Parent(s): a3c5e3e

Upload folder using huggingface_hub

Browse files
Files changed (31) hide show
  1. .gitattributes +4 -0
  2. build/torch210-metal-aarch64-darwin/__init__.py +600 -0
  3. build/torch210-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
  4. build/torch210-metal-aarch64-darwin/_ops.py +9 -0
  5. build/torch210-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
  6. build/torch210-metal-aarch64-darwin/metadata.json +4 -0
  7. build/torch210-metal-aarch64-darwin/nn.py +191 -0
  8. build/torch210-metal-aarch64-darwin/transformers_glue.py +284 -0
  9. build/torch211-metal-aarch64-darwin/__init__.py +600 -0
  10. build/torch211-metal-aarch64-darwin/__pycache__/__init__.cpython-313.pyc +0 -0
  11. build/torch211-metal-aarch64-darwin/__pycache__/_ops.cpython-313.pyc +0 -0
  12. build/torch211-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
  13. build/torch211-metal-aarch64-darwin/_ops.py +9 -0
  14. build/torch211-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
  15. build/torch211-metal-aarch64-darwin/metadata.json +4 -0
  16. build/torch211-metal-aarch64-darwin/nn.py +191 -0
  17. build/torch211-metal-aarch64-darwin/transformers_glue.py +284 -0
  18. build/torch28-metal-aarch64-darwin/__init__.py +600 -0
  19. build/torch28-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
  20. build/torch28-metal-aarch64-darwin/_ops.py +9 -0
  21. build/torch28-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
  22. build/torch28-metal-aarch64-darwin/metadata.json +4 -0
  23. build/torch28-metal-aarch64-darwin/nn.py +191 -0
  24. build/torch28-metal-aarch64-darwin/transformers_glue.py +284 -0
  25. build/torch29-metal-aarch64-darwin/__init__.py +600 -0
  26. build/torch29-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
  27. build/torch29-metal-aarch64-darwin/_ops.py +9 -0
  28. build/torch29-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
  29. build/torch29-metal-aarch64-darwin/metadata.json +4 -0
  30. build/torch29-metal-aarch64-darwin/nn.py +191 -0
  31. build/torch29-metal-aarch64-darwin/transformers_glue.py +284 -0
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ build/torch210-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so filter=lfs diff=lfs merge=lfs -text
37
+ build/torch211-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so filter=lfs diff=lfs merge=lfs -text
38
+ build/torch28-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so filter=lfs diff=lfs merge=lfs -text
39
+ build/torch29-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so filter=lfs diff=lfs merge=lfs -text
build/torch210-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GGUF dequantization kernels for Apple Silicon (Metal).
2
+
3
+ Six standalone kernels covering the GGUF formats most common in the wild:
4
+
5
+ Q4_0 Q8_0 Q4_K Q5_K Q6_K IQ4_NL IQ4_XS
6
+
7
+ Two API levels are exposed for each:
8
+
9
+ - ``dequantize_<format>_bytes(quant_bytes) -> np.ndarray``
10
+ One-shot path used by the tests for bit-exact validation against a
11
+ NumPy reference. Allocates input/output buffers internally.
12
+
13
+ - ``allocate_<format>_plan(nblocks)`` + ``dispatch_<format>(plan)``
14
+ Low-level path for benchmarks. Buffers are pre-allocated; ``dispatch``
15
+ only encodes + dispatches + syncs, so the timing isolates GPU work
16
+ from CPU<->buffer copies.
17
+
18
+ The runtime path uses PyObjC + Metal.framework so the correctness loop
19
+ works on any Apple Silicon Mac without Xcode 26 / Nix / kernel-builder.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ # Lazy globals — populated on first use.
29
+ _DEVICE = None
30
+ _COMMAND_QUEUE = None
31
+ _LIBRARY = None
32
+ _PIPELINES: dict = {}
33
+
34
+ _KERNEL_SOURCE_PATH = Path(__file__).resolve().parent.parent.parent / "gguf_dequant_metal" / "dequantize.metal"
35
+
36
+
37
+ def _ensure_metal():
38
+ global _DEVICE, _COMMAND_QUEUE, _LIBRARY
39
+ if _DEVICE is not None:
40
+ return
41
+ import Metal # pyobjc-framework-metal
42
+
43
+ _DEVICE = Metal.MTLCreateSystemDefaultDevice()
44
+ if _DEVICE is None:
45
+ raise RuntimeError("No default Metal device available")
46
+ _COMMAND_QUEUE = _DEVICE.newCommandQueue()
47
+
48
+ src = _KERNEL_SOURCE_PATH.read_text()
49
+ options = Metal.MTLCompileOptions.new()
50
+ lib, err = _DEVICE.newLibraryWithSource_options_error_(src, options, None)
51
+ if lib is None:
52
+ raise RuntimeError(f"Metal compile failed: {err}")
53
+ _LIBRARY = lib
54
+
55
+
56
+ def _pipeline(fn_name: str):
57
+ _ensure_metal()
58
+ pso = _PIPELINES.get(fn_name)
59
+ if pso is not None:
60
+ return pso
61
+
62
+ fn = _LIBRARY.newFunctionWithName_(fn_name)
63
+ if fn is None:
64
+ raise RuntimeError(f"Kernel function not found: {fn_name}")
65
+ pso, err = _DEVICE.newComputePipelineStateWithFunction_error_(fn, None)
66
+ if pso is None:
67
+ raise RuntimeError(f"Pipeline state creation failed: {err}")
68
+ _PIPELINES[fn_name] = pso
69
+ return pso
70
+
71
+
72
+ # (kernel_name, bytes_per_block, elems_per_block, threads_per_block_group)
73
+ _FORMATS = {
74
+ "q4_0": ("dequantize_q4_0", 18, 32, 16),
75
+ "q8_0": ("dequantize_q8_0", 34, 32, 32),
76
+ "q4_K": ("dequantize_q4_K", 144, 256, 256),
77
+ "q5_K": ("dequantize_q5_K", 176, 256, 256),
78
+ "q6_K": ("dequantize_q6_K", 210, 256, 256),
79
+ "iq4_nl": ("dequantize_iq4_nl", 18, 32, 16),
80
+ "iq4_xs": ("dequantize_iq4_xs", 136, 256, 256),
81
+ }
82
+
83
+
84
+ class Plan:
85
+ """Pre-allocated I/O buffers for a single (format, nblocks) shape."""
86
+
87
+ __slots__ = ("fmt", "nblocks", "in_buf", "out_buf", "_nblocks_bytes")
88
+
89
+ def __init__(self, fmt: str, nblocks: int, in_buf, out_buf):
90
+ self.fmt = fmt
91
+ self.nblocks = nblocks
92
+ self.in_buf = in_buf
93
+ self.out_buf = out_buf
94
+ self._nblocks_bytes = np.array([nblocks], dtype=np.uint32).tobytes()
95
+
96
+
97
+ def allocate_plan(fmt: str, nblocks: int) -> Plan:
98
+ import Metal
99
+
100
+ _ensure_metal()
101
+ _, bpb, epb, _ = _FORMATS[fmt]
102
+ in_buf = _DEVICE.newBufferWithLength_options_(nblocks * bpb, Metal.MTLResourceStorageModeShared)
103
+ out_buf = _DEVICE.newBufferWithLength_options_(nblocks * epb * 4, Metal.MTLResourceStorageModeShared)
104
+ return Plan(fmt=fmt, nblocks=nblocks, in_buf=in_buf, out_buf=out_buf)
105
+
106
+
107
+ def dispatch(plan: Plan, sync: bool = True):
108
+ import Metal
109
+
110
+ kernel_name, _, _, tg = _FORMATS[plan.fmt]
111
+ pso = _pipeline(kernel_name)
112
+ cmd = _COMMAND_QUEUE.commandBuffer()
113
+ enc = cmd.computeCommandEncoder()
114
+ enc.setComputePipelineState_(pso)
115
+ enc.setBuffer_offset_atIndex_(plan.in_buf, 0, 0)
116
+ enc.setBuffer_offset_atIndex_(plan.out_buf, 0, 1)
117
+ enc.setBytes_length_atIndex_(plan._nblocks_bytes, len(plan._nblocks_bytes), 2)
118
+ enc.dispatchThreads_threadsPerThreadgroup_(
119
+ Metal.MTLSizeMake(plan.nblocks * tg, 1, 1),
120
+ Metal.MTLSizeMake(tg, 1, 1),
121
+ )
122
+ enc.endEncoding()
123
+ cmd.commit()
124
+ if sync:
125
+ cmd.waitUntilCompleted()
126
+ return cmd
127
+
128
+
129
+ def dequantize_bytes(fmt: str, quant_bytes) -> np.ndarray:
130
+ """One-shot bytes-in / float32-out for the named format."""
131
+ _ensure_metal()
132
+ _, bpb, epb, _ = _FORMATS[fmt]
133
+
134
+ buf = bytes(quant_bytes) if not isinstance(quant_bytes, (bytes, bytearray)) else quant_bytes
135
+ nbytes = len(buf)
136
+ if nbytes % bpb != 0:
137
+ raise ValueError(f"{fmt} input not a multiple of {bpb} bytes: got {nbytes}")
138
+ nblocks = nbytes // bpb
139
+
140
+ plan = allocate_plan(fmt, nblocks)
141
+ in_view = np.frombuffer(plan.in_buf.contents().as_buffer(nbytes), dtype=np.uint8)
142
+ in_view[:] = np.frombuffer(buf, dtype=np.uint8)
143
+ dispatch(plan, sync=True)
144
+ out_nbytes = nblocks * epb * 4
145
+ return np.frombuffer(plan.out_buf.contents().as_buffer(out_nbytes), dtype=np.float32).copy()
146
+
147
+
148
+ # --- Per-format convenience wrappers ----------------------------------------
149
+
150
+ def dequantize_q4_0_bytes(qb): return dequantize_bytes("q4_0", qb)
151
+ def dequantize_q8_0_bytes(qb): return dequantize_bytes("q8_0", qb)
152
+ def dequantize_q4_K_bytes(qb): return dequantize_bytes("q4_K", qb)
153
+ def dequantize_q5_K_bytes(qb): return dequantize_bytes("q5_K", qb)
154
+ def dequantize_q6_K_bytes(qb): return dequantize_bytes("q6_K", qb)
155
+ def dequantize_iq4_nl_bytes(qb): return dequantize_bytes("iq4_nl", qb)
156
+ def dequantize_iq4_xs_bytes(qb): return dequantize_bytes("iq4_xs", qb)
157
+
158
+ def allocate_q4_0_plan(nblocks): return allocate_plan("q4_0", nblocks)
159
+ def allocate_q8_0_plan(nblocks): return allocate_plan("q8_0", nblocks)
160
+ def allocate_q4_K_plan(nblocks): return allocate_plan("q4_K", nblocks)
161
+ def allocate_q5_K_plan(nblocks): return allocate_plan("q5_K", nblocks)
162
+ def allocate_q6_K_plan(nblocks): return allocate_plan("q6_K", nblocks)
163
+ def allocate_iq4_nl_plan(nblocks): return allocate_plan("iq4_nl", nblocks)
164
+ def allocate_iq4_xs_plan(nblocks): return allocate_plan("iq4_xs", nblocks)
165
+
166
+ def dispatch_q4_0(plan, **kw): return dispatch(plan, **kw)
167
+ def dispatch_q8_0(plan, **kw): return dispatch(plan, **kw)
168
+ def dispatch_q4_K(plan, **kw): return dispatch(plan, **kw)
169
+ def dispatch_q5_K(plan, **kw): return dispatch(plan, **kw)
170
+ def dispatch_q6_K(plan, **kw): return dispatch(plan, **kw)
171
+ def dispatch_iq4_nl(plan, **kw): return dispatch(plan, **kw)
172
+ def dispatch_iq4_xs(plan, **kw): return dispatch(plan, **kw)
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Torch op entry points — the production path used by transformers (no PyObjC,
177
+ # no runtime compile). Only available in builds produced by kernel-builder,
178
+ # which generates `_ops.py` exporting `ops = torch.ops._gguf_dequant_<rev>`.
179
+ # In a dev-mode checkout (running from torch-ext/ directly) `_ops` isn't
180
+ # present, so the torch_* entry points raise on call.
181
+ # ---------------------------------------------------------------------------
182
+
183
+ try:
184
+ from ._ops import ops as _ops # generated by kernel-builder
185
+ except ImportError:
186
+ _ops = None
187
+
188
+
189
+ def _require_ops():
190
+ if _ops is None:
191
+ raise RuntimeError(
192
+ "torch op binding not available — this build was loaded from the dev tree. "
193
+ "Use the *_bytes / *_plan APIs (PyObjC path) or install a kernel-builder build."
194
+ )
195
+ return _ops
196
+
197
+
198
+ def torch_dequantize_q4_0(input, output): _require_ops().dequantize_q4_0(input, output)
199
+ def torch_dequantize_q8_0(input, output): _require_ops().dequantize_q8_0(input, output)
200
+ def torch_dequantize_q4_K(input, output): _require_ops().dequantize_q4_K(input, output)
201
+ def torch_dequantize_q5_K(input, output): _require_ops().dequantize_q5_K(input, output)
202
+ def torch_dequantize_q6_K(input, output): _require_ops().dequantize_q6_K(input, output)
203
+ def torch_dequantize_iq4_nl(input, output): _require_ops().dequantize_iq4_nl(input, output)
204
+ def torch_dequantize_iq4_xs(input, output): _require_ops().dequantize_iq4_xs(input, output)
205
+
206
+
207
+ # =============================================================================
208
+ # Q4_K × fp32 matmul — `GgufLinear` ceiling experiment.
209
+ # =============================================================================
210
+ # Layout convention (matches llama.cpp's kernel_mul_mm):
211
+ # A (Q4_K weights): shape (M, K) M = out_features, K = in_features.
212
+ # M and K must be multiples of 64 and 256 respectively for
213
+ # the simdgroup tile geometry to land cleanly.
214
+ # B (fp32 input): shape (N, K) N = batch * seqlen, contiguous in K.
215
+ # C (fp32 output): shape (M, N) column-major-ish (matches llama.cpp).
216
+
217
+ _BLOCK_SIZE_M = 64
218
+ _BLOCK_SIZE_N = 32
219
+ _THREAD_PER_BLOCK = 128
220
+ _SHARED_BYTES = 8192
221
+
222
+
223
+ class MatMulQ4KPlan:
224
+ __slots__ = ("M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
225
+
226
+ def __init__(self, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
227
+ self.M = M
228
+ self.N = N
229
+ self.K = K
230
+ self.a_buf = a_buf
231
+ self.b_buf = b_buf
232
+ self.c_buf = c_buf
233
+ self._dims_bytes = dims_bytes
234
+
235
+
236
+ def allocate_matmul_q4_K_plan(M: int, N: int, K: int) -> MatMulQ4KPlan:
237
+ """Pre-allocate weight (Q4_K), input (fp32), output (fp32) MTLBuffers."""
238
+ import Metal
239
+
240
+ _ensure_metal()
241
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
242
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
243
+ assert K % 256 == 0, f"K must be multiple of 256, got {K}"
244
+
245
+ a_bytes = M * (K // 256) * 144 # M rows × (K/QK_K) super-blocks × 144 bytes
246
+ b_bytes = N * K * 4 # fp32
247
+ c_bytes = M * N * 4 # fp32
248
+
249
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
250
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
251
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
252
+
253
+ # Pack ne00, ne01, ne11, nb01, nb11 into a uniforms buffer.
254
+ nb01 = (K // 256) * 144 # bytes per A row
255
+ nb11 = K * 4 # bytes per B row
256
+ dims = np.array([K], dtype=np.int64).tobytes() # ne00
257
+ dims += np.array([M], dtype=np.int64).tobytes() # ne01
258
+ dims += np.array([N], dtype=np.int64).tobytes() # ne11
259
+ dims += np.array([nb01], dtype=np.uint64).tobytes()
260
+ dims += np.array([nb11], dtype=np.uint64).tobytes()
261
+
262
+ return MatMulQ4KPlan(M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
263
+
264
+
265
+ def dispatch_matmul_q4_K(plan: MatMulQ4KPlan, sync: bool = True):
266
+ """Dispatch mul_mat_q4_K_f32 on the pre-allocated buffers."""
267
+ import Metal
268
+
269
+ pso = _pipeline("mul_mat_q4_K_f32")
270
+ cmd = _COMMAND_QUEUE.commandBuffer()
271
+ enc = cmd.computeCommandEncoder()
272
+ enc.setComputePipelineState_(pso)
273
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
274
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
275
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
276
+ # ne00 (K), ne01 (M), ne11 (N), nb01, nb11 via setBytes — 5 scalars
277
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
278
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
279
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
280
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
281
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
282
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
283
+
284
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
285
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
286
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
287
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
288
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
289
+ )
290
+ enc.endEncoding()
291
+ cmd.commit()
292
+ if sync:
293
+ cmd.waitUntilCompleted()
294
+ return cmd
295
+
296
+
297
+ # =============================================================================
298
+ # mul_mat_vec_q4_K — batch-1 decode (memory-bandwidth-bound regime).
299
+ # =============================================================================
300
+
301
+ _N_DST = 4
302
+ _SIMD_WIDTH = 32
303
+
304
+
305
+ class MatVecQ4KPlan:
306
+ __slots__ = ("M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
307
+
308
+ def __init__(self, M, K, a_buf, b_buf, c_buf, dims_bytes):
309
+ self.M = M
310
+ self.K = K
311
+ self.a_buf = a_buf
312
+ self.b_buf = b_buf
313
+ self.c_buf = c_buf
314
+ self._dims_bytes = dims_bytes
315
+
316
+
317
+ def allocate_matvec_q4_K_plan(M: int, K: int) -> MatVecQ4KPlan:
318
+ import Metal
319
+
320
+ _ensure_metal()
321
+ assert M % _N_DST == 0, f"M must be a multiple of {_N_DST}, got {M}"
322
+ assert K % 256 == 0, f"K must be a multiple of 256, got {K}"
323
+
324
+ a_bytes = M * (K // 256) * 144
325
+ b_bytes = K * 4
326
+ c_bytes = M * 4
327
+
328
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
329
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
330
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
331
+
332
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
333
+ return MatVecQ4KPlan(M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
334
+
335
+
336
+ def dispatch_matvec_q4_K(plan: MatVecQ4KPlan, sync: bool = True):
337
+ import Metal
338
+
339
+ pso = _pipeline("mul_mat_vec_q4_K_f32")
340
+ cmd = _COMMAND_QUEUE.commandBuffer()
341
+ enc = cmd.computeCommandEncoder()
342
+ enc.setComputePipelineState_(pso)
343
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
344
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
345
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
346
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3) # ne00 = K
347
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4) # ne01 = M
348
+
349
+ grid_x = (plan.M + _N_DST - 1) // _N_DST
350
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
351
+ Metal.MTLSizeMake(grid_x, 1, 1),
352
+ Metal.MTLSizeMake(_SIMD_WIDTH, 1, 1),
353
+ )
354
+ enc.endEncoding()
355
+ cmd.commit()
356
+ if sync:
357
+ cmd.waitUntilCompleted()
358
+ return cmd
359
+
360
+
361
+ # =============================================================================
362
+ # Q4_0 / Q8_0 matmul + matvec — same layout as Q4_K, smaller blocks (32 elems).
363
+ # =============================================================================
364
+
365
+ _N_SIMDGROUP = 2 # Q4_0/Q8_0 matvec uses 2 simdgroups per threadgroup
366
+
367
+ # Per quant type for the mat path: (block bytes, block elems, matvec_M_alignment).
368
+ # block_elems = 32 for Q4_0/Q8_0/IQ4_NL, 256 for all K-quants and IQ4_XS.
369
+ # matvec_M_alignment:
370
+ # Q4_0/Q8_0 → 2 simdgroups × N_DST(4) = 8 rows per threadgroup
371
+ # Q4_K → 1 simdgroup × N_DST(4) = 4 rows per threadgroup
372
+ # Q5_K → 2 simdgroups × 2 rows = 4 rows per threadgroup
373
+ # Q6_K → 2 simdgroups × 1 row = 2 rows per threadgroup
374
+ _MAT_FORMATS = {
375
+ "q4_0": (18, 32, 8),
376
+ "q8_0": (34, 32, 8),
377
+ "q4_K": (144, 256, 4),
378
+ "q5_K": (176, 256, 4),
379
+ "q6_K": (210, 256, 2),
380
+ "iq4_nl": (18, 32, 4), # 2 sg × 2 rows/sg = 4 rows/threadgroup
381
+ "iq4_xs": (136, 256, 4),
382
+ }
383
+
384
+
385
+ class MatMulPlan:
386
+ """Pre-allocated I/O buffers for `mul_mat_<fmt>_f32`. Block element size
387
+ depends on the format (32 for Q4_0/Q8_0, 256 for Q4_K)."""
388
+
389
+ __slots__ = ("fmt", "M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
390
+
391
+ def __init__(self, fmt, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
392
+ self.fmt = fmt
393
+ self.M = M
394
+ self.N = N
395
+ self.K = K
396
+ self.a_buf = a_buf
397
+ self.b_buf = b_buf
398
+ self.c_buf = c_buf
399
+ self._dims_bytes = dims_bytes
400
+
401
+
402
+ def _quant_block_elems(fmt: str) -> int:
403
+ return _MAT_FORMATS[fmt][1]
404
+
405
+
406
+ def _quant_block_bytes(fmt: str) -> int:
407
+ return _MAT_FORMATS[fmt][0]
408
+
409
+
410
+ def _matvec_row_alignment(fmt: str) -> int | None:
411
+ return _MAT_FORMATS[fmt][2]
412
+
413
+
414
+ def allocate_matmul_plan(fmt: str, M: int, N: int, K: int) -> MatMulPlan:
415
+ import Metal
416
+
417
+ _ensure_metal()
418
+ assert fmt in _MAT_FORMATS, f"unsupported fmt {fmt}"
419
+ block_elems = _quant_block_elems(fmt)
420
+ block_bytes = _quant_block_bytes(fmt)
421
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
422
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
423
+ assert K % block_elems == 0, f"K must be multiple of {block_elems} for {fmt}, got {K}"
424
+
425
+ a_bytes = M * (K // block_elems) * block_bytes
426
+ b_bytes = N * K * 4
427
+ c_bytes = M * N * 4
428
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
429
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
430
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
431
+
432
+ nb01 = (K // block_elems) * block_bytes
433
+ nb11 = K * 4
434
+ dims = (
435
+ np.array([K], dtype=np.int64).tobytes()
436
+ + np.array([M], dtype=np.int64).tobytes()
437
+ + np.array([N], dtype=np.int64).tobytes()
438
+ + np.array([nb01], dtype=np.uint64).tobytes()
439
+ + np.array([nb11], dtype=np.uint64).tobytes()
440
+ )
441
+ return MatMulPlan(fmt=fmt, M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
442
+
443
+
444
+ def dispatch_matmul(plan: MatMulPlan, sync: bool = True):
445
+ import Metal
446
+
447
+ pso = _pipeline(f"mul_mat_{plan.fmt}_f32")
448
+ cmd = _COMMAND_QUEUE.commandBuffer()
449
+ enc = cmd.computeCommandEncoder()
450
+ enc.setComputePipelineState_(pso)
451
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
452
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
453
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
454
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
455
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
456
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
457
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
458
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
459
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
460
+
461
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
462
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
463
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
464
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
465
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
466
+ )
467
+ enc.endEncoding()
468
+ cmd.commit()
469
+ if sync:
470
+ cmd.waitUntilCompleted()
471
+ return cmd
472
+
473
+
474
+ class MatVecPlan:
475
+ __slots__ = ("fmt", "M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
476
+
477
+ def __init__(self, fmt, M, K, a_buf, b_buf, c_buf, dims_bytes):
478
+ self.fmt = fmt
479
+ self.M = M
480
+ self.K = K
481
+ self.a_buf = a_buf
482
+ self.b_buf = b_buf
483
+ self.c_buf = c_buf
484
+ self._dims_bytes = dims_bytes
485
+
486
+
487
+ def allocate_matvec_plan(fmt: str, M: int, K: int) -> MatVecPlan:
488
+ """Allocate I/O for `mul_mat_vec_<fmt>_f32`. Format-specific row alignment:
489
+ Q4_0/Q8_0: 8, Q4_K: 4, Q5_K: 4, Q6_K: 2."""
490
+ import Metal
491
+
492
+ _ensure_metal()
493
+ block_elems = _quant_block_elems(fmt)
494
+ block_bytes = _quant_block_bytes(fmt)
495
+ align = _matvec_row_alignment(fmt)
496
+ if align is None:
497
+ raise ValueError(f"mul_mat_vec not implemented for {fmt}")
498
+ assert M % align == 0, f"M must be a multiple of {align} for {fmt}, got {M}"
499
+ assert K % block_elems == 0, f"K must be a multiple of {block_elems} for {fmt}, got {K}"
500
+
501
+ a_bytes = M * (K // block_elems) * block_bytes
502
+ b_bytes = K * 4
503
+ c_bytes = M * 4
504
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
505
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
506
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
507
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
508
+ return MatVecPlan(fmt=fmt, M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
509
+
510
+
511
+ def encode_matvec(plan: MatVecPlan, enc):
512
+ """Encode a single matvec dispatch onto an existing MTLComputeCommandEncoder.
513
+ Caller is responsible for committing the parent command buffer. Threadgroup
514
+ memory for the IQ4 lookup table is set via setThreadgroupMemoryLength here."""
515
+ import Metal
516
+
517
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
518
+ enc.setComputePipelineState_(pso)
519
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
520
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
521
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
522
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
523
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
524
+
525
+ if plan.fmt == "q4_K":
526
+ rows_per_tg = _N_DST
527
+ tg_threads = _SIMD_WIDTH
528
+ elif plan.fmt in ("q4_0", "q8_0"):
529
+ rows_per_tg = _N_SIMDGROUP * _N_DST
530
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
531
+ elif plan.fmt == "q5_K":
532
+ rows_per_tg = _N_SIMDGROUP * 2
533
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
534
+ elif plan.fmt == "q6_K":
535
+ rows_per_tg = _N_SIMDGROUP * 1
536
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
537
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
538
+ rows_per_tg = _N_SIMDGROUP * 2
539
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
540
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0)
541
+ else:
542
+ raise ValueError(f"matvec not wired for {plan.fmt}")
543
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
544
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
545
+ Metal.MTLSizeMake(grid_x, 1, 1),
546
+ Metal.MTLSizeMake(tg_threads, 1, 1),
547
+ )
548
+
549
+
550
+ def new_command_buffer():
551
+ """Start a fresh MTLCommandBuffer. Caller batches `encode_matvec(plan, enc)`
552
+ onto its encoder, then calls `cb.commit()` and `cb.waitUntilCompleted()`
553
+ once for the whole batch — matching llama.cpp's per-step semantics."""
554
+ _ensure_metal()
555
+ return _COMMAND_QUEUE.commandBuffer()
556
+
557
+
558
+ def dispatch_matvec(plan: MatVecPlan, sync: bool = True):
559
+ import Metal
560
+
561
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
562
+ cmd = _COMMAND_QUEUE.commandBuffer()
563
+ enc = cmd.computeCommandEncoder()
564
+ enc.setComputePipelineState_(pso)
565
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
566
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
567
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
568
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
569
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
570
+
571
+ needs_shared = plan.fmt in ("iq4_nl", "iq4_xs")
572
+ if plan.fmt == "q4_K":
573
+ rows_per_tg = _N_DST
574
+ tg_threads = _SIMD_WIDTH
575
+ elif plan.fmt in ("q4_0", "q8_0"):
576
+ rows_per_tg = _N_SIMDGROUP * _N_DST
577
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
578
+ elif plan.fmt == "q5_K":
579
+ rows_per_tg = _N_SIMDGROUP * 2
580
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
581
+ elif plan.fmt == "q6_K":
582
+ rows_per_tg = _N_SIMDGROUP * 1
583
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
584
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
585
+ rows_per_tg = _N_SIMDGROUP * 2 # 2 sg × 2 rows/sg
586
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
587
+ else:
588
+ raise ValueError(f"matvec not wired for {plan.fmt}")
589
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
590
+ if needs_shared:
591
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0) # 64 floats per simdgroup × 2 sg
592
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
593
+ Metal.MTLSizeMake(grid_x, 1, 1),
594
+ Metal.MTLSizeMake(tg_threads, 1, 1),
595
+ )
596
+ enc.endEncoding()
597
+ cmd.commit()
598
+ if sync:
599
+ cmd.waitUntilCompleted()
600
+ return cmd
build/torch210-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e502779f760fa35dac6a8aea0c4e63e6dbac0641cb3daf0ae34e4115754899d8
3
+ size 237040
build/torch210-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _gguf_dequant_530e374
3
+ ops = torch.ops._gguf_dequant_530e374
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_gguf_dequant_530e374::{op_name}"
build/torch210-metal-aarch64-darwin/gguf_dequant/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import sys
3
+
4
+ import importlib
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch210-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "python-depends": []
4
+ }
build/torch210-metal-aarch64-darwin/nn.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GgufLinear — drop-in nn.Linear replacement that runs quantized matmul directly.
2
+
3
+ Forward pass:
4
+ - If the input has effective batch size 1 (single token), use mul_mat_vec
5
+ (memory-bandwidth-bound, faster than bf16 mat-vec on MPS).
6
+ - Otherwise use mul_mat (compute-bound, similar throughput to bf16 GEMM but
7
+ keeps weights at native quantization → 3–4× less RAM than bf16).
8
+
9
+ Supported quant types today: Q4_0, Q8_0, Q4_K. Adding more is just a matter of
10
+ porting the candle kernels — the dispatch logic is generic over `quant_type`.
11
+
12
+ Prototype design: weights are stored as raw uint8 bytes in `qweight`. The
13
+ forward path copies activations into a shared MTLBuffer, runs the Metal
14
+ kernel, reads the fp32 output back. Production path (kernels-community +
15
+ PR #767) skips the copies via direct MTLBuffer handoff.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import numpy as np
22
+ import torch
23
+
24
+ from . import (
25
+ allocate_matmul_plan,
26
+ allocate_matvec_plan,
27
+ dispatch_matmul,
28
+ dispatch_matvec,
29
+ )
30
+
31
+
32
+ # Per-quant-type geometry: (block_bytes, block_elems, matvec_M_alignment)
33
+ _QUANT_INFO = {
34
+ "Q4_0": (18, 32, 8), # matvec uses 2 simdgroups × N_DST=4 = 8 rows/threadgroup
35
+ "Q8_0": (34, 32, 8),
36
+ "Q4_K": (144, 256, 4), # matvec uses 1 simdgroup × N_DST=4 = 4 rows/threadgroup
37
+ }
38
+
39
+ # Map our Python-side quant name to the Metal kernel suffix.
40
+ _KERNEL_FMT = {
41
+ "Q4_0": "q4_0",
42
+ "Q8_0": "q8_0",
43
+ "Q4_K": "q4_K",
44
+ }
45
+
46
+
47
+ class GgufLinear(torch.nn.Module):
48
+ """Linear layer backed by GGUF-quantized weights.
49
+
50
+ Args:
51
+ in_features: K dimension. Must be a multiple of the format's block size
52
+ (32 for Q4_0/Q8_0, 256 for Q4_K).
53
+ out_features: M dimension. Must be a multiple of the format's matvec
54
+ row alignment (8 for Q4_0/Q8_0, 4 for Q4_K) and of 64 for matmul.
55
+ quant_type: one of "Q4_0", "Q8_0", "Q4_K".
56
+ bias: whether to add a learnable bias after the matmul.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ in_features: int,
62
+ out_features: int,
63
+ quant_type: str = "Q4_K",
64
+ bias: bool = False,
65
+ device: Optional[torch.device | str] = None,
66
+ ):
67
+ super().__init__()
68
+ if quant_type not in _QUANT_INFO:
69
+ raise NotImplementedError(
70
+ f"GgufLinear supports {sorted(_QUANT_INFO)} today, got {quant_type}"
71
+ )
72
+ block_bytes, block_elems, matvec_align = _QUANT_INFO[quant_type]
73
+ if in_features % block_elems != 0:
74
+ raise ValueError(
75
+ f"in_features must be a multiple of {block_elems} for {quant_type}, got {in_features}"
76
+ )
77
+ if out_features % matvec_align != 0:
78
+ raise ValueError(
79
+ f"out_features must be a multiple of {matvec_align} for {quant_type}, got {out_features}"
80
+ )
81
+
82
+ self.in_features = in_features
83
+ self.out_features = out_features
84
+ self.quant_type = quant_type
85
+ self._fmt = _KERNEL_FMT[quant_type]
86
+ self._block_bytes = block_bytes
87
+ self._block_elems = block_elems
88
+
89
+ nblocks_per_row = in_features // block_elems
90
+ qweight_nbytes = out_features * nblocks_per_row * block_bytes
91
+ self.register_buffer(
92
+ "qweight",
93
+ torch.empty(qweight_nbytes, dtype=torch.uint8, device=device or "cpu"),
94
+ persistent=True,
95
+ )
96
+
97
+ if bias:
98
+ self.bias = torch.nn.Parameter(
99
+ torch.zeros(out_features, dtype=torch.float32, device=device or "cpu"),
100
+ requires_grad=False,
101
+ )
102
+ else:
103
+ self.register_parameter("bias", None)
104
+
105
+ def extra_repr(self) -> str:
106
+ return (
107
+ f"in_features={self.in_features}, out_features={self.out_features}, "
108
+ f"quant_type={self.quant_type}, bias={self.bias is not None}"
109
+ )
110
+
111
+ @torch.no_grad()
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ """x: shape (..., K). Returns (..., M) fp32."""
114
+ if x.shape[-1] != self.in_features:
115
+ raise ValueError(f"Last dim of x must be {self.in_features}, got {x.shape[-1]}")
116
+
117
+ batch_shape = x.shape[:-1]
118
+ x_flat = x.reshape(-1, self.in_features).contiguous().to(torch.float32)
119
+ N = x_flat.shape[0]
120
+
121
+ # CPU view of qweight for the bytes copy into shared MTLBuffers.
122
+ # Production path skips this once the kernels-community build is wired.
123
+ qweight_cpu = self.qweight.detach().to("cpu", copy=False)
124
+ x_cpu = x_flat.to("cpu", copy=False)
125
+
126
+ if N == 1:
127
+ out = self._forward_matvec(qweight_cpu, x_cpu.reshape(-1))
128
+ else:
129
+ out = self._forward_matmul(qweight_cpu, x_cpu)
130
+
131
+ out = out.reshape(*batch_shape, self.out_features)
132
+ if x.device != out.device:
133
+ out = out.to(x.device)
134
+ if self.bias is not None:
135
+ out = out + self.bias.to(out.device).to(out.dtype)
136
+ return out
137
+
138
+ def _forward_matvec(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
139
+ M, K = self.out_features, self.in_features
140
+ plan = allocate_matvec_plan(self._fmt, M=M, K=K)
141
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
142
+ a_view[:] = qweight_cpu.numpy()
143
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(K * 4), dtype=np.float32)
144
+ b_view[:] = x_cpu.numpy()
145
+ dispatch_matvec(plan, sync=True)
146
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * 4), dtype=np.float32)
147
+ return torch.from_numpy(c_view.copy())
148
+
149
+ def _forward_matmul(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
150
+ N, K = x_cpu.shape
151
+ M = self.out_features
152
+ plan = allocate_matmul_plan(self._fmt, M=M, N=N, K=K)
153
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
154
+ a_view[:] = qweight_cpu.numpy()
155
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(N * K * 4), dtype=np.float32)
156
+ b_view[:] = x_cpu.numpy().reshape(-1)
157
+ dispatch_matmul(plan, sync=True)
158
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * N * 4), dtype=np.float32)
159
+ # Kernel output layout is (N, M) in row-major; we want (N, M).
160
+ return torch.from_numpy(c_view.reshape(N, M).copy())
161
+
162
+ @classmethod
163
+ def from_quantized_bytes(
164
+ cls,
165
+ quant_type: str,
166
+ qweight_bytes: bytes,
167
+ in_features: int,
168
+ out_features: int,
169
+ bias: Optional[torch.Tensor] = None,
170
+ device: Optional[torch.device | str] = None,
171
+ ) -> "GgufLinear":
172
+ """Convenience for tests/loaders: hydrate from raw GGUF block bytes."""
173
+ layer = cls(in_features=in_features, out_features=out_features,
174
+ quant_type=quant_type, bias=bias is not None, device=device)
175
+ expected = layer.qweight.numel()
176
+ if len(qweight_bytes) != expected:
177
+ raise ValueError(
178
+ f"qweight_bytes length {len(qweight_bytes)} != expected {expected} "
179
+ f"for {quant_type} M={out_features} K={in_features}"
180
+ )
181
+ layer.qweight.copy_(torch.frombuffer(bytearray(qweight_bytes), dtype=torch.uint8))
182
+ if bias is not None:
183
+ layer.bias.copy_(bias.to(torch.float32))
184
+ return layer
185
+
186
+ # Back-compat shim for the existing Q4_K-only test
187
+ @classmethod
188
+ def from_q4_K_bytes(cls, qweight_bytes, in_features, out_features, bias=None, device=None):
189
+ return cls.from_quantized_bytes(
190
+ "Q4_K", qweight_bytes, in_features, out_features, bias=bias, device=device
191
+ )
build/torch210-metal-aarch64-darwin/transformers_glue.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Glue for plugging `GgufLinear` into a transformers model.
2
+
3
+ The flow is intentionally non-invasive: we read raw block bytes directly from
4
+ the GGUF file via `gguf.GGUFReader`, walk the model, and for every `nn.Linear`
5
+ whose corresponding GGUF tensor is one of our supported quant types we
6
+ in-place replace the module with `GgufLinear` and copy the bytes into
7
+ `qweight`. Tensors that don't map to a Linear (or are unsupported quants like
8
+ F32 norms, embeddings) get dequantized via `gguf.dequantize` and copied into
9
+ the existing parameter as fp32.
10
+
11
+ This avoids modifying the transformers loader. The trade-off is that the
12
+ model has to be constructed first with regular `nn.Linear` shells (which we
13
+ then mutate), but those shells are on the meta device or freshly initialised
14
+ and don't carry the dequantized weight at the time of swap — so peak memory
15
+ during load is bounded by `(model_size_fp32) + (model_size_quantized_qweights)`,
16
+ which fits in RAM for the small models we care about.
17
+
18
+ Typical use:
19
+
20
+ from transformers import AutoConfig, AutoModelForCausalLM
21
+ cfg = AutoConfig.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct-GGUF",
22
+ gguf_file="qwen2.5-0.5b-instruct-q4_0.gguf")
23
+ model = AutoModelForCausalLM.from_config(cfg).eval()
24
+ load_gguf_into_model_with_gguf_linear(
25
+ model,
26
+ gguf_path="…/qwen2.5-0.5b-instruct-q4_0.gguf",
27
+ )
28
+ out = model.generate(input_ids=…, max_new_tokens=10)
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+ import gguf
36
+ import numpy as np
37
+ import torch
38
+ import torch.nn as nn
39
+
40
+ from .nn import GgufLinear, _QUANT_INFO
41
+
42
+
43
+ # Map gguf.GGMLQuantizationType.name → our GgufLinear quant_type string.
44
+ _GGUF_NAME_TO_GGUF_LINEAR = {
45
+ "Q4_0": "Q4_0",
46
+ "Q8_0": "Q8_0",
47
+ "Q4_K": "Q4_K",
48
+ }
49
+
50
+
51
+ def _quant_supported_by_gguf_linear(qt: gguf.GGMLQuantizationType) -> bool:
52
+ return qt.name in _GGUF_NAME_TO_GGUF_LINEAR
53
+
54
+
55
+ # --- Tensor-name mapping ------------------------------------------------------
56
+ # GGUF tensors use llama.cpp-style names like `blk.0.attn_q.weight`. HF models
57
+ # use `model.layers.0.self_attn.q_proj.weight`. We use a tiny version of the
58
+ # transformers rename table here so the swap util can stay self-contained.
59
+ # (Transformers' own GGUF loader has a much more thorough version of this.)
60
+
61
+ _BASIC_RENAMES = [
62
+ (r"^blk\.", "model.layers."),
63
+ (r"\.attn_q\.weight$", ".self_attn.q_proj.weight"),
64
+ (r"\.attn_k\.weight$", ".self_attn.k_proj.weight"),
65
+ (r"\.attn_v\.weight$", ".self_attn.v_proj.weight"),
66
+ (r"\.attn_output\.weight$", ".self_attn.o_proj.weight"),
67
+ (r"\.ffn_gate\.weight$", ".mlp.gate_proj.weight"),
68
+ (r"\.ffn_up\.weight$", ".mlp.up_proj.weight"),
69
+ (r"\.ffn_down\.weight$", ".mlp.down_proj.weight"),
70
+ (r"\.attn_norm\.weight$", ".input_layernorm.weight"),
71
+ (r"\.ffn_norm\.weight$", ".post_attention_layernorm.weight"),
72
+ (r"\.attn_(q|k|v)\.bias$", r".self_attn.\1_proj.bias"),
73
+ (r"^token_embd\.weight$", "model.embed_tokens.weight"),
74
+ (r"^output_norm\.weight$", "model.norm.weight"),
75
+ (r"^output\.weight$", "lm_head.weight"),
76
+ ]
77
+
78
+
79
+ def _gguf_name_to_hf_name(gguf_name: str) -> str:
80
+ """Apply our minimal rename table to map a GGUF tensor name to an HF parameter name."""
81
+ import re
82
+
83
+ out = gguf_name
84
+ for pat, repl in _BASIC_RENAMES:
85
+ out = re.sub(pat, repl, out)
86
+ return out
87
+
88
+
89
+ def _is_attn_qk(gguf_name: str) -> bool:
90
+ return ".attn_q.weight" in gguf_name or ".attn_k.weight" in gguf_name
91
+
92
+
93
+ def _reverse_permute_qk(w: np.ndarray, n_head: int, n_kv: int | None) -> np.ndarray:
94
+ """GGUF Q/K weights are permuted on quantize; undo before loading into HF.
95
+ Reference: https://github.com/ggerganov/llama.cpp/blob/.../convert_hf_to_gguf.py."""
96
+ if n_kv is not None and n_head != n_kv:
97
+ n_head = n_kv
98
+ dim = w.shape[0] // n_head // 2
99
+ w = w.reshape(n_head, dim, 2, *w.shape[1:])
100
+ return w.swapaxes(2, 1).reshape(*w.shape[:1], *w.shape[3:]) if False else w.reshape(-1, *w.shape[3:]).swapaxes(2, 1).reshape(n_head * dim * 2, *w.shape[3:])
101
+
102
+
103
+ def _set_module_param(model: nn.Module, qualified_name: str, value: torch.Tensor) -> bool:
104
+ """Set a parameter or buffer on `model` by dotted name. Returns True on success."""
105
+ parts = qualified_name.split(".")
106
+ mod = model
107
+ for p in parts[:-1]:
108
+ if not hasattr(mod, p):
109
+ return False
110
+ mod = getattr(mod, p)
111
+ leaf = parts[-1]
112
+ if leaf in mod._parameters:
113
+ param = mod._parameters[leaf]
114
+ if param is None:
115
+ return False
116
+ with torch.no_grad():
117
+ mod._parameters[leaf] = nn.Parameter(value.to(dtype=param.dtype, device=param.device),
118
+ requires_grad=param.requires_grad)
119
+ return True
120
+ if leaf in mod._buffers:
121
+ with torch.no_grad():
122
+ mod._buffers[leaf] = value
123
+ return True
124
+ return False
125
+
126
+
127
+ def _get_module_by_name(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
128
+ """Return the leaf module matching `qualified_name` minus its terminal attribute,
129
+ or None if anything along the path is missing."""
130
+ parts = qualified_name.split(".")
131
+ mod = model
132
+ for p in parts[:-1]:
133
+ if not hasattr(mod, p):
134
+ return None
135
+ mod = getattr(mod, p)
136
+ return mod
137
+
138
+
139
+ def _get_attr_by_path(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
140
+ """Walk dotted `qualified_name` on `model` and return the final attribute,
141
+ or None if anything along the path is missing."""
142
+ if not qualified_name:
143
+ return model
144
+ mod = model
145
+ for p in qualified_name.split("."):
146
+ if not hasattr(mod, p):
147
+ return None
148
+ mod = getattr(mod, p)
149
+ return mod
150
+
151
+
152
+ def _swap_linear_to_gguf(
153
+ parent: nn.Module, attr: str, in_features: int, out_features: int, quant_type: str,
154
+ bias_tensor: Optional[torch.Tensor], device: Optional[torch.device | str] = None
155
+ ) -> GgufLinear:
156
+ """Replace `parent.attr` (an nn.Linear) with a new GgufLinear of the same shape."""
157
+ new = GgufLinear(in_features=in_features, out_features=out_features,
158
+ quant_type=quant_type, bias=bias_tensor is not None, device=device)
159
+ if bias_tensor is not None and new.bias is not None:
160
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
161
+ setattr(parent, attr, new)
162
+ return new
163
+
164
+
165
+ def load_gguf_into_model_with_gguf_linear(
166
+ model: nn.Module,
167
+ gguf_path: str | Path,
168
+ skip_modules: Optional[set[str]] = None,
169
+ force_dequant: bool = False,
170
+ ) -> dict:
171
+ """Read a GGUF file and populate `model` in-place.
172
+
173
+ Two-phase load to keep the byte path simple and the bias path correct:
174
+
175
+ Phase 1 — full dequant: every tensor (linear weights, norms, embeddings,
176
+ biases) is dequantized to fp32 and written into the matching HF
177
+ parameter. After this pass the model would already produce correct
178
+ outputs (this is what `force_dequant=True` stops at).
179
+ Phase 2 — swap linears: walk every `nn.Linear` that maps to a tensor
180
+ whose GGUF quant type is supported by `GgufLinear`; swap it for a
181
+ `GgufLinear` and re-attach raw block bytes (preserving the bias that
182
+ Phase 1 just loaded).
183
+
184
+ Returns a dict with diagnostic counts.
185
+ """
186
+ reader = gguf.GGUFReader(str(gguf_path))
187
+
188
+ # Pull a few config bits for the Q/K reverse-permute.
189
+ def _read_int(field: str, default: int = 0) -> int:
190
+ f = reader.fields.get(field)
191
+ if f is None:
192
+ return default
193
+ return int(f.parts[f.data[0]][0]) if f.data else default
194
+
195
+ arch = ""
196
+ arch_field = reader.fields.get("general.architecture")
197
+ if arch_field is not None and arch_field.data:
198
+ arch = bytes(arch_field.parts[arch_field.data[0]]).decode("utf-8", errors="ignore")
199
+ n_head = _read_int(f"{arch}.attention.head_count")
200
+ n_kv_head = _read_int(f"{arch}.attention.head_count_kv") or n_head
201
+
202
+ skip_modules = skip_modules or set()
203
+ stats = {"as_gguf_linear": 0, "dequantized": 0, "skipped": 0, "unmatched": []}
204
+
205
+ # Track which (hf_name → quant_type) pairs are eligible for GgufLinear swap
206
+ # after Phase 1. We'll re-quantize the loaded fp32 weight in Phase 2.
207
+ swap_candidates: list[tuple[str, str]] = []
208
+
209
+ # ---------------- Phase 1: dequant everything into the model. -------------
210
+ for t in reader.tensors:
211
+ hf_name = _gguf_name_to_hf_name(t.name)
212
+ if any(skip in hf_name for skip in skip_modules):
213
+ stats["skipped"] += 1
214
+ continue
215
+
216
+ parent = _get_module_by_name(model, hf_name)
217
+ if parent is None or not hasattr(parent, hf_name.rsplit(".", 1)[-1]):
218
+ stats["unmatched"].append(t.name)
219
+ continue
220
+ leaf = hf_name.rsplit(".", 1)[-1]
221
+ cur = getattr(parent, leaf)
222
+
223
+ fp32 = gguf.dequantize(
224
+ np.frombuffer(np.ascontiguousarray(t.data).tobytes(), dtype=np.uint8),
225
+ t.tensor_type,
226
+ ).astype(np.float32)
227
+ if _is_attn_qk(t.name) and n_head > 0:
228
+ shape = list(t.shape)
229
+ M, K = int(shape[1]), int(shape[0])
230
+ heads = n_head if ".attn_q." in t.name else n_kv_head
231
+ head_dim = M // heads
232
+ fp32 = fp32.reshape(M, K).reshape(heads, head_dim // 2, 2, K).swapaxes(2, 1).reshape(M, K)
233
+ target_shape = tuple(cur.shape) if hasattr(cur, "shape") else (fp32.size,)
234
+ if np.prod(target_shape) == fp32.size:
235
+ fp32 = fp32.reshape(target_shape)
236
+ tensor = torch.from_numpy(fp32)
237
+ if not _set_module_param(model, hf_name, tensor):
238
+ stats["unmatched"].append(t.name)
239
+ continue
240
+ stats["dequantized"] += 1
241
+
242
+ # Record candidacy for swap in Phase 2.
243
+ is_linear_weight = isinstance(parent, nn.Linear) and leaf == "weight"
244
+ if is_linear_weight and _quant_supported_by_gguf_linear(t.tensor_type) and not force_dequant:
245
+ swap_candidates.append((hf_name, _GGUF_NAME_TO_GGUF_LINEAR[t.tensor_type.name]))
246
+
247
+ # ---------------- Phase 2: swap Linear → GgufLinear. ----------------------
248
+ # Re-quantize the loaded fp32 weight via gguf-py (works for Q4_0/Q8_0;
249
+ # K-quants don't have a Python quantizer yet — those will skip swap).
250
+ for hf_name, quant_type in swap_candidates:
251
+ # hf_name ends in `.weight`; the Linear module is at hf_name[:-len(".weight")]
252
+ linear_path = hf_name[: -len(".weight")]
253
+ grandparent_path, _, linear_attr = linear_path.rpartition(".")
254
+ grandparent = model if not grandparent_path else _get_attr_by_path(model, grandparent_path)
255
+ linear = _get_attr_by_path(model, linear_path)
256
+ if grandparent is None or not isinstance(linear, nn.Linear):
257
+ continue
258
+
259
+ # Re-quantize the (now permuted-if-applicable) fp32 weight.
260
+ try:
261
+ qt = getattr(gguf.GGMLQuantizationType, quant_type)
262
+ qbytes = bytes(
263
+ gguf.quantize(linear.weight.detach().cpu().numpy().astype(np.float32), qt).tobytes()
264
+ )
265
+ except (NotImplementedError, Exception) as e:
266
+ # K-quants etc. — leave as Linear (fp32 weight is already loaded).
267
+ stats.setdefault("re_quantize_failed", []).append((hf_name, str(e)))
268
+ continue
269
+
270
+ bias_tensor = linear.bias.detach().clone() if linear.bias is not None else None
271
+ new = GgufLinear(
272
+ in_features=linear.in_features,
273
+ out_features=linear.out_features,
274
+ quant_type=quant_type,
275
+ bias=bias_tensor is not None,
276
+ )
277
+ if bias_tensor is not None:
278
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
279
+ new.qweight.copy_(torch.frombuffer(bytearray(qbytes), dtype=torch.uint8))
280
+ setattr(grandparent, linear_attr, new)
281
+ stats["as_gguf_linear"] += 1
282
+ stats["dequantized"] -= 1 # this one's no longer "just" dequant — swapped
283
+
284
+ return stats
build/torch211-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GGUF dequantization kernels for Apple Silicon (Metal).
2
+
3
+ Six standalone kernels covering the GGUF formats most common in the wild:
4
+
5
+ Q4_0 Q8_0 Q4_K Q5_K Q6_K IQ4_NL IQ4_XS
6
+
7
+ Two API levels are exposed for each:
8
+
9
+ - ``dequantize_<format>_bytes(quant_bytes) -> np.ndarray``
10
+ One-shot path used by the tests for bit-exact validation against a
11
+ NumPy reference. Allocates input/output buffers internally.
12
+
13
+ - ``allocate_<format>_plan(nblocks)`` + ``dispatch_<format>(plan)``
14
+ Low-level path for benchmarks. Buffers are pre-allocated; ``dispatch``
15
+ only encodes + dispatches + syncs, so the timing isolates GPU work
16
+ from CPU<->buffer copies.
17
+
18
+ The runtime path uses PyObjC + Metal.framework so the correctness loop
19
+ works on any Apple Silicon Mac without Xcode 26 / Nix / kernel-builder.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ # Lazy globals — populated on first use.
29
+ _DEVICE = None
30
+ _COMMAND_QUEUE = None
31
+ _LIBRARY = None
32
+ _PIPELINES: dict = {}
33
+
34
+ _KERNEL_SOURCE_PATH = Path(__file__).resolve().parent.parent.parent / "gguf_dequant_metal" / "dequantize.metal"
35
+
36
+
37
+ def _ensure_metal():
38
+ global _DEVICE, _COMMAND_QUEUE, _LIBRARY
39
+ if _DEVICE is not None:
40
+ return
41
+ import Metal # pyobjc-framework-metal
42
+
43
+ _DEVICE = Metal.MTLCreateSystemDefaultDevice()
44
+ if _DEVICE is None:
45
+ raise RuntimeError("No default Metal device available")
46
+ _COMMAND_QUEUE = _DEVICE.newCommandQueue()
47
+
48
+ src = _KERNEL_SOURCE_PATH.read_text()
49
+ options = Metal.MTLCompileOptions.new()
50
+ lib, err = _DEVICE.newLibraryWithSource_options_error_(src, options, None)
51
+ if lib is None:
52
+ raise RuntimeError(f"Metal compile failed: {err}")
53
+ _LIBRARY = lib
54
+
55
+
56
+ def _pipeline(fn_name: str):
57
+ _ensure_metal()
58
+ pso = _PIPELINES.get(fn_name)
59
+ if pso is not None:
60
+ return pso
61
+
62
+ fn = _LIBRARY.newFunctionWithName_(fn_name)
63
+ if fn is None:
64
+ raise RuntimeError(f"Kernel function not found: {fn_name}")
65
+ pso, err = _DEVICE.newComputePipelineStateWithFunction_error_(fn, None)
66
+ if pso is None:
67
+ raise RuntimeError(f"Pipeline state creation failed: {err}")
68
+ _PIPELINES[fn_name] = pso
69
+ return pso
70
+
71
+
72
+ # (kernel_name, bytes_per_block, elems_per_block, threads_per_block_group)
73
+ _FORMATS = {
74
+ "q4_0": ("dequantize_q4_0", 18, 32, 16),
75
+ "q8_0": ("dequantize_q8_0", 34, 32, 32),
76
+ "q4_K": ("dequantize_q4_K", 144, 256, 256),
77
+ "q5_K": ("dequantize_q5_K", 176, 256, 256),
78
+ "q6_K": ("dequantize_q6_K", 210, 256, 256),
79
+ "iq4_nl": ("dequantize_iq4_nl", 18, 32, 16),
80
+ "iq4_xs": ("dequantize_iq4_xs", 136, 256, 256),
81
+ }
82
+
83
+
84
+ class Plan:
85
+ """Pre-allocated I/O buffers for a single (format, nblocks) shape."""
86
+
87
+ __slots__ = ("fmt", "nblocks", "in_buf", "out_buf", "_nblocks_bytes")
88
+
89
+ def __init__(self, fmt: str, nblocks: int, in_buf, out_buf):
90
+ self.fmt = fmt
91
+ self.nblocks = nblocks
92
+ self.in_buf = in_buf
93
+ self.out_buf = out_buf
94
+ self._nblocks_bytes = np.array([nblocks], dtype=np.uint32).tobytes()
95
+
96
+
97
+ def allocate_plan(fmt: str, nblocks: int) -> Plan:
98
+ import Metal
99
+
100
+ _ensure_metal()
101
+ _, bpb, epb, _ = _FORMATS[fmt]
102
+ in_buf = _DEVICE.newBufferWithLength_options_(nblocks * bpb, Metal.MTLResourceStorageModeShared)
103
+ out_buf = _DEVICE.newBufferWithLength_options_(nblocks * epb * 4, Metal.MTLResourceStorageModeShared)
104
+ return Plan(fmt=fmt, nblocks=nblocks, in_buf=in_buf, out_buf=out_buf)
105
+
106
+
107
+ def dispatch(plan: Plan, sync: bool = True):
108
+ import Metal
109
+
110
+ kernel_name, _, _, tg = _FORMATS[plan.fmt]
111
+ pso = _pipeline(kernel_name)
112
+ cmd = _COMMAND_QUEUE.commandBuffer()
113
+ enc = cmd.computeCommandEncoder()
114
+ enc.setComputePipelineState_(pso)
115
+ enc.setBuffer_offset_atIndex_(plan.in_buf, 0, 0)
116
+ enc.setBuffer_offset_atIndex_(plan.out_buf, 0, 1)
117
+ enc.setBytes_length_atIndex_(plan._nblocks_bytes, len(plan._nblocks_bytes), 2)
118
+ enc.dispatchThreads_threadsPerThreadgroup_(
119
+ Metal.MTLSizeMake(plan.nblocks * tg, 1, 1),
120
+ Metal.MTLSizeMake(tg, 1, 1),
121
+ )
122
+ enc.endEncoding()
123
+ cmd.commit()
124
+ if sync:
125
+ cmd.waitUntilCompleted()
126
+ return cmd
127
+
128
+
129
+ def dequantize_bytes(fmt: str, quant_bytes) -> np.ndarray:
130
+ """One-shot bytes-in / float32-out for the named format."""
131
+ _ensure_metal()
132
+ _, bpb, epb, _ = _FORMATS[fmt]
133
+
134
+ buf = bytes(quant_bytes) if not isinstance(quant_bytes, (bytes, bytearray)) else quant_bytes
135
+ nbytes = len(buf)
136
+ if nbytes % bpb != 0:
137
+ raise ValueError(f"{fmt} input not a multiple of {bpb} bytes: got {nbytes}")
138
+ nblocks = nbytes // bpb
139
+
140
+ plan = allocate_plan(fmt, nblocks)
141
+ in_view = np.frombuffer(plan.in_buf.contents().as_buffer(nbytes), dtype=np.uint8)
142
+ in_view[:] = np.frombuffer(buf, dtype=np.uint8)
143
+ dispatch(plan, sync=True)
144
+ out_nbytes = nblocks * epb * 4
145
+ return np.frombuffer(plan.out_buf.contents().as_buffer(out_nbytes), dtype=np.float32).copy()
146
+
147
+
148
+ # --- Per-format convenience wrappers ----------------------------------------
149
+
150
+ def dequantize_q4_0_bytes(qb): return dequantize_bytes("q4_0", qb)
151
+ def dequantize_q8_0_bytes(qb): return dequantize_bytes("q8_0", qb)
152
+ def dequantize_q4_K_bytes(qb): return dequantize_bytes("q4_K", qb)
153
+ def dequantize_q5_K_bytes(qb): return dequantize_bytes("q5_K", qb)
154
+ def dequantize_q6_K_bytes(qb): return dequantize_bytes("q6_K", qb)
155
+ def dequantize_iq4_nl_bytes(qb): return dequantize_bytes("iq4_nl", qb)
156
+ def dequantize_iq4_xs_bytes(qb): return dequantize_bytes("iq4_xs", qb)
157
+
158
+ def allocate_q4_0_plan(nblocks): return allocate_plan("q4_0", nblocks)
159
+ def allocate_q8_0_plan(nblocks): return allocate_plan("q8_0", nblocks)
160
+ def allocate_q4_K_plan(nblocks): return allocate_plan("q4_K", nblocks)
161
+ def allocate_q5_K_plan(nblocks): return allocate_plan("q5_K", nblocks)
162
+ def allocate_q6_K_plan(nblocks): return allocate_plan("q6_K", nblocks)
163
+ def allocate_iq4_nl_plan(nblocks): return allocate_plan("iq4_nl", nblocks)
164
+ def allocate_iq4_xs_plan(nblocks): return allocate_plan("iq4_xs", nblocks)
165
+
166
+ def dispatch_q4_0(plan, **kw): return dispatch(plan, **kw)
167
+ def dispatch_q8_0(plan, **kw): return dispatch(plan, **kw)
168
+ def dispatch_q4_K(plan, **kw): return dispatch(plan, **kw)
169
+ def dispatch_q5_K(plan, **kw): return dispatch(plan, **kw)
170
+ def dispatch_q6_K(plan, **kw): return dispatch(plan, **kw)
171
+ def dispatch_iq4_nl(plan, **kw): return dispatch(plan, **kw)
172
+ def dispatch_iq4_xs(plan, **kw): return dispatch(plan, **kw)
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Torch op entry points — the production path used by transformers (no PyObjC,
177
+ # no runtime compile). Only available in builds produced by kernel-builder,
178
+ # which generates `_ops.py` exporting `ops = torch.ops._gguf_dequant_<rev>`.
179
+ # In a dev-mode checkout (running from torch-ext/ directly) `_ops` isn't
180
+ # present, so the torch_* entry points raise on call.
181
+ # ---------------------------------------------------------------------------
182
+
183
+ try:
184
+ from ._ops import ops as _ops # generated by kernel-builder
185
+ except ImportError:
186
+ _ops = None
187
+
188
+
189
+ def _require_ops():
190
+ if _ops is None:
191
+ raise RuntimeError(
192
+ "torch op binding not available — this build was loaded from the dev tree. "
193
+ "Use the *_bytes / *_plan APIs (PyObjC path) or install a kernel-builder build."
194
+ )
195
+ return _ops
196
+
197
+
198
+ def torch_dequantize_q4_0(input, output): _require_ops().dequantize_q4_0(input, output)
199
+ def torch_dequantize_q8_0(input, output): _require_ops().dequantize_q8_0(input, output)
200
+ def torch_dequantize_q4_K(input, output): _require_ops().dequantize_q4_K(input, output)
201
+ def torch_dequantize_q5_K(input, output): _require_ops().dequantize_q5_K(input, output)
202
+ def torch_dequantize_q6_K(input, output): _require_ops().dequantize_q6_K(input, output)
203
+ def torch_dequantize_iq4_nl(input, output): _require_ops().dequantize_iq4_nl(input, output)
204
+ def torch_dequantize_iq4_xs(input, output): _require_ops().dequantize_iq4_xs(input, output)
205
+
206
+
207
+ # =============================================================================
208
+ # Q4_K × fp32 matmul — `GgufLinear` ceiling experiment.
209
+ # =============================================================================
210
+ # Layout convention (matches llama.cpp's kernel_mul_mm):
211
+ # A (Q4_K weights): shape (M, K) M = out_features, K = in_features.
212
+ # M and K must be multiples of 64 and 256 respectively for
213
+ # the simdgroup tile geometry to land cleanly.
214
+ # B (fp32 input): shape (N, K) N = batch * seqlen, contiguous in K.
215
+ # C (fp32 output): shape (M, N) column-major-ish (matches llama.cpp).
216
+
217
+ _BLOCK_SIZE_M = 64
218
+ _BLOCK_SIZE_N = 32
219
+ _THREAD_PER_BLOCK = 128
220
+ _SHARED_BYTES = 8192
221
+
222
+
223
+ class MatMulQ4KPlan:
224
+ __slots__ = ("M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
225
+
226
+ def __init__(self, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
227
+ self.M = M
228
+ self.N = N
229
+ self.K = K
230
+ self.a_buf = a_buf
231
+ self.b_buf = b_buf
232
+ self.c_buf = c_buf
233
+ self._dims_bytes = dims_bytes
234
+
235
+
236
+ def allocate_matmul_q4_K_plan(M: int, N: int, K: int) -> MatMulQ4KPlan:
237
+ """Pre-allocate weight (Q4_K), input (fp32), output (fp32) MTLBuffers."""
238
+ import Metal
239
+
240
+ _ensure_metal()
241
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
242
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
243
+ assert K % 256 == 0, f"K must be multiple of 256, got {K}"
244
+
245
+ a_bytes = M * (K // 256) * 144 # M rows × (K/QK_K) super-blocks × 144 bytes
246
+ b_bytes = N * K * 4 # fp32
247
+ c_bytes = M * N * 4 # fp32
248
+
249
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
250
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
251
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
252
+
253
+ # Pack ne00, ne01, ne11, nb01, nb11 into a uniforms buffer.
254
+ nb01 = (K // 256) * 144 # bytes per A row
255
+ nb11 = K * 4 # bytes per B row
256
+ dims = np.array([K], dtype=np.int64).tobytes() # ne00
257
+ dims += np.array([M], dtype=np.int64).tobytes() # ne01
258
+ dims += np.array([N], dtype=np.int64).tobytes() # ne11
259
+ dims += np.array([nb01], dtype=np.uint64).tobytes()
260
+ dims += np.array([nb11], dtype=np.uint64).tobytes()
261
+
262
+ return MatMulQ4KPlan(M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
263
+
264
+
265
+ def dispatch_matmul_q4_K(plan: MatMulQ4KPlan, sync: bool = True):
266
+ """Dispatch mul_mat_q4_K_f32 on the pre-allocated buffers."""
267
+ import Metal
268
+
269
+ pso = _pipeline("mul_mat_q4_K_f32")
270
+ cmd = _COMMAND_QUEUE.commandBuffer()
271
+ enc = cmd.computeCommandEncoder()
272
+ enc.setComputePipelineState_(pso)
273
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
274
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
275
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
276
+ # ne00 (K), ne01 (M), ne11 (N), nb01, nb11 via setBytes — 5 scalars
277
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
278
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
279
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
280
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
281
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
282
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
283
+
284
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
285
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
286
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
287
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
288
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
289
+ )
290
+ enc.endEncoding()
291
+ cmd.commit()
292
+ if sync:
293
+ cmd.waitUntilCompleted()
294
+ return cmd
295
+
296
+
297
+ # =============================================================================
298
+ # mul_mat_vec_q4_K — batch-1 decode (memory-bandwidth-bound regime).
299
+ # =============================================================================
300
+
301
+ _N_DST = 4
302
+ _SIMD_WIDTH = 32
303
+
304
+
305
+ class MatVecQ4KPlan:
306
+ __slots__ = ("M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
307
+
308
+ def __init__(self, M, K, a_buf, b_buf, c_buf, dims_bytes):
309
+ self.M = M
310
+ self.K = K
311
+ self.a_buf = a_buf
312
+ self.b_buf = b_buf
313
+ self.c_buf = c_buf
314
+ self._dims_bytes = dims_bytes
315
+
316
+
317
+ def allocate_matvec_q4_K_plan(M: int, K: int) -> MatVecQ4KPlan:
318
+ import Metal
319
+
320
+ _ensure_metal()
321
+ assert M % _N_DST == 0, f"M must be a multiple of {_N_DST}, got {M}"
322
+ assert K % 256 == 0, f"K must be a multiple of 256, got {K}"
323
+
324
+ a_bytes = M * (K // 256) * 144
325
+ b_bytes = K * 4
326
+ c_bytes = M * 4
327
+
328
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
329
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
330
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
331
+
332
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
333
+ return MatVecQ4KPlan(M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
334
+
335
+
336
+ def dispatch_matvec_q4_K(plan: MatVecQ4KPlan, sync: bool = True):
337
+ import Metal
338
+
339
+ pso = _pipeline("mul_mat_vec_q4_K_f32")
340
+ cmd = _COMMAND_QUEUE.commandBuffer()
341
+ enc = cmd.computeCommandEncoder()
342
+ enc.setComputePipelineState_(pso)
343
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
344
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
345
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
346
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3) # ne00 = K
347
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4) # ne01 = M
348
+
349
+ grid_x = (plan.M + _N_DST - 1) // _N_DST
350
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
351
+ Metal.MTLSizeMake(grid_x, 1, 1),
352
+ Metal.MTLSizeMake(_SIMD_WIDTH, 1, 1),
353
+ )
354
+ enc.endEncoding()
355
+ cmd.commit()
356
+ if sync:
357
+ cmd.waitUntilCompleted()
358
+ return cmd
359
+
360
+
361
+ # =============================================================================
362
+ # Q4_0 / Q8_0 matmul + matvec — same layout as Q4_K, smaller blocks (32 elems).
363
+ # =============================================================================
364
+
365
+ _N_SIMDGROUP = 2 # Q4_0/Q8_0 matvec uses 2 simdgroups per threadgroup
366
+
367
+ # Per quant type for the mat path: (block bytes, block elems, matvec_M_alignment).
368
+ # block_elems = 32 for Q4_0/Q8_0/IQ4_NL, 256 for all K-quants and IQ4_XS.
369
+ # matvec_M_alignment:
370
+ # Q4_0/Q8_0 → 2 simdgroups × N_DST(4) = 8 rows per threadgroup
371
+ # Q4_K → 1 simdgroup × N_DST(4) = 4 rows per threadgroup
372
+ # Q5_K → 2 simdgroups × 2 rows = 4 rows per threadgroup
373
+ # Q6_K → 2 simdgroups × 1 row = 2 rows per threadgroup
374
+ _MAT_FORMATS = {
375
+ "q4_0": (18, 32, 8),
376
+ "q8_0": (34, 32, 8),
377
+ "q4_K": (144, 256, 4),
378
+ "q5_K": (176, 256, 4),
379
+ "q6_K": (210, 256, 2),
380
+ "iq4_nl": (18, 32, 4), # 2 sg × 2 rows/sg = 4 rows/threadgroup
381
+ "iq4_xs": (136, 256, 4),
382
+ }
383
+
384
+
385
+ class MatMulPlan:
386
+ """Pre-allocated I/O buffers for `mul_mat_<fmt>_f32`. Block element size
387
+ depends on the format (32 for Q4_0/Q8_0, 256 for Q4_K)."""
388
+
389
+ __slots__ = ("fmt", "M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
390
+
391
+ def __init__(self, fmt, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
392
+ self.fmt = fmt
393
+ self.M = M
394
+ self.N = N
395
+ self.K = K
396
+ self.a_buf = a_buf
397
+ self.b_buf = b_buf
398
+ self.c_buf = c_buf
399
+ self._dims_bytes = dims_bytes
400
+
401
+
402
+ def _quant_block_elems(fmt: str) -> int:
403
+ return _MAT_FORMATS[fmt][1]
404
+
405
+
406
+ def _quant_block_bytes(fmt: str) -> int:
407
+ return _MAT_FORMATS[fmt][0]
408
+
409
+
410
+ def _matvec_row_alignment(fmt: str) -> int | None:
411
+ return _MAT_FORMATS[fmt][2]
412
+
413
+
414
+ def allocate_matmul_plan(fmt: str, M: int, N: int, K: int) -> MatMulPlan:
415
+ import Metal
416
+
417
+ _ensure_metal()
418
+ assert fmt in _MAT_FORMATS, f"unsupported fmt {fmt}"
419
+ block_elems = _quant_block_elems(fmt)
420
+ block_bytes = _quant_block_bytes(fmt)
421
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
422
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
423
+ assert K % block_elems == 0, f"K must be multiple of {block_elems} for {fmt}, got {K}"
424
+
425
+ a_bytes = M * (K // block_elems) * block_bytes
426
+ b_bytes = N * K * 4
427
+ c_bytes = M * N * 4
428
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
429
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
430
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
431
+
432
+ nb01 = (K // block_elems) * block_bytes
433
+ nb11 = K * 4
434
+ dims = (
435
+ np.array([K], dtype=np.int64).tobytes()
436
+ + np.array([M], dtype=np.int64).tobytes()
437
+ + np.array([N], dtype=np.int64).tobytes()
438
+ + np.array([nb01], dtype=np.uint64).tobytes()
439
+ + np.array([nb11], dtype=np.uint64).tobytes()
440
+ )
441
+ return MatMulPlan(fmt=fmt, M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
442
+
443
+
444
+ def dispatch_matmul(plan: MatMulPlan, sync: bool = True):
445
+ import Metal
446
+
447
+ pso = _pipeline(f"mul_mat_{plan.fmt}_f32")
448
+ cmd = _COMMAND_QUEUE.commandBuffer()
449
+ enc = cmd.computeCommandEncoder()
450
+ enc.setComputePipelineState_(pso)
451
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
452
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
453
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
454
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
455
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
456
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
457
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
458
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
459
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
460
+
461
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
462
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
463
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
464
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
465
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
466
+ )
467
+ enc.endEncoding()
468
+ cmd.commit()
469
+ if sync:
470
+ cmd.waitUntilCompleted()
471
+ return cmd
472
+
473
+
474
+ class MatVecPlan:
475
+ __slots__ = ("fmt", "M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
476
+
477
+ def __init__(self, fmt, M, K, a_buf, b_buf, c_buf, dims_bytes):
478
+ self.fmt = fmt
479
+ self.M = M
480
+ self.K = K
481
+ self.a_buf = a_buf
482
+ self.b_buf = b_buf
483
+ self.c_buf = c_buf
484
+ self._dims_bytes = dims_bytes
485
+
486
+
487
+ def allocate_matvec_plan(fmt: str, M: int, K: int) -> MatVecPlan:
488
+ """Allocate I/O for `mul_mat_vec_<fmt>_f32`. Format-specific row alignment:
489
+ Q4_0/Q8_0: 8, Q4_K: 4, Q5_K: 4, Q6_K: 2."""
490
+ import Metal
491
+
492
+ _ensure_metal()
493
+ block_elems = _quant_block_elems(fmt)
494
+ block_bytes = _quant_block_bytes(fmt)
495
+ align = _matvec_row_alignment(fmt)
496
+ if align is None:
497
+ raise ValueError(f"mul_mat_vec not implemented for {fmt}")
498
+ assert M % align == 0, f"M must be a multiple of {align} for {fmt}, got {M}"
499
+ assert K % block_elems == 0, f"K must be a multiple of {block_elems} for {fmt}, got {K}"
500
+
501
+ a_bytes = M * (K // block_elems) * block_bytes
502
+ b_bytes = K * 4
503
+ c_bytes = M * 4
504
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
505
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
506
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
507
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
508
+ return MatVecPlan(fmt=fmt, M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
509
+
510
+
511
+ def encode_matvec(plan: MatVecPlan, enc):
512
+ """Encode a single matvec dispatch onto an existing MTLComputeCommandEncoder.
513
+ Caller is responsible for committing the parent command buffer. Threadgroup
514
+ memory for the IQ4 lookup table is set via setThreadgroupMemoryLength here."""
515
+ import Metal
516
+
517
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
518
+ enc.setComputePipelineState_(pso)
519
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
520
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
521
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
522
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
523
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
524
+
525
+ if plan.fmt == "q4_K":
526
+ rows_per_tg = _N_DST
527
+ tg_threads = _SIMD_WIDTH
528
+ elif plan.fmt in ("q4_0", "q8_0"):
529
+ rows_per_tg = _N_SIMDGROUP * _N_DST
530
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
531
+ elif plan.fmt == "q5_K":
532
+ rows_per_tg = _N_SIMDGROUP * 2
533
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
534
+ elif plan.fmt == "q6_K":
535
+ rows_per_tg = _N_SIMDGROUP * 1
536
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
537
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
538
+ rows_per_tg = _N_SIMDGROUP * 2
539
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
540
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0)
541
+ else:
542
+ raise ValueError(f"matvec not wired for {plan.fmt}")
543
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
544
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
545
+ Metal.MTLSizeMake(grid_x, 1, 1),
546
+ Metal.MTLSizeMake(tg_threads, 1, 1),
547
+ )
548
+
549
+
550
+ def new_command_buffer():
551
+ """Start a fresh MTLCommandBuffer. Caller batches `encode_matvec(plan, enc)`
552
+ onto its encoder, then calls `cb.commit()` and `cb.waitUntilCompleted()`
553
+ once for the whole batch — matching llama.cpp's per-step semantics."""
554
+ _ensure_metal()
555
+ return _COMMAND_QUEUE.commandBuffer()
556
+
557
+
558
+ def dispatch_matvec(plan: MatVecPlan, sync: bool = True):
559
+ import Metal
560
+
561
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
562
+ cmd = _COMMAND_QUEUE.commandBuffer()
563
+ enc = cmd.computeCommandEncoder()
564
+ enc.setComputePipelineState_(pso)
565
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
566
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
567
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
568
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
569
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
570
+
571
+ needs_shared = plan.fmt in ("iq4_nl", "iq4_xs")
572
+ if plan.fmt == "q4_K":
573
+ rows_per_tg = _N_DST
574
+ tg_threads = _SIMD_WIDTH
575
+ elif plan.fmt in ("q4_0", "q8_0"):
576
+ rows_per_tg = _N_SIMDGROUP * _N_DST
577
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
578
+ elif plan.fmt == "q5_K":
579
+ rows_per_tg = _N_SIMDGROUP * 2
580
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
581
+ elif plan.fmt == "q6_K":
582
+ rows_per_tg = _N_SIMDGROUP * 1
583
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
584
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
585
+ rows_per_tg = _N_SIMDGROUP * 2 # 2 sg × 2 rows/sg
586
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
587
+ else:
588
+ raise ValueError(f"matvec not wired for {plan.fmt}")
589
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
590
+ if needs_shared:
591
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0) # 64 floats per simdgroup × 2 sg
592
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
593
+ Metal.MTLSizeMake(grid_x, 1, 1),
594
+ Metal.MTLSizeMake(tg_threads, 1, 1),
595
+ )
596
+ enc.endEncoding()
597
+ cmd.commit()
598
+ if sync:
599
+ cmd.waitUntilCompleted()
600
+ return cmd
build/torch211-metal-aarch64-darwin/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (31.8 kB). View file
 
build/torch211-metal-aarch64-darwin/__pycache__/_ops.cpython-313.pyc ADDED
Binary file (543 Bytes). View file
 
build/torch211-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e502779f760fa35dac6a8aea0c4e63e6dbac0641cb3daf0ae34e4115754899d8
3
+ size 237040
build/torch211-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _gguf_dequant_530e374
3
+ ops = torch.ops._gguf_dequant_530e374
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_gguf_dequant_530e374::{op_name}"
build/torch211-metal-aarch64-darwin/gguf_dequant/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import sys
3
+
4
+ import importlib
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch211-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "python-depends": []
4
+ }
build/torch211-metal-aarch64-darwin/nn.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GgufLinear — drop-in nn.Linear replacement that runs quantized matmul directly.
2
+
3
+ Forward pass:
4
+ - If the input has effective batch size 1 (single token), use mul_mat_vec
5
+ (memory-bandwidth-bound, faster than bf16 mat-vec on MPS).
6
+ - Otherwise use mul_mat (compute-bound, similar throughput to bf16 GEMM but
7
+ keeps weights at native quantization → 3–4× less RAM than bf16).
8
+
9
+ Supported quant types today: Q4_0, Q8_0, Q4_K. Adding more is just a matter of
10
+ porting the candle kernels — the dispatch logic is generic over `quant_type`.
11
+
12
+ Prototype design: weights are stored as raw uint8 bytes in `qweight`. The
13
+ forward path copies activations into a shared MTLBuffer, runs the Metal
14
+ kernel, reads the fp32 output back. Production path (kernels-community +
15
+ PR #767) skips the copies via direct MTLBuffer handoff.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import numpy as np
22
+ import torch
23
+
24
+ from . import (
25
+ allocate_matmul_plan,
26
+ allocate_matvec_plan,
27
+ dispatch_matmul,
28
+ dispatch_matvec,
29
+ )
30
+
31
+
32
+ # Per-quant-type geometry: (block_bytes, block_elems, matvec_M_alignment)
33
+ _QUANT_INFO = {
34
+ "Q4_0": (18, 32, 8), # matvec uses 2 simdgroups × N_DST=4 = 8 rows/threadgroup
35
+ "Q8_0": (34, 32, 8),
36
+ "Q4_K": (144, 256, 4), # matvec uses 1 simdgroup × N_DST=4 = 4 rows/threadgroup
37
+ }
38
+
39
+ # Map our Python-side quant name to the Metal kernel suffix.
40
+ _KERNEL_FMT = {
41
+ "Q4_0": "q4_0",
42
+ "Q8_0": "q8_0",
43
+ "Q4_K": "q4_K",
44
+ }
45
+
46
+
47
+ class GgufLinear(torch.nn.Module):
48
+ """Linear layer backed by GGUF-quantized weights.
49
+
50
+ Args:
51
+ in_features: K dimension. Must be a multiple of the format's block size
52
+ (32 for Q4_0/Q8_0, 256 for Q4_K).
53
+ out_features: M dimension. Must be a multiple of the format's matvec
54
+ row alignment (8 for Q4_0/Q8_0, 4 for Q4_K) and of 64 for matmul.
55
+ quant_type: one of "Q4_0", "Q8_0", "Q4_K".
56
+ bias: whether to add a learnable bias after the matmul.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ in_features: int,
62
+ out_features: int,
63
+ quant_type: str = "Q4_K",
64
+ bias: bool = False,
65
+ device: Optional[torch.device | str] = None,
66
+ ):
67
+ super().__init__()
68
+ if quant_type not in _QUANT_INFO:
69
+ raise NotImplementedError(
70
+ f"GgufLinear supports {sorted(_QUANT_INFO)} today, got {quant_type}"
71
+ )
72
+ block_bytes, block_elems, matvec_align = _QUANT_INFO[quant_type]
73
+ if in_features % block_elems != 0:
74
+ raise ValueError(
75
+ f"in_features must be a multiple of {block_elems} for {quant_type}, got {in_features}"
76
+ )
77
+ if out_features % matvec_align != 0:
78
+ raise ValueError(
79
+ f"out_features must be a multiple of {matvec_align} for {quant_type}, got {out_features}"
80
+ )
81
+
82
+ self.in_features = in_features
83
+ self.out_features = out_features
84
+ self.quant_type = quant_type
85
+ self._fmt = _KERNEL_FMT[quant_type]
86
+ self._block_bytes = block_bytes
87
+ self._block_elems = block_elems
88
+
89
+ nblocks_per_row = in_features // block_elems
90
+ qweight_nbytes = out_features * nblocks_per_row * block_bytes
91
+ self.register_buffer(
92
+ "qweight",
93
+ torch.empty(qweight_nbytes, dtype=torch.uint8, device=device or "cpu"),
94
+ persistent=True,
95
+ )
96
+
97
+ if bias:
98
+ self.bias = torch.nn.Parameter(
99
+ torch.zeros(out_features, dtype=torch.float32, device=device or "cpu"),
100
+ requires_grad=False,
101
+ )
102
+ else:
103
+ self.register_parameter("bias", None)
104
+
105
+ def extra_repr(self) -> str:
106
+ return (
107
+ f"in_features={self.in_features}, out_features={self.out_features}, "
108
+ f"quant_type={self.quant_type}, bias={self.bias is not None}"
109
+ )
110
+
111
+ @torch.no_grad()
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ """x: shape (..., K). Returns (..., M) fp32."""
114
+ if x.shape[-1] != self.in_features:
115
+ raise ValueError(f"Last dim of x must be {self.in_features}, got {x.shape[-1]}")
116
+
117
+ batch_shape = x.shape[:-1]
118
+ x_flat = x.reshape(-1, self.in_features).contiguous().to(torch.float32)
119
+ N = x_flat.shape[0]
120
+
121
+ # CPU view of qweight for the bytes copy into shared MTLBuffers.
122
+ # Production path skips this once the kernels-community build is wired.
123
+ qweight_cpu = self.qweight.detach().to("cpu", copy=False)
124
+ x_cpu = x_flat.to("cpu", copy=False)
125
+
126
+ if N == 1:
127
+ out = self._forward_matvec(qweight_cpu, x_cpu.reshape(-1))
128
+ else:
129
+ out = self._forward_matmul(qweight_cpu, x_cpu)
130
+
131
+ out = out.reshape(*batch_shape, self.out_features)
132
+ if x.device != out.device:
133
+ out = out.to(x.device)
134
+ if self.bias is not None:
135
+ out = out + self.bias.to(out.device).to(out.dtype)
136
+ return out
137
+
138
+ def _forward_matvec(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
139
+ M, K = self.out_features, self.in_features
140
+ plan = allocate_matvec_plan(self._fmt, M=M, K=K)
141
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
142
+ a_view[:] = qweight_cpu.numpy()
143
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(K * 4), dtype=np.float32)
144
+ b_view[:] = x_cpu.numpy()
145
+ dispatch_matvec(plan, sync=True)
146
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * 4), dtype=np.float32)
147
+ return torch.from_numpy(c_view.copy())
148
+
149
+ def _forward_matmul(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
150
+ N, K = x_cpu.shape
151
+ M = self.out_features
152
+ plan = allocate_matmul_plan(self._fmt, M=M, N=N, K=K)
153
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
154
+ a_view[:] = qweight_cpu.numpy()
155
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(N * K * 4), dtype=np.float32)
156
+ b_view[:] = x_cpu.numpy().reshape(-1)
157
+ dispatch_matmul(plan, sync=True)
158
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * N * 4), dtype=np.float32)
159
+ # Kernel output layout is (N, M) in row-major; we want (N, M).
160
+ return torch.from_numpy(c_view.reshape(N, M).copy())
161
+
162
+ @classmethod
163
+ def from_quantized_bytes(
164
+ cls,
165
+ quant_type: str,
166
+ qweight_bytes: bytes,
167
+ in_features: int,
168
+ out_features: int,
169
+ bias: Optional[torch.Tensor] = None,
170
+ device: Optional[torch.device | str] = None,
171
+ ) -> "GgufLinear":
172
+ """Convenience for tests/loaders: hydrate from raw GGUF block bytes."""
173
+ layer = cls(in_features=in_features, out_features=out_features,
174
+ quant_type=quant_type, bias=bias is not None, device=device)
175
+ expected = layer.qweight.numel()
176
+ if len(qweight_bytes) != expected:
177
+ raise ValueError(
178
+ f"qweight_bytes length {len(qweight_bytes)} != expected {expected} "
179
+ f"for {quant_type} M={out_features} K={in_features}"
180
+ )
181
+ layer.qweight.copy_(torch.frombuffer(bytearray(qweight_bytes), dtype=torch.uint8))
182
+ if bias is not None:
183
+ layer.bias.copy_(bias.to(torch.float32))
184
+ return layer
185
+
186
+ # Back-compat shim for the existing Q4_K-only test
187
+ @classmethod
188
+ def from_q4_K_bytes(cls, qweight_bytes, in_features, out_features, bias=None, device=None):
189
+ return cls.from_quantized_bytes(
190
+ "Q4_K", qweight_bytes, in_features, out_features, bias=bias, device=device
191
+ )
build/torch211-metal-aarch64-darwin/transformers_glue.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Glue for plugging `GgufLinear` into a transformers model.
2
+
3
+ The flow is intentionally non-invasive: we read raw block bytes directly from
4
+ the GGUF file via `gguf.GGUFReader`, walk the model, and for every `nn.Linear`
5
+ whose corresponding GGUF tensor is one of our supported quant types we
6
+ in-place replace the module with `GgufLinear` and copy the bytes into
7
+ `qweight`. Tensors that don't map to a Linear (or are unsupported quants like
8
+ F32 norms, embeddings) get dequantized via `gguf.dequantize` and copied into
9
+ the existing parameter as fp32.
10
+
11
+ This avoids modifying the transformers loader. The trade-off is that the
12
+ model has to be constructed first with regular `nn.Linear` shells (which we
13
+ then mutate), but those shells are on the meta device or freshly initialised
14
+ and don't carry the dequantized weight at the time of swap — so peak memory
15
+ during load is bounded by `(model_size_fp32) + (model_size_quantized_qweights)`,
16
+ which fits in RAM for the small models we care about.
17
+
18
+ Typical use:
19
+
20
+ from transformers import AutoConfig, AutoModelForCausalLM
21
+ cfg = AutoConfig.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct-GGUF",
22
+ gguf_file="qwen2.5-0.5b-instruct-q4_0.gguf")
23
+ model = AutoModelForCausalLM.from_config(cfg).eval()
24
+ load_gguf_into_model_with_gguf_linear(
25
+ model,
26
+ gguf_path="…/qwen2.5-0.5b-instruct-q4_0.gguf",
27
+ )
28
+ out = model.generate(input_ids=…, max_new_tokens=10)
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+ import gguf
36
+ import numpy as np
37
+ import torch
38
+ import torch.nn as nn
39
+
40
+ from .nn import GgufLinear, _QUANT_INFO
41
+
42
+
43
+ # Map gguf.GGMLQuantizationType.name → our GgufLinear quant_type string.
44
+ _GGUF_NAME_TO_GGUF_LINEAR = {
45
+ "Q4_0": "Q4_0",
46
+ "Q8_0": "Q8_0",
47
+ "Q4_K": "Q4_K",
48
+ }
49
+
50
+
51
+ def _quant_supported_by_gguf_linear(qt: gguf.GGMLQuantizationType) -> bool:
52
+ return qt.name in _GGUF_NAME_TO_GGUF_LINEAR
53
+
54
+
55
+ # --- Tensor-name mapping ------------------------------------------------------
56
+ # GGUF tensors use llama.cpp-style names like `blk.0.attn_q.weight`. HF models
57
+ # use `model.layers.0.self_attn.q_proj.weight`. We use a tiny version of the
58
+ # transformers rename table here so the swap util can stay self-contained.
59
+ # (Transformers' own GGUF loader has a much more thorough version of this.)
60
+
61
+ _BASIC_RENAMES = [
62
+ (r"^blk\.", "model.layers."),
63
+ (r"\.attn_q\.weight$", ".self_attn.q_proj.weight"),
64
+ (r"\.attn_k\.weight$", ".self_attn.k_proj.weight"),
65
+ (r"\.attn_v\.weight$", ".self_attn.v_proj.weight"),
66
+ (r"\.attn_output\.weight$", ".self_attn.o_proj.weight"),
67
+ (r"\.ffn_gate\.weight$", ".mlp.gate_proj.weight"),
68
+ (r"\.ffn_up\.weight$", ".mlp.up_proj.weight"),
69
+ (r"\.ffn_down\.weight$", ".mlp.down_proj.weight"),
70
+ (r"\.attn_norm\.weight$", ".input_layernorm.weight"),
71
+ (r"\.ffn_norm\.weight$", ".post_attention_layernorm.weight"),
72
+ (r"\.attn_(q|k|v)\.bias$", r".self_attn.\1_proj.bias"),
73
+ (r"^token_embd\.weight$", "model.embed_tokens.weight"),
74
+ (r"^output_norm\.weight$", "model.norm.weight"),
75
+ (r"^output\.weight$", "lm_head.weight"),
76
+ ]
77
+
78
+
79
+ def _gguf_name_to_hf_name(gguf_name: str) -> str:
80
+ """Apply our minimal rename table to map a GGUF tensor name to an HF parameter name."""
81
+ import re
82
+
83
+ out = gguf_name
84
+ for pat, repl in _BASIC_RENAMES:
85
+ out = re.sub(pat, repl, out)
86
+ return out
87
+
88
+
89
+ def _is_attn_qk(gguf_name: str) -> bool:
90
+ return ".attn_q.weight" in gguf_name or ".attn_k.weight" in gguf_name
91
+
92
+
93
+ def _reverse_permute_qk(w: np.ndarray, n_head: int, n_kv: int | None) -> np.ndarray:
94
+ """GGUF Q/K weights are permuted on quantize; undo before loading into HF.
95
+ Reference: https://github.com/ggerganov/llama.cpp/blob/.../convert_hf_to_gguf.py."""
96
+ if n_kv is not None and n_head != n_kv:
97
+ n_head = n_kv
98
+ dim = w.shape[0] // n_head // 2
99
+ w = w.reshape(n_head, dim, 2, *w.shape[1:])
100
+ return w.swapaxes(2, 1).reshape(*w.shape[:1], *w.shape[3:]) if False else w.reshape(-1, *w.shape[3:]).swapaxes(2, 1).reshape(n_head * dim * 2, *w.shape[3:])
101
+
102
+
103
+ def _set_module_param(model: nn.Module, qualified_name: str, value: torch.Tensor) -> bool:
104
+ """Set a parameter or buffer on `model` by dotted name. Returns True on success."""
105
+ parts = qualified_name.split(".")
106
+ mod = model
107
+ for p in parts[:-1]:
108
+ if not hasattr(mod, p):
109
+ return False
110
+ mod = getattr(mod, p)
111
+ leaf = parts[-1]
112
+ if leaf in mod._parameters:
113
+ param = mod._parameters[leaf]
114
+ if param is None:
115
+ return False
116
+ with torch.no_grad():
117
+ mod._parameters[leaf] = nn.Parameter(value.to(dtype=param.dtype, device=param.device),
118
+ requires_grad=param.requires_grad)
119
+ return True
120
+ if leaf in mod._buffers:
121
+ with torch.no_grad():
122
+ mod._buffers[leaf] = value
123
+ return True
124
+ return False
125
+
126
+
127
+ def _get_module_by_name(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
128
+ """Return the leaf module matching `qualified_name` minus its terminal attribute,
129
+ or None if anything along the path is missing."""
130
+ parts = qualified_name.split(".")
131
+ mod = model
132
+ for p in parts[:-1]:
133
+ if not hasattr(mod, p):
134
+ return None
135
+ mod = getattr(mod, p)
136
+ return mod
137
+
138
+
139
+ def _get_attr_by_path(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
140
+ """Walk dotted `qualified_name` on `model` and return the final attribute,
141
+ or None if anything along the path is missing."""
142
+ if not qualified_name:
143
+ return model
144
+ mod = model
145
+ for p in qualified_name.split("."):
146
+ if not hasattr(mod, p):
147
+ return None
148
+ mod = getattr(mod, p)
149
+ return mod
150
+
151
+
152
+ def _swap_linear_to_gguf(
153
+ parent: nn.Module, attr: str, in_features: int, out_features: int, quant_type: str,
154
+ bias_tensor: Optional[torch.Tensor], device: Optional[torch.device | str] = None
155
+ ) -> GgufLinear:
156
+ """Replace `parent.attr` (an nn.Linear) with a new GgufLinear of the same shape."""
157
+ new = GgufLinear(in_features=in_features, out_features=out_features,
158
+ quant_type=quant_type, bias=bias_tensor is not None, device=device)
159
+ if bias_tensor is not None and new.bias is not None:
160
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
161
+ setattr(parent, attr, new)
162
+ return new
163
+
164
+
165
+ def load_gguf_into_model_with_gguf_linear(
166
+ model: nn.Module,
167
+ gguf_path: str | Path,
168
+ skip_modules: Optional[set[str]] = None,
169
+ force_dequant: bool = False,
170
+ ) -> dict:
171
+ """Read a GGUF file and populate `model` in-place.
172
+
173
+ Two-phase load to keep the byte path simple and the bias path correct:
174
+
175
+ Phase 1 — full dequant: every tensor (linear weights, norms, embeddings,
176
+ biases) is dequantized to fp32 and written into the matching HF
177
+ parameter. After this pass the model would already produce correct
178
+ outputs (this is what `force_dequant=True` stops at).
179
+ Phase 2 — swap linears: walk every `nn.Linear` that maps to a tensor
180
+ whose GGUF quant type is supported by `GgufLinear`; swap it for a
181
+ `GgufLinear` and re-attach raw block bytes (preserving the bias that
182
+ Phase 1 just loaded).
183
+
184
+ Returns a dict with diagnostic counts.
185
+ """
186
+ reader = gguf.GGUFReader(str(gguf_path))
187
+
188
+ # Pull a few config bits for the Q/K reverse-permute.
189
+ def _read_int(field: str, default: int = 0) -> int:
190
+ f = reader.fields.get(field)
191
+ if f is None:
192
+ return default
193
+ return int(f.parts[f.data[0]][0]) if f.data else default
194
+
195
+ arch = ""
196
+ arch_field = reader.fields.get("general.architecture")
197
+ if arch_field is not None and arch_field.data:
198
+ arch = bytes(arch_field.parts[arch_field.data[0]]).decode("utf-8", errors="ignore")
199
+ n_head = _read_int(f"{arch}.attention.head_count")
200
+ n_kv_head = _read_int(f"{arch}.attention.head_count_kv") or n_head
201
+
202
+ skip_modules = skip_modules or set()
203
+ stats = {"as_gguf_linear": 0, "dequantized": 0, "skipped": 0, "unmatched": []}
204
+
205
+ # Track which (hf_name → quant_type) pairs are eligible for GgufLinear swap
206
+ # after Phase 1. We'll re-quantize the loaded fp32 weight in Phase 2.
207
+ swap_candidates: list[tuple[str, str]] = []
208
+
209
+ # ---------------- Phase 1: dequant everything into the model. -------------
210
+ for t in reader.tensors:
211
+ hf_name = _gguf_name_to_hf_name(t.name)
212
+ if any(skip in hf_name for skip in skip_modules):
213
+ stats["skipped"] += 1
214
+ continue
215
+
216
+ parent = _get_module_by_name(model, hf_name)
217
+ if parent is None or not hasattr(parent, hf_name.rsplit(".", 1)[-1]):
218
+ stats["unmatched"].append(t.name)
219
+ continue
220
+ leaf = hf_name.rsplit(".", 1)[-1]
221
+ cur = getattr(parent, leaf)
222
+
223
+ fp32 = gguf.dequantize(
224
+ np.frombuffer(np.ascontiguousarray(t.data).tobytes(), dtype=np.uint8),
225
+ t.tensor_type,
226
+ ).astype(np.float32)
227
+ if _is_attn_qk(t.name) and n_head > 0:
228
+ shape = list(t.shape)
229
+ M, K = int(shape[1]), int(shape[0])
230
+ heads = n_head if ".attn_q." in t.name else n_kv_head
231
+ head_dim = M // heads
232
+ fp32 = fp32.reshape(M, K).reshape(heads, head_dim // 2, 2, K).swapaxes(2, 1).reshape(M, K)
233
+ target_shape = tuple(cur.shape) if hasattr(cur, "shape") else (fp32.size,)
234
+ if np.prod(target_shape) == fp32.size:
235
+ fp32 = fp32.reshape(target_shape)
236
+ tensor = torch.from_numpy(fp32)
237
+ if not _set_module_param(model, hf_name, tensor):
238
+ stats["unmatched"].append(t.name)
239
+ continue
240
+ stats["dequantized"] += 1
241
+
242
+ # Record candidacy for swap in Phase 2.
243
+ is_linear_weight = isinstance(parent, nn.Linear) and leaf == "weight"
244
+ if is_linear_weight and _quant_supported_by_gguf_linear(t.tensor_type) and not force_dequant:
245
+ swap_candidates.append((hf_name, _GGUF_NAME_TO_GGUF_LINEAR[t.tensor_type.name]))
246
+
247
+ # ---------------- Phase 2: swap Linear → GgufLinear. ----------------------
248
+ # Re-quantize the loaded fp32 weight via gguf-py (works for Q4_0/Q8_0;
249
+ # K-quants don't have a Python quantizer yet — those will skip swap).
250
+ for hf_name, quant_type in swap_candidates:
251
+ # hf_name ends in `.weight`; the Linear module is at hf_name[:-len(".weight")]
252
+ linear_path = hf_name[: -len(".weight")]
253
+ grandparent_path, _, linear_attr = linear_path.rpartition(".")
254
+ grandparent = model if not grandparent_path else _get_attr_by_path(model, grandparent_path)
255
+ linear = _get_attr_by_path(model, linear_path)
256
+ if grandparent is None or not isinstance(linear, nn.Linear):
257
+ continue
258
+
259
+ # Re-quantize the (now permuted-if-applicable) fp32 weight.
260
+ try:
261
+ qt = getattr(gguf.GGMLQuantizationType, quant_type)
262
+ qbytes = bytes(
263
+ gguf.quantize(linear.weight.detach().cpu().numpy().astype(np.float32), qt).tobytes()
264
+ )
265
+ except (NotImplementedError, Exception) as e:
266
+ # K-quants etc. — leave as Linear (fp32 weight is already loaded).
267
+ stats.setdefault("re_quantize_failed", []).append((hf_name, str(e)))
268
+ continue
269
+
270
+ bias_tensor = linear.bias.detach().clone() if linear.bias is not None else None
271
+ new = GgufLinear(
272
+ in_features=linear.in_features,
273
+ out_features=linear.out_features,
274
+ quant_type=quant_type,
275
+ bias=bias_tensor is not None,
276
+ )
277
+ if bias_tensor is not None:
278
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
279
+ new.qweight.copy_(torch.frombuffer(bytearray(qbytes), dtype=torch.uint8))
280
+ setattr(grandparent, linear_attr, new)
281
+ stats["as_gguf_linear"] += 1
282
+ stats["dequantized"] -= 1 # this one's no longer "just" dequant — swapped
283
+
284
+ return stats
build/torch28-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GGUF dequantization kernels for Apple Silicon (Metal).
2
+
3
+ Six standalone kernels covering the GGUF formats most common in the wild:
4
+
5
+ Q4_0 Q8_0 Q4_K Q5_K Q6_K IQ4_NL IQ4_XS
6
+
7
+ Two API levels are exposed for each:
8
+
9
+ - ``dequantize_<format>_bytes(quant_bytes) -> np.ndarray``
10
+ One-shot path used by the tests for bit-exact validation against a
11
+ NumPy reference. Allocates input/output buffers internally.
12
+
13
+ - ``allocate_<format>_plan(nblocks)`` + ``dispatch_<format>(plan)``
14
+ Low-level path for benchmarks. Buffers are pre-allocated; ``dispatch``
15
+ only encodes + dispatches + syncs, so the timing isolates GPU work
16
+ from CPU<->buffer copies.
17
+
18
+ The runtime path uses PyObjC + Metal.framework so the correctness loop
19
+ works on any Apple Silicon Mac without Xcode 26 / Nix / kernel-builder.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ # Lazy globals — populated on first use.
29
+ _DEVICE = None
30
+ _COMMAND_QUEUE = None
31
+ _LIBRARY = None
32
+ _PIPELINES: dict = {}
33
+
34
+ _KERNEL_SOURCE_PATH = Path(__file__).resolve().parent.parent.parent / "gguf_dequant_metal" / "dequantize.metal"
35
+
36
+
37
+ def _ensure_metal():
38
+ global _DEVICE, _COMMAND_QUEUE, _LIBRARY
39
+ if _DEVICE is not None:
40
+ return
41
+ import Metal # pyobjc-framework-metal
42
+
43
+ _DEVICE = Metal.MTLCreateSystemDefaultDevice()
44
+ if _DEVICE is None:
45
+ raise RuntimeError("No default Metal device available")
46
+ _COMMAND_QUEUE = _DEVICE.newCommandQueue()
47
+
48
+ src = _KERNEL_SOURCE_PATH.read_text()
49
+ options = Metal.MTLCompileOptions.new()
50
+ lib, err = _DEVICE.newLibraryWithSource_options_error_(src, options, None)
51
+ if lib is None:
52
+ raise RuntimeError(f"Metal compile failed: {err}")
53
+ _LIBRARY = lib
54
+
55
+
56
+ def _pipeline(fn_name: str):
57
+ _ensure_metal()
58
+ pso = _PIPELINES.get(fn_name)
59
+ if pso is not None:
60
+ return pso
61
+
62
+ fn = _LIBRARY.newFunctionWithName_(fn_name)
63
+ if fn is None:
64
+ raise RuntimeError(f"Kernel function not found: {fn_name}")
65
+ pso, err = _DEVICE.newComputePipelineStateWithFunction_error_(fn, None)
66
+ if pso is None:
67
+ raise RuntimeError(f"Pipeline state creation failed: {err}")
68
+ _PIPELINES[fn_name] = pso
69
+ return pso
70
+
71
+
72
+ # (kernel_name, bytes_per_block, elems_per_block, threads_per_block_group)
73
+ _FORMATS = {
74
+ "q4_0": ("dequantize_q4_0", 18, 32, 16),
75
+ "q8_0": ("dequantize_q8_0", 34, 32, 32),
76
+ "q4_K": ("dequantize_q4_K", 144, 256, 256),
77
+ "q5_K": ("dequantize_q5_K", 176, 256, 256),
78
+ "q6_K": ("dequantize_q6_K", 210, 256, 256),
79
+ "iq4_nl": ("dequantize_iq4_nl", 18, 32, 16),
80
+ "iq4_xs": ("dequantize_iq4_xs", 136, 256, 256),
81
+ }
82
+
83
+
84
+ class Plan:
85
+ """Pre-allocated I/O buffers for a single (format, nblocks) shape."""
86
+
87
+ __slots__ = ("fmt", "nblocks", "in_buf", "out_buf", "_nblocks_bytes")
88
+
89
+ def __init__(self, fmt: str, nblocks: int, in_buf, out_buf):
90
+ self.fmt = fmt
91
+ self.nblocks = nblocks
92
+ self.in_buf = in_buf
93
+ self.out_buf = out_buf
94
+ self._nblocks_bytes = np.array([nblocks], dtype=np.uint32).tobytes()
95
+
96
+
97
+ def allocate_plan(fmt: str, nblocks: int) -> Plan:
98
+ import Metal
99
+
100
+ _ensure_metal()
101
+ _, bpb, epb, _ = _FORMATS[fmt]
102
+ in_buf = _DEVICE.newBufferWithLength_options_(nblocks * bpb, Metal.MTLResourceStorageModeShared)
103
+ out_buf = _DEVICE.newBufferWithLength_options_(nblocks * epb * 4, Metal.MTLResourceStorageModeShared)
104
+ return Plan(fmt=fmt, nblocks=nblocks, in_buf=in_buf, out_buf=out_buf)
105
+
106
+
107
+ def dispatch(plan: Plan, sync: bool = True):
108
+ import Metal
109
+
110
+ kernel_name, _, _, tg = _FORMATS[plan.fmt]
111
+ pso = _pipeline(kernel_name)
112
+ cmd = _COMMAND_QUEUE.commandBuffer()
113
+ enc = cmd.computeCommandEncoder()
114
+ enc.setComputePipelineState_(pso)
115
+ enc.setBuffer_offset_atIndex_(plan.in_buf, 0, 0)
116
+ enc.setBuffer_offset_atIndex_(plan.out_buf, 0, 1)
117
+ enc.setBytes_length_atIndex_(plan._nblocks_bytes, len(plan._nblocks_bytes), 2)
118
+ enc.dispatchThreads_threadsPerThreadgroup_(
119
+ Metal.MTLSizeMake(plan.nblocks * tg, 1, 1),
120
+ Metal.MTLSizeMake(tg, 1, 1),
121
+ )
122
+ enc.endEncoding()
123
+ cmd.commit()
124
+ if sync:
125
+ cmd.waitUntilCompleted()
126
+ return cmd
127
+
128
+
129
+ def dequantize_bytes(fmt: str, quant_bytes) -> np.ndarray:
130
+ """One-shot bytes-in / float32-out for the named format."""
131
+ _ensure_metal()
132
+ _, bpb, epb, _ = _FORMATS[fmt]
133
+
134
+ buf = bytes(quant_bytes) if not isinstance(quant_bytes, (bytes, bytearray)) else quant_bytes
135
+ nbytes = len(buf)
136
+ if nbytes % bpb != 0:
137
+ raise ValueError(f"{fmt} input not a multiple of {bpb} bytes: got {nbytes}")
138
+ nblocks = nbytes // bpb
139
+
140
+ plan = allocate_plan(fmt, nblocks)
141
+ in_view = np.frombuffer(plan.in_buf.contents().as_buffer(nbytes), dtype=np.uint8)
142
+ in_view[:] = np.frombuffer(buf, dtype=np.uint8)
143
+ dispatch(plan, sync=True)
144
+ out_nbytes = nblocks * epb * 4
145
+ return np.frombuffer(plan.out_buf.contents().as_buffer(out_nbytes), dtype=np.float32).copy()
146
+
147
+
148
+ # --- Per-format convenience wrappers ----------------------------------------
149
+
150
+ def dequantize_q4_0_bytes(qb): return dequantize_bytes("q4_0", qb)
151
+ def dequantize_q8_0_bytes(qb): return dequantize_bytes("q8_0", qb)
152
+ def dequantize_q4_K_bytes(qb): return dequantize_bytes("q4_K", qb)
153
+ def dequantize_q5_K_bytes(qb): return dequantize_bytes("q5_K", qb)
154
+ def dequantize_q6_K_bytes(qb): return dequantize_bytes("q6_K", qb)
155
+ def dequantize_iq4_nl_bytes(qb): return dequantize_bytes("iq4_nl", qb)
156
+ def dequantize_iq4_xs_bytes(qb): return dequantize_bytes("iq4_xs", qb)
157
+
158
+ def allocate_q4_0_plan(nblocks): return allocate_plan("q4_0", nblocks)
159
+ def allocate_q8_0_plan(nblocks): return allocate_plan("q8_0", nblocks)
160
+ def allocate_q4_K_plan(nblocks): return allocate_plan("q4_K", nblocks)
161
+ def allocate_q5_K_plan(nblocks): return allocate_plan("q5_K", nblocks)
162
+ def allocate_q6_K_plan(nblocks): return allocate_plan("q6_K", nblocks)
163
+ def allocate_iq4_nl_plan(nblocks): return allocate_plan("iq4_nl", nblocks)
164
+ def allocate_iq4_xs_plan(nblocks): return allocate_plan("iq4_xs", nblocks)
165
+
166
+ def dispatch_q4_0(plan, **kw): return dispatch(plan, **kw)
167
+ def dispatch_q8_0(plan, **kw): return dispatch(plan, **kw)
168
+ def dispatch_q4_K(plan, **kw): return dispatch(plan, **kw)
169
+ def dispatch_q5_K(plan, **kw): return dispatch(plan, **kw)
170
+ def dispatch_q6_K(plan, **kw): return dispatch(plan, **kw)
171
+ def dispatch_iq4_nl(plan, **kw): return dispatch(plan, **kw)
172
+ def dispatch_iq4_xs(plan, **kw): return dispatch(plan, **kw)
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Torch op entry points — the production path used by transformers (no PyObjC,
177
+ # no runtime compile). Only available in builds produced by kernel-builder,
178
+ # which generates `_ops.py` exporting `ops = torch.ops._gguf_dequant_<rev>`.
179
+ # In a dev-mode checkout (running from torch-ext/ directly) `_ops` isn't
180
+ # present, so the torch_* entry points raise on call.
181
+ # ---------------------------------------------------------------------------
182
+
183
+ try:
184
+ from ._ops import ops as _ops # generated by kernel-builder
185
+ except ImportError:
186
+ _ops = None
187
+
188
+
189
+ def _require_ops():
190
+ if _ops is None:
191
+ raise RuntimeError(
192
+ "torch op binding not available — this build was loaded from the dev tree. "
193
+ "Use the *_bytes / *_plan APIs (PyObjC path) or install a kernel-builder build."
194
+ )
195
+ return _ops
196
+
197
+
198
+ def torch_dequantize_q4_0(input, output): _require_ops().dequantize_q4_0(input, output)
199
+ def torch_dequantize_q8_0(input, output): _require_ops().dequantize_q8_0(input, output)
200
+ def torch_dequantize_q4_K(input, output): _require_ops().dequantize_q4_K(input, output)
201
+ def torch_dequantize_q5_K(input, output): _require_ops().dequantize_q5_K(input, output)
202
+ def torch_dequantize_q6_K(input, output): _require_ops().dequantize_q6_K(input, output)
203
+ def torch_dequantize_iq4_nl(input, output): _require_ops().dequantize_iq4_nl(input, output)
204
+ def torch_dequantize_iq4_xs(input, output): _require_ops().dequantize_iq4_xs(input, output)
205
+
206
+
207
+ # =============================================================================
208
+ # Q4_K × fp32 matmul — `GgufLinear` ceiling experiment.
209
+ # =============================================================================
210
+ # Layout convention (matches llama.cpp's kernel_mul_mm):
211
+ # A (Q4_K weights): shape (M, K) M = out_features, K = in_features.
212
+ # M and K must be multiples of 64 and 256 respectively for
213
+ # the simdgroup tile geometry to land cleanly.
214
+ # B (fp32 input): shape (N, K) N = batch * seqlen, contiguous in K.
215
+ # C (fp32 output): shape (M, N) column-major-ish (matches llama.cpp).
216
+
217
+ _BLOCK_SIZE_M = 64
218
+ _BLOCK_SIZE_N = 32
219
+ _THREAD_PER_BLOCK = 128
220
+ _SHARED_BYTES = 8192
221
+
222
+
223
+ class MatMulQ4KPlan:
224
+ __slots__ = ("M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
225
+
226
+ def __init__(self, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
227
+ self.M = M
228
+ self.N = N
229
+ self.K = K
230
+ self.a_buf = a_buf
231
+ self.b_buf = b_buf
232
+ self.c_buf = c_buf
233
+ self._dims_bytes = dims_bytes
234
+
235
+
236
+ def allocate_matmul_q4_K_plan(M: int, N: int, K: int) -> MatMulQ4KPlan:
237
+ """Pre-allocate weight (Q4_K), input (fp32), output (fp32) MTLBuffers."""
238
+ import Metal
239
+
240
+ _ensure_metal()
241
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
242
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
243
+ assert K % 256 == 0, f"K must be multiple of 256, got {K}"
244
+
245
+ a_bytes = M * (K // 256) * 144 # M rows × (K/QK_K) super-blocks × 144 bytes
246
+ b_bytes = N * K * 4 # fp32
247
+ c_bytes = M * N * 4 # fp32
248
+
249
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
250
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
251
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
252
+
253
+ # Pack ne00, ne01, ne11, nb01, nb11 into a uniforms buffer.
254
+ nb01 = (K // 256) * 144 # bytes per A row
255
+ nb11 = K * 4 # bytes per B row
256
+ dims = np.array([K], dtype=np.int64).tobytes() # ne00
257
+ dims += np.array([M], dtype=np.int64).tobytes() # ne01
258
+ dims += np.array([N], dtype=np.int64).tobytes() # ne11
259
+ dims += np.array([nb01], dtype=np.uint64).tobytes()
260
+ dims += np.array([nb11], dtype=np.uint64).tobytes()
261
+
262
+ return MatMulQ4KPlan(M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
263
+
264
+
265
+ def dispatch_matmul_q4_K(plan: MatMulQ4KPlan, sync: bool = True):
266
+ """Dispatch mul_mat_q4_K_f32 on the pre-allocated buffers."""
267
+ import Metal
268
+
269
+ pso = _pipeline("mul_mat_q4_K_f32")
270
+ cmd = _COMMAND_QUEUE.commandBuffer()
271
+ enc = cmd.computeCommandEncoder()
272
+ enc.setComputePipelineState_(pso)
273
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
274
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
275
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
276
+ # ne00 (K), ne01 (M), ne11 (N), nb01, nb11 via setBytes — 5 scalars
277
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
278
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
279
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
280
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
281
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
282
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
283
+
284
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
285
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
286
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
287
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
288
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
289
+ )
290
+ enc.endEncoding()
291
+ cmd.commit()
292
+ if sync:
293
+ cmd.waitUntilCompleted()
294
+ return cmd
295
+
296
+
297
+ # =============================================================================
298
+ # mul_mat_vec_q4_K — batch-1 decode (memory-bandwidth-bound regime).
299
+ # =============================================================================
300
+
301
+ _N_DST = 4
302
+ _SIMD_WIDTH = 32
303
+
304
+
305
+ class MatVecQ4KPlan:
306
+ __slots__ = ("M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
307
+
308
+ def __init__(self, M, K, a_buf, b_buf, c_buf, dims_bytes):
309
+ self.M = M
310
+ self.K = K
311
+ self.a_buf = a_buf
312
+ self.b_buf = b_buf
313
+ self.c_buf = c_buf
314
+ self._dims_bytes = dims_bytes
315
+
316
+
317
+ def allocate_matvec_q4_K_plan(M: int, K: int) -> MatVecQ4KPlan:
318
+ import Metal
319
+
320
+ _ensure_metal()
321
+ assert M % _N_DST == 0, f"M must be a multiple of {_N_DST}, got {M}"
322
+ assert K % 256 == 0, f"K must be a multiple of 256, got {K}"
323
+
324
+ a_bytes = M * (K // 256) * 144
325
+ b_bytes = K * 4
326
+ c_bytes = M * 4
327
+
328
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
329
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
330
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
331
+
332
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
333
+ return MatVecQ4KPlan(M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
334
+
335
+
336
+ def dispatch_matvec_q4_K(plan: MatVecQ4KPlan, sync: bool = True):
337
+ import Metal
338
+
339
+ pso = _pipeline("mul_mat_vec_q4_K_f32")
340
+ cmd = _COMMAND_QUEUE.commandBuffer()
341
+ enc = cmd.computeCommandEncoder()
342
+ enc.setComputePipelineState_(pso)
343
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
344
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
345
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
346
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3) # ne00 = K
347
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4) # ne01 = M
348
+
349
+ grid_x = (plan.M + _N_DST - 1) // _N_DST
350
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
351
+ Metal.MTLSizeMake(grid_x, 1, 1),
352
+ Metal.MTLSizeMake(_SIMD_WIDTH, 1, 1),
353
+ )
354
+ enc.endEncoding()
355
+ cmd.commit()
356
+ if sync:
357
+ cmd.waitUntilCompleted()
358
+ return cmd
359
+
360
+
361
+ # =============================================================================
362
+ # Q4_0 / Q8_0 matmul + matvec — same layout as Q4_K, smaller blocks (32 elems).
363
+ # =============================================================================
364
+
365
+ _N_SIMDGROUP = 2 # Q4_0/Q8_0 matvec uses 2 simdgroups per threadgroup
366
+
367
+ # Per quant type for the mat path: (block bytes, block elems, matvec_M_alignment).
368
+ # block_elems = 32 for Q4_0/Q8_0/IQ4_NL, 256 for all K-quants and IQ4_XS.
369
+ # matvec_M_alignment:
370
+ # Q4_0/Q8_0 → 2 simdgroups × N_DST(4) = 8 rows per threadgroup
371
+ # Q4_K → 1 simdgroup × N_DST(4) = 4 rows per threadgroup
372
+ # Q5_K → 2 simdgroups × 2 rows = 4 rows per threadgroup
373
+ # Q6_K → 2 simdgroups × 1 row = 2 rows per threadgroup
374
+ _MAT_FORMATS = {
375
+ "q4_0": (18, 32, 8),
376
+ "q8_0": (34, 32, 8),
377
+ "q4_K": (144, 256, 4),
378
+ "q5_K": (176, 256, 4),
379
+ "q6_K": (210, 256, 2),
380
+ "iq4_nl": (18, 32, 4), # 2 sg × 2 rows/sg = 4 rows/threadgroup
381
+ "iq4_xs": (136, 256, 4),
382
+ }
383
+
384
+
385
+ class MatMulPlan:
386
+ """Pre-allocated I/O buffers for `mul_mat_<fmt>_f32`. Block element size
387
+ depends on the format (32 for Q4_0/Q8_0, 256 for Q4_K)."""
388
+
389
+ __slots__ = ("fmt", "M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
390
+
391
+ def __init__(self, fmt, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
392
+ self.fmt = fmt
393
+ self.M = M
394
+ self.N = N
395
+ self.K = K
396
+ self.a_buf = a_buf
397
+ self.b_buf = b_buf
398
+ self.c_buf = c_buf
399
+ self._dims_bytes = dims_bytes
400
+
401
+
402
+ def _quant_block_elems(fmt: str) -> int:
403
+ return _MAT_FORMATS[fmt][1]
404
+
405
+
406
+ def _quant_block_bytes(fmt: str) -> int:
407
+ return _MAT_FORMATS[fmt][0]
408
+
409
+
410
+ def _matvec_row_alignment(fmt: str) -> int | None:
411
+ return _MAT_FORMATS[fmt][2]
412
+
413
+
414
+ def allocate_matmul_plan(fmt: str, M: int, N: int, K: int) -> MatMulPlan:
415
+ import Metal
416
+
417
+ _ensure_metal()
418
+ assert fmt in _MAT_FORMATS, f"unsupported fmt {fmt}"
419
+ block_elems = _quant_block_elems(fmt)
420
+ block_bytes = _quant_block_bytes(fmt)
421
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
422
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
423
+ assert K % block_elems == 0, f"K must be multiple of {block_elems} for {fmt}, got {K}"
424
+
425
+ a_bytes = M * (K // block_elems) * block_bytes
426
+ b_bytes = N * K * 4
427
+ c_bytes = M * N * 4
428
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
429
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
430
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
431
+
432
+ nb01 = (K // block_elems) * block_bytes
433
+ nb11 = K * 4
434
+ dims = (
435
+ np.array([K], dtype=np.int64).tobytes()
436
+ + np.array([M], dtype=np.int64).tobytes()
437
+ + np.array([N], dtype=np.int64).tobytes()
438
+ + np.array([nb01], dtype=np.uint64).tobytes()
439
+ + np.array([nb11], dtype=np.uint64).tobytes()
440
+ )
441
+ return MatMulPlan(fmt=fmt, M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
442
+
443
+
444
+ def dispatch_matmul(plan: MatMulPlan, sync: bool = True):
445
+ import Metal
446
+
447
+ pso = _pipeline(f"mul_mat_{plan.fmt}_f32")
448
+ cmd = _COMMAND_QUEUE.commandBuffer()
449
+ enc = cmd.computeCommandEncoder()
450
+ enc.setComputePipelineState_(pso)
451
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
452
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
453
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
454
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
455
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
456
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
457
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
458
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
459
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
460
+
461
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
462
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
463
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
464
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
465
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
466
+ )
467
+ enc.endEncoding()
468
+ cmd.commit()
469
+ if sync:
470
+ cmd.waitUntilCompleted()
471
+ return cmd
472
+
473
+
474
+ class MatVecPlan:
475
+ __slots__ = ("fmt", "M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
476
+
477
+ def __init__(self, fmt, M, K, a_buf, b_buf, c_buf, dims_bytes):
478
+ self.fmt = fmt
479
+ self.M = M
480
+ self.K = K
481
+ self.a_buf = a_buf
482
+ self.b_buf = b_buf
483
+ self.c_buf = c_buf
484
+ self._dims_bytes = dims_bytes
485
+
486
+
487
+ def allocate_matvec_plan(fmt: str, M: int, K: int) -> MatVecPlan:
488
+ """Allocate I/O for `mul_mat_vec_<fmt>_f32`. Format-specific row alignment:
489
+ Q4_0/Q8_0: 8, Q4_K: 4, Q5_K: 4, Q6_K: 2."""
490
+ import Metal
491
+
492
+ _ensure_metal()
493
+ block_elems = _quant_block_elems(fmt)
494
+ block_bytes = _quant_block_bytes(fmt)
495
+ align = _matvec_row_alignment(fmt)
496
+ if align is None:
497
+ raise ValueError(f"mul_mat_vec not implemented for {fmt}")
498
+ assert M % align == 0, f"M must be a multiple of {align} for {fmt}, got {M}"
499
+ assert K % block_elems == 0, f"K must be a multiple of {block_elems} for {fmt}, got {K}"
500
+
501
+ a_bytes = M * (K // block_elems) * block_bytes
502
+ b_bytes = K * 4
503
+ c_bytes = M * 4
504
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
505
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
506
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
507
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
508
+ return MatVecPlan(fmt=fmt, M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
509
+
510
+
511
+ def encode_matvec(plan: MatVecPlan, enc):
512
+ """Encode a single matvec dispatch onto an existing MTLComputeCommandEncoder.
513
+ Caller is responsible for committing the parent command buffer. Threadgroup
514
+ memory for the IQ4 lookup table is set via setThreadgroupMemoryLength here."""
515
+ import Metal
516
+
517
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
518
+ enc.setComputePipelineState_(pso)
519
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
520
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
521
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
522
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
523
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
524
+
525
+ if plan.fmt == "q4_K":
526
+ rows_per_tg = _N_DST
527
+ tg_threads = _SIMD_WIDTH
528
+ elif plan.fmt in ("q4_0", "q8_0"):
529
+ rows_per_tg = _N_SIMDGROUP * _N_DST
530
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
531
+ elif plan.fmt == "q5_K":
532
+ rows_per_tg = _N_SIMDGROUP * 2
533
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
534
+ elif plan.fmt == "q6_K":
535
+ rows_per_tg = _N_SIMDGROUP * 1
536
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
537
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
538
+ rows_per_tg = _N_SIMDGROUP * 2
539
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
540
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0)
541
+ else:
542
+ raise ValueError(f"matvec not wired for {plan.fmt}")
543
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
544
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
545
+ Metal.MTLSizeMake(grid_x, 1, 1),
546
+ Metal.MTLSizeMake(tg_threads, 1, 1),
547
+ )
548
+
549
+
550
+ def new_command_buffer():
551
+ """Start a fresh MTLCommandBuffer. Caller batches `encode_matvec(plan, enc)`
552
+ onto its encoder, then calls `cb.commit()` and `cb.waitUntilCompleted()`
553
+ once for the whole batch — matching llama.cpp's per-step semantics."""
554
+ _ensure_metal()
555
+ return _COMMAND_QUEUE.commandBuffer()
556
+
557
+
558
+ def dispatch_matvec(plan: MatVecPlan, sync: bool = True):
559
+ import Metal
560
+
561
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
562
+ cmd = _COMMAND_QUEUE.commandBuffer()
563
+ enc = cmd.computeCommandEncoder()
564
+ enc.setComputePipelineState_(pso)
565
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
566
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
567
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
568
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
569
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
570
+
571
+ needs_shared = plan.fmt in ("iq4_nl", "iq4_xs")
572
+ if plan.fmt == "q4_K":
573
+ rows_per_tg = _N_DST
574
+ tg_threads = _SIMD_WIDTH
575
+ elif plan.fmt in ("q4_0", "q8_0"):
576
+ rows_per_tg = _N_SIMDGROUP * _N_DST
577
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
578
+ elif plan.fmt == "q5_K":
579
+ rows_per_tg = _N_SIMDGROUP * 2
580
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
581
+ elif plan.fmt == "q6_K":
582
+ rows_per_tg = _N_SIMDGROUP * 1
583
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
584
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
585
+ rows_per_tg = _N_SIMDGROUP * 2 # 2 sg × 2 rows/sg
586
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
587
+ else:
588
+ raise ValueError(f"matvec not wired for {plan.fmt}")
589
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
590
+ if needs_shared:
591
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0) # 64 floats per simdgroup × 2 sg
592
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
593
+ Metal.MTLSizeMake(grid_x, 1, 1),
594
+ Metal.MTLSizeMake(tg_threads, 1, 1),
595
+ )
596
+ enc.endEncoding()
597
+ cmd.commit()
598
+ if sync:
599
+ cmd.waitUntilCompleted()
600
+ return cmd
build/torch28-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:28a3233811126e83e9a2e95f84828b33889973a5ef37c402949253da8ce3ffda
3
+ size 236176
build/torch28-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _gguf_dequant_530e374
3
+ ops = torch.ops._gguf_dequant_530e374
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_gguf_dequant_530e374::{op_name}"
build/torch28-metal-aarch64-darwin/gguf_dequant/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import sys
3
+
4
+ import importlib
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch28-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "python-depends": []
4
+ }
build/torch28-metal-aarch64-darwin/nn.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GgufLinear — drop-in nn.Linear replacement that runs quantized matmul directly.
2
+
3
+ Forward pass:
4
+ - If the input has effective batch size 1 (single token), use mul_mat_vec
5
+ (memory-bandwidth-bound, faster than bf16 mat-vec on MPS).
6
+ - Otherwise use mul_mat (compute-bound, similar throughput to bf16 GEMM but
7
+ keeps weights at native quantization → 3–4× less RAM than bf16).
8
+
9
+ Supported quant types today: Q4_0, Q8_0, Q4_K. Adding more is just a matter of
10
+ porting the candle kernels — the dispatch logic is generic over `quant_type`.
11
+
12
+ Prototype design: weights are stored as raw uint8 bytes in `qweight`. The
13
+ forward path copies activations into a shared MTLBuffer, runs the Metal
14
+ kernel, reads the fp32 output back. Production path (kernels-community +
15
+ PR #767) skips the copies via direct MTLBuffer handoff.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import numpy as np
22
+ import torch
23
+
24
+ from . import (
25
+ allocate_matmul_plan,
26
+ allocate_matvec_plan,
27
+ dispatch_matmul,
28
+ dispatch_matvec,
29
+ )
30
+
31
+
32
+ # Per-quant-type geometry: (block_bytes, block_elems, matvec_M_alignment)
33
+ _QUANT_INFO = {
34
+ "Q4_0": (18, 32, 8), # matvec uses 2 simdgroups × N_DST=4 = 8 rows/threadgroup
35
+ "Q8_0": (34, 32, 8),
36
+ "Q4_K": (144, 256, 4), # matvec uses 1 simdgroup × N_DST=4 = 4 rows/threadgroup
37
+ }
38
+
39
+ # Map our Python-side quant name to the Metal kernel suffix.
40
+ _KERNEL_FMT = {
41
+ "Q4_0": "q4_0",
42
+ "Q8_0": "q8_0",
43
+ "Q4_K": "q4_K",
44
+ }
45
+
46
+
47
+ class GgufLinear(torch.nn.Module):
48
+ """Linear layer backed by GGUF-quantized weights.
49
+
50
+ Args:
51
+ in_features: K dimension. Must be a multiple of the format's block size
52
+ (32 for Q4_0/Q8_0, 256 for Q4_K).
53
+ out_features: M dimension. Must be a multiple of the format's matvec
54
+ row alignment (8 for Q4_0/Q8_0, 4 for Q4_K) and of 64 for matmul.
55
+ quant_type: one of "Q4_0", "Q8_0", "Q4_K".
56
+ bias: whether to add a learnable bias after the matmul.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ in_features: int,
62
+ out_features: int,
63
+ quant_type: str = "Q4_K",
64
+ bias: bool = False,
65
+ device: Optional[torch.device | str] = None,
66
+ ):
67
+ super().__init__()
68
+ if quant_type not in _QUANT_INFO:
69
+ raise NotImplementedError(
70
+ f"GgufLinear supports {sorted(_QUANT_INFO)} today, got {quant_type}"
71
+ )
72
+ block_bytes, block_elems, matvec_align = _QUANT_INFO[quant_type]
73
+ if in_features % block_elems != 0:
74
+ raise ValueError(
75
+ f"in_features must be a multiple of {block_elems} for {quant_type}, got {in_features}"
76
+ )
77
+ if out_features % matvec_align != 0:
78
+ raise ValueError(
79
+ f"out_features must be a multiple of {matvec_align} for {quant_type}, got {out_features}"
80
+ )
81
+
82
+ self.in_features = in_features
83
+ self.out_features = out_features
84
+ self.quant_type = quant_type
85
+ self._fmt = _KERNEL_FMT[quant_type]
86
+ self._block_bytes = block_bytes
87
+ self._block_elems = block_elems
88
+
89
+ nblocks_per_row = in_features // block_elems
90
+ qweight_nbytes = out_features * nblocks_per_row * block_bytes
91
+ self.register_buffer(
92
+ "qweight",
93
+ torch.empty(qweight_nbytes, dtype=torch.uint8, device=device or "cpu"),
94
+ persistent=True,
95
+ )
96
+
97
+ if bias:
98
+ self.bias = torch.nn.Parameter(
99
+ torch.zeros(out_features, dtype=torch.float32, device=device or "cpu"),
100
+ requires_grad=False,
101
+ )
102
+ else:
103
+ self.register_parameter("bias", None)
104
+
105
+ def extra_repr(self) -> str:
106
+ return (
107
+ f"in_features={self.in_features}, out_features={self.out_features}, "
108
+ f"quant_type={self.quant_type}, bias={self.bias is not None}"
109
+ )
110
+
111
+ @torch.no_grad()
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ """x: shape (..., K). Returns (..., M) fp32."""
114
+ if x.shape[-1] != self.in_features:
115
+ raise ValueError(f"Last dim of x must be {self.in_features}, got {x.shape[-1]}")
116
+
117
+ batch_shape = x.shape[:-1]
118
+ x_flat = x.reshape(-1, self.in_features).contiguous().to(torch.float32)
119
+ N = x_flat.shape[0]
120
+
121
+ # CPU view of qweight for the bytes copy into shared MTLBuffers.
122
+ # Production path skips this once the kernels-community build is wired.
123
+ qweight_cpu = self.qweight.detach().to("cpu", copy=False)
124
+ x_cpu = x_flat.to("cpu", copy=False)
125
+
126
+ if N == 1:
127
+ out = self._forward_matvec(qweight_cpu, x_cpu.reshape(-1))
128
+ else:
129
+ out = self._forward_matmul(qweight_cpu, x_cpu)
130
+
131
+ out = out.reshape(*batch_shape, self.out_features)
132
+ if x.device != out.device:
133
+ out = out.to(x.device)
134
+ if self.bias is not None:
135
+ out = out + self.bias.to(out.device).to(out.dtype)
136
+ return out
137
+
138
+ def _forward_matvec(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
139
+ M, K = self.out_features, self.in_features
140
+ plan = allocate_matvec_plan(self._fmt, M=M, K=K)
141
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
142
+ a_view[:] = qweight_cpu.numpy()
143
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(K * 4), dtype=np.float32)
144
+ b_view[:] = x_cpu.numpy()
145
+ dispatch_matvec(plan, sync=True)
146
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * 4), dtype=np.float32)
147
+ return torch.from_numpy(c_view.copy())
148
+
149
+ def _forward_matmul(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
150
+ N, K = x_cpu.shape
151
+ M = self.out_features
152
+ plan = allocate_matmul_plan(self._fmt, M=M, N=N, K=K)
153
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
154
+ a_view[:] = qweight_cpu.numpy()
155
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(N * K * 4), dtype=np.float32)
156
+ b_view[:] = x_cpu.numpy().reshape(-1)
157
+ dispatch_matmul(plan, sync=True)
158
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * N * 4), dtype=np.float32)
159
+ # Kernel output layout is (N, M) in row-major; we want (N, M).
160
+ return torch.from_numpy(c_view.reshape(N, M).copy())
161
+
162
+ @classmethod
163
+ def from_quantized_bytes(
164
+ cls,
165
+ quant_type: str,
166
+ qweight_bytes: bytes,
167
+ in_features: int,
168
+ out_features: int,
169
+ bias: Optional[torch.Tensor] = None,
170
+ device: Optional[torch.device | str] = None,
171
+ ) -> "GgufLinear":
172
+ """Convenience for tests/loaders: hydrate from raw GGUF block bytes."""
173
+ layer = cls(in_features=in_features, out_features=out_features,
174
+ quant_type=quant_type, bias=bias is not None, device=device)
175
+ expected = layer.qweight.numel()
176
+ if len(qweight_bytes) != expected:
177
+ raise ValueError(
178
+ f"qweight_bytes length {len(qweight_bytes)} != expected {expected} "
179
+ f"for {quant_type} M={out_features} K={in_features}"
180
+ )
181
+ layer.qweight.copy_(torch.frombuffer(bytearray(qweight_bytes), dtype=torch.uint8))
182
+ if bias is not None:
183
+ layer.bias.copy_(bias.to(torch.float32))
184
+ return layer
185
+
186
+ # Back-compat shim for the existing Q4_K-only test
187
+ @classmethod
188
+ def from_q4_K_bytes(cls, qweight_bytes, in_features, out_features, bias=None, device=None):
189
+ return cls.from_quantized_bytes(
190
+ "Q4_K", qweight_bytes, in_features, out_features, bias=bias, device=device
191
+ )
build/torch28-metal-aarch64-darwin/transformers_glue.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Glue for plugging `GgufLinear` into a transformers model.
2
+
3
+ The flow is intentionally non-invasive: we read raw block bytes directly from
4
+ the GGUF file via `gguf.GGUFReader`, walk the model, and for every `nn.Linear`
5
+ whose corresponding GGUF tensor is one of our supported quant types we
6
+ in-place replace the module with `GgufLinear` and copy the bytes into
7
+ `qweight`. Tensors that don't map to a Linear (or are unsupported quants like
8
+ F32 norms, embeddings) get dequantized via `gguf.dequantize` and copied into
9
+ the existing parameter as fp32.
10
+
11
+ This avoids modifying the transformers loader. The trade-off is that the
12
+ model has to be constructed first with regular `nn.Linear` shells (which we
13
+ then mutate), but those shells are on the meta device or freshly initialised
14
+ and don't carry the dequantized weight at the time of swap — so peak memory
15
+ during load is bounded by `(model_size_fp32) + (model_size_quantized_qweights)`,
16
+ which fits in RAM for the small models we care about.
17
+
18
+ Typical use:
19
+
20
+ from transformers import AutoConfig, AutoModelForCausalLM
21
+ cfg = AutoConfig.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct-GGUF",
22
+ gguf_file="qwen2.5-0.5b-instruct-q4_0.gguf")
23
+ model = AutoModelForCausalLM.from_config(cfg).eval()
24
+ load_gguf_into_model_with_gguf_linear(
25
+ model,
26
+ gguf_path="…/qwen2.5-0.5b-instruct-q4_0.gguf",
27
+ )
28
+ out = model.generate(input_ids=…, max_new_tokens=10)
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+ import gguf
36
+ import numpy as np
37
+ import torch
38
+ import torch.nn as nn
39
+
40
+ from .nn import GgufLinear, _QUANT_INFO
41
+
42
+
43
+ # Map gguf.GGMLQuantizationType.name → our GgufLinear quant_type string.
44
+ _GGUF_NAME_TO_GGUF_LINEAR = {
45
+ "Q4_0": "Q4_0",
46
+ "Q8_0": "Q8_0",
47
+ "Q4_K": "Q4_K",
48
+ }
49
+
50
+
51
+ def _quant_supported_by_gguf_linear(qt: gguf.GGMLQuantizationType) -> bool:
52
+ return qt.name in _GGUF_NAME_TO_GGUF_LINEAR
53
+
54
+
55
+ # --- Tensor-name mapping ------------------------------------------------------
56
+ # GGUF tensors use llama.cpp-style names like `blk.0.attn_q.weight`. HF models
57
+ # use `model.layers.0.self_attn.q_proj.weight`. We use a tiny version of the
58
+ # transformers rename table here so the swap util can stay self-contained.
59
+ # (Transformers' own GGUF loader has a much more thorough version of this.)
60
+
61
+ _BASIC_RENAMES = [
62
+ (r"^blk\.", "model.layers."),
63
+ (r"\.attn_q\.weight$", ".self_attn.q_proj.weight"),
64
+ (r"\.attn_k\.weight$", ".self_attn.k_proj.weight"),
65
+ (r"\.attn_v\.weight$", ".self_attn.v_proj.weight"),
66
+ (r"\.attn_output\.weight$", ".self_attn.o_proj.weight"),
67
+ (r"\.ffn_gate\.weight$", ".mlp.gate_proj.weight"),
68
+ (r"\.ffn_up\.weight$", ".mlp.up_proj.weight"),
69
+ (r"\.ffn_down\.weight$", ".mlp.down_proj.weight"),
70
+ (r"\.attn_norm\.weight$", ".input_layernorm.weight"),
71
+ (r"\.ffn_norm\.weight$", ".post_attention_layernorm.weight"),
72
+ (r"\.attn_(q|k|v)\.bias$", r".self_attn.\1_proj.bias"),
73
+ (r"^token_embd\.weight$", "model.embed_tokens.weight"),
74
+ (r"^output_norm\.weight$", "model.norm.weight"),
75
+ (r"^output\.weight$", "lm_head.weight"),
76
+ ]
77
+
78
+
79
+ def _gguf_name_to_hf_name(gguf_name: str) -> str:
80
+ """Apply our minimal rename table to map a GGUF tensor name to an HF parameter name."""
81
+ import re
82
+
83
+ out = gguf_name
84
+ for pat, repl in _BASIC_RENAMES:
85
+ out = re.sub(pat, repl, out)
86
+ return out
87
+
88
+
89
+ def _is_attn_qk(gguf_name: str) -> bool:
90
+ return ".attn_q.weight" in gguf_name or ".attn_k.weight" in gguf_name
91
+
92
+
93
+ def _reverse_permute_qk(w: np.ndarray, n_head: int, n_kv: int | None) -> np.ndarray:
94
+ """GGUF Q/K weights are permuted on quantize; undo before loading into HF.
95
+ Reference: https://github.com/ggerganov/llama.cpp/blob/.../convert_hf_to_gguf.py."""
96
+ if n_kv is not None and n_head != n_kv:
97
+ n_head = n_kv
98
+ dim = w.shape[0] // n_head // 2
99
+ w = w.reshape(n_head, dim, 2, *w.shape[1:])
100
+ return w.swapaxes(2, 1).reshape(*w.shape[:1], *w.shape[3:]) if False else w.reshape(-1, *w.shape[3:]).swapaxes(2, 1).reshape(n_head * dim * 2, *w.shape[3:])
101
+
102
+
103
+ def _set_module_param(model: nn.Module, qualified_name: str, value: torch.Tensor) -> bool:
104
+ """Set a parameter or buffer on `model` by dotted name. Returns True on success."""
105
+ parts = qualified_name.split(".")
106
+ mod = model
107
+ for p in parts[:-1]:
108
+ if not hasattr(mod, p):
109
+ return False
110
+ mod = getattr(mod, p)
111
+ leaf = parts[-1]
112
+ if leaf in mod._parameters:
113
+ param = mod._parameters[leaf]
114
+ if param is None:
115
+ return False
116
+ with torch.no_grad():
117
+ mod._parameters[leaf] = nn.Parameter(value.to(dtype=param.dtype, device=param.device),
118
+ requires_grad=param.requires_grad)
119
+ return True
120
+ if leaf in mod._buffers:
121
+ with torch.no_grad():
122
+ mod._buffers[leaf] = value
123
+ return True
124
+ return False
125
+
126
+
127
+ def _get_module_by_name(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
128
+ """Return the leaf module matching `qualified_name` minus its terminal attribute,
129
+ or None if anything along the path is missing."""
130
+ parts = qualified_name.split(".")
131
+ mod = model
132
+ for p in parts[:-1]:
133
+ if not hasattr(mod, p):
134
+ return None
135
+ mod = getattr(mod, p)
136
+ return mod
137
+
138
+
139
+ def _get_attr_by_path(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
140
+ """Walk dotted `qualified_name` on `model` and return the final attribute,
141
+ or None if anything along the path is missing."""
142
+ if not qualified_name:
143
+ return model
144
+ mod = model
145
+ for p in qualified_name.split("."):
146
+ if not hasattr(mod, p):
147
+ return None
148
+ mod = getattr(mod, p)
149
+ return mod
150
+
151
+
152
+ def _swap_linear_to_gguf(
153
+ parent: nn.Module, attr: str, in_features: int, out_features: int, quant_type: str,
154
+ bias_tensor: Optional[torch.Tensor], device: Optional[torch.device | str] = None
155
+ ) -> GgufLinear:
156
+ """Replace `parent.attr` (an nn.Linear) with a new GgufLinear of the same shape."""
157
+ new = GgufLinear(in_features=in_features, out_features=out_features,
158
+ quant_type=quant_type, bias=bias_tensor is not None, device=device)
159
+ if bias_tensor is not None and new.bias is not None:
160
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
161
+ setattr(parent, attr, new)
162
+ return new
163
+
164
+
165
+ def load_gguf_into_model_with_gguf_linear(
166
+ model: nn.Module,
167
+ gguf_path: str | Path,
168
+ skip_modules: Optional[set[str]] = None,
169
+ force_dequant: bool = False,
170
+ ) -> dict:
171
+ """Read a GGUF file and populate `model` in-place.
172
+
173
+ Two-phase load to keep the byte path simple and the bias path correct:
174
+
175
+ Phase 1 — full dequant: every tensor (linear weights, norms, embeddings,
176
+ biases) is dequantized to fp32 and written into the matching HF
177
+ parameter. After this pass the model would already produce correct
178
+ outputs (this is what `force_dequant=True` stops at).
179
+ Phase 2 — swap linears: walk every `nn.Linear` that maps to a tensor
180
+ whose GGUF quant type is supported by `GgufLinear`; swap it for a
181
+ `GgufLinear` and re-attach raw block bytes (preserving the bias that
182
+ Phase 1 just loaded).
183
+
184
+ Returns a dict with diagnostic counts.
185
+ """
186
+ reader = gguf.GGUFReader(str(gguf_path))
187
+
188
+ # Pull a few config bits for the Q/K reverse-permute.
189
+ def _read_int(field: str, default: int = 0) -> int:
190
+ f = reader.fields.get(field)
191
+ if f is None:
192
+ return default
193
+ return int(f.parts[f.data[0]][0]) if f.data else default
194
+
195
+ arch = ""
196
+ arch_field = reader.fields.get("general.architecture")
197
+ if arch_field is not None and arch_field.data:
198
+ arch = bytes(arch_field.parts[arch_field.data[0]]).decode("utf-8", errors="ignore")
199
+ n_head = _read_int(f"{arch}.attention.head_count")
200
+ n_kv_head = _read_int(f"{arch}.attention.head_count_kv") or n_head
201
+
202
+ skip_modules = skip_modules or set()
203
+ stats = {"as_gguf_linear": 0, "dequantized": 0, "skipped": 0, "unmatched": []}
204
+
205
+ # Track which (hf_name → quant_type) pairs are eligible for GgufLinear swap
206
+ # after Phase 1. We'll re-quantize the loaded fp32 weight in Phase 2.
207
+ swap_candidates: list[tuple[str, str]] = []
208
+
209
+ # ---------------- Phase 1: dequant everything into the model. -------------
210
+ for t in reader.tensors:
211
+ hf_name = _gguf_name_to_hf_name(t.name)
212
+ if any(skip in hf_name for skip in skip_modules):
213
+ stats["skipped"] += 1
214
+ continue
215
+
216
+ parent = _get_module_by_name(model, hf_name)
217
+ if parent is None or not hasattr(parent, hf_name.rsplit(".", 1)[-1]):
218
+ stats["unmatched"].append(t.name)
219
+ continue
220
+ leaf = hf_name.rsplit(".", 1)[-1]
221
+ cur = getattr(parent, leaf)
222
+
223
+ fp32 = gguf.dequantize(
224
+ np.frombuffer(np.ascontiguousarray(t.data).tobytes(), dtype=np.uint8),
225
+ t.tensor_type,
226
+ ).astype(np.float32)
227
+ if _is_attn_qk(t.name) and n_head > 0:
228
+ shape = list(t.shape)
229
+ M, K = int(shape[1]), int(shape[0])
230
+ heads = n_head if ".attn_q." in t.name else n_kv_head
231
+ head_dim = M // heads
232
+ fp32 = fp32.reshape(M, K).reshape(heads, head_dim // 2, 2, K).swapaxes(2, 1).reshape(M, K)
233
+ target_shape = tuple(cur.shape) if hasattr(cur, "shape") else (fp32.size,)
234
+ if np.prod(target_shape) == fp32.size:
235
+ fp32 = fp32.reshape(target_shape)
236
+ tensor = torch.from_numpy(fp32)
237
+ if not _set_module_param(model, hf_name, tensor):
238
+ stats["unmatched"].append(t.name)
239
+ continue
240
+ stats["dequantized"] += 1
241
+
242
+ # Record candidacy for swap in Phase 2.
243
+ is_linear_weight = isinstance(parent, nn.Linear) and leaf == "weight"
244
+ if is_linear_weight and _quant_supported_by_gguf_linear(t.tensor_type) and not force_dequant:
245
+ swap_candidates.append((hf_name, _GGUF_NAME_TO_GGUF_LINEAR[t.tensor_type.name]))
246
+
247
+ # ---------------- Phase 2: swap Linear → GgufLinear. ----------------------
248
+ # Re-quantize the loaded fp32 weight via gguf-py (works for Q4_0/Q8_0;
249
+ # K-quants don't have a Python quantizer yet — those will skip swap).
250
+ for hf_name, quant_type in swap_candidates:
251
+ # hf_name ends in `.weight`; the Linear module is at hf_name[:-len(".weight")]
252
+ linear_path = hf_name[: -len(".weight")]
253
+ grandparent_path, _, linear_attr = linear_path.rpartition(".")
254
+ grandparent = model if not grandparent_path else _get_attr_by_path(model, grandparent_path)
255
+ linear = _get_attr_by_path(model, linear_path)
256
+ if grandparent is None or not isinstance(linear, nn.Linear):
257
+ continue
258
+
259
+ # Re-quantize the (now permuted-if-applicable) fp32 weight.
260
+ try:
261
+ qt = getattr(gguf.GGMLQuantizationType, quant_type)
262
+ qbytes = bytes(
263
+ gguf.quantize(linear.weight.detach().cpu().numpy().astype(np.float32), qt).tobytes()
264
+ )
265
+ except (NotImplementedError, Exception) as e:
266
+ # K-quants etc. — leave as Linear (fp32 weight is already loaded).
267
+ stats.setdefault("re_quantize_failed", []).append((hf_name, str(e)))
268
+ continue
269
+
270
+ bias_tensor = linear.bias.detach().clone() if linear.bias is not None else None
271
+ new = GgufLinear(
272
+ in_features=linear.in_features,
273
+ out_features=linear.out_features,
274
+ quant_type=quant_type,
275
+ bias=bias_tensor is not None,
276
+ )
277
+ if bias_tensor is not None:
278
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
279
+ new.qweight.copy_(torch.frombuffer(bytearray(qbytes), dtype=torch.uint8))
280
+ setattr(grandparent, linear_attr, new)
281
+ stats["as_gguf_linear"] += 1
282
+ stats["dequantized"] -= 1 # this one's no longer "just" dequant — swapped
283
+
284
+ return stats
build/torch29-metal-aarch64-darwin/__init__.py ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GGUF dequantization kernels for Apple Silicon (Metal).
2
+
3
+ Six standalone kernels covering the GGUF formats most common in the wild:
4
+
5
+ Q4_0 Q8_0 Q4_K Q5_K Q6_K IQ4_NL IQ4_XS
6
+
7
+ Two API levels are exposed for each:
8
+
9
+ - ``dequantize_<format>_bytes(quant_bytes) -> np.ndarray``
10
+ One-shot path used by the tests for bit-exact validation against a
11
+ NumPy reference. Allocates input/output buffers internally.
12
+
13
+ - ``allocate_<format>_plan(nblocks)`` + ``dispatch_<format>(plan)``
14
+ Low-level path for benchmarks. Buffers are pre-allocated; ``dispatch``
15
+ only encodes + dispatches + syncs, so the timing isolates GPU work
16
+ from CPU<->buffer copies.
17
+
18
+ The runtime path uses PyObjC + Metal.framework so the correctness loop
19
+ works on any Apple Silicon Mac without Xcode 26 / Nix / kernel-builder.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ # Lazy globals — populated on first use.
29
+ _DEVICE = None
30
+ _COMMAND_QUEUE = None
31
+ _LIBRARY = None
32
+ _PIPELINES: dict = {}
33
+
34
+ _KERNEL_SOURCE_PATH = Path(__file__).resolve().parent.parent.parent / "gguf_dequant_metal" / "dequantize.metal"
35
+
36
+
37
+ def _ensure_metal():
38
+ global _DEVICE, _COMMAND_QUEUE, _LIBRARY
39
+ if _DEVICE is not None:
40
+ return
41
+ import Metal # pyobjc-framework-metal
42
+
43
+ _DEVICE = Metal.MTLCreateSystemDefaultDevice()
44
+ if _DEVICE is None:
45
+ raise RuntimeError("No default Metal device available")
46
+ _COMMAND_QUEUE = _DEVICE.newCommandQueue()
47
+
48
+ src = _KERNEL_SOURCE_PATH.read_text()
49
+ options = Metal.MTLCompileOptions.new()
50
+ lib, err = _DEVICE.newLibraryWithSource_options_error_(src, options, None)
51
+ if lib is None:
52
+ raise RuntimeError(f"Metal compile failed: {err}")
53
+ _LIBRARY = lib
54
+
55
+
56
+ def _pipeline(fn_name: str):
57
+ _ensure_metal()
58
+ pso = _PIPELINES.get(fn_name)
59
+ if pso is not None:
60
+ return pso
61
+
62
+ fn = _LIBRARY.newFunctionWithName_(fn_name)
63
+ if fn is None:
64
+ raise RuntimeError(f"Kernel function not found: {fn_name}")
65
+ pso, err = _DEVICE.newComputePipelineStateWithFunction_error_(fn, None)
66
+ if pso is None:
67
+ raise RuntimeError(f"Pipeline state creation failed: {err}")
68
+ _PIPELINES[fn_name] = pso
69
+ return pso
70
+
71
+
72
+ # (kernel_name, bytes_per_block, elems_per_block, threads_per_block_group)
73
+ _FORMATS = {
74
+ "q4_0": ("dequantize_q4_0", 18, 32, 16),
75
+ "q8_0": ("dequantize_q8_0", 34, 32, 32),
76
+ "q4_K": ("dequantize_q4_K", 144, 256, 256),
77
+ "q5_K": ("dequantize_q5_K", 176, 256, 256),
78
+ "q6_K": ("dequantize_q6_K", 210, 256, 256),
79
+ "iq4_nl": ("dequantize_iq4_nl", 18, 32, 16),
80
+ "iq4_xs": ("dequantize_iq4_xs", 136, 256, 256),
81
+ }
82
+
83
+
84
+ class Plan:
85
+ """Pre-allocated I/O buffers for a single (format, nblocks) shape."""
86
+
87
+ __slots__ = ("fmt", "nblocks", "in_buf", "out_buf", "_nblocks_bytes")
88
+
89
+ def __init__(self, fmt: str, nblocks: int, in_buf, out_buf):
90
+ self.fmt = fmt
91
+ self.nblocks = nblocks
92
+ self.in_buf = in_buf
93
+ self.out_buf = out_buf
94
+ self._nblocks_bytes = np.array([nblocks], dtype=np.uint32).tobytes()
95
+
96
+
97
+ def allocate_plan(fmt: str, nblocks: int) -> Plan:
98
+ import Metal
99
+
100
+ _ensure_metal()
101
+ _, bpb, epb, _ = _FORMATS[fmt]
102
+ in_buf = _DEVICE.newBufferWithLength_options_(nblocks * bpb, Metal.MTLResourceStorageModeShared)
103
+ out_buf = _DEVICE.newBufferWithLength_options_(nblocks * epb * 4, Metal.MTLResourceStorageModeShared)
104
+ return Plan(fmt=fmt, nblocks=nblocks, in_buf=in_buf, out_buf=out_buf)
105
+
106
+
107
+ def dispatch(plan: Plan, sync: bool = True):
108
+ import Metal
109
+
110
+ kernel_name, _, _, tg = _FORMATS[plan.fmt]
111
+ pso = _pipeline(kernel_name)
112
+ cmd = _COMMAND_QUEUE.commandBuffer()
113
+ enc = cmd.computeCommandEncoder()
114
+ enc.setComputePipelineState_(pso)
115
+ enc.setBuffer_offset_atIndex_(plan.in_buf, 0, 0)
116
+ enc.setBuffer_offset_atIndex_(plan.out_buf, 0, 1)
117
+ enc.setBytes_length_atIndex_(plan._nblocks_bytes, len(plan._nblocks_bytes), 2)
118
+ enc.dispatchThreads_threadsPerThreadgroup_(
119
+ Metal.MTLSizeMake(plan.nblocks * tg, 1, 1),
120
+ Metal.MTLSizeMake(tg, 1, 1),
121
+ )
122
+ enc.endEncoding()
123
+ cmd.commit()
124
+ if sync:
125
+ cmd.waitUntilCompleted()
126
+ return cmd
127
+
128
+
129
+ def dequantize_bytes(fmt: str, quant_bytes) -> np.ndarray:
130
+ """One-shot bytes-in / float32-out for the named format."""
131
+ _ensure_metal()
132
+ _, bpb, epb, _ = _FORMATS[fmt]
133
+
134
+ buf = bytes(quant_bytes) if not isinstance(quant_bytes, (bytes, bytearray)) else quant_bytes
135
+ nbytes = len(buf)
136
+ if nbytes % bpb != 0:
137
+ raise ValueError(f"{fmt} input not a multiple of {bpb} bytes: got {nbytes}")
138
+ nblocks = nbytes // bpb
139
+
140
+ plan = allocate_plan(fmt, nblocks)
141
+ in_view = np.frombuffer(plan.in_buf.contents().as_buffer(nbytes), dtype=np.uint8)
142
+ in_view[:] = np.frombuffer(buf, dtype=np.uint8)
143
+ dispatch(plan, sync=True)
144
+ out_nbytes = nblocks * epb * 4
145
+ return np.frombuffer(plan.out_buf.contents().as_buffer(out_nbytes), dtype=np.float32).copy()
146
+
147
+
148
+ # --- Per-format convenience wrappers ----------------------------------------
149
+
150
+ def dequantize_q4_0_bytes(qb): return dequantize_bytes("q4_0", qb)
151
+ def dequantize_q8_0_bytes(qb): return dequantize_bytes("q8_0", qb)
152
+ def dequantize_q4_K_bytes(qb): return dequantize_bytes("q4_K", qb)
153
+ def dequantize_q5_K_bytes(qb): return dequantize_bytes("q5_K", qb)
154
+ def dequantize_q6_K_bytes(qb): return dequantize_bytes("q6_K", qb)
155
+ def dequantize_iq4_nl_bytes(qb): return dequantize_bytes("iq4_nl", qb)
156
+ def dequantize_iq4_xs_bytes(qb): return dequantize_bytes("iq4_xs", qb)
157
+
158
+ def allocate_q4_0_plan(nblocks): return allocate_plan("q4_0", nblocks)
159
+ def allocate_q8_0_plan(nblocks): return allocate_plan("q8_0", nblocks)
160
+ def allocate_q4_K_plan(nblocks): return allocate_plan("q4_K", nblocks)
161
+ def allocate_q5_K_plan(nblocks): return allocate_plan("q5_K", nblocks)
162
+ def allocate_q6_K_plan(nblocks): return allocate_plan("q6_K", nblocks)
163
+ def allocate_iq4_nl_plan(nblocks): return allocate_plan("iq4_nl", nblocks)
164
+ def allocate_iq4_xs_plan(nblocks): return allocate_plan("iq4_xs", nblocks)
165
+
166
+ def dispatch_q4_0(plan, **kw): return dispatch(plan, **kw)
167
+ def dispatch_q8_0(plan, **kw): return dispatch(plan, **kw)
168
+ def dispatch_q4_K(plan, **kw): return dispatch(plan, **kw)
169
+ def dispatch_q5_K(plan, **kw): return dispatch(plan, **kw)
170
+ def dispatch_q6_K(plan, **kw): return dispatch(plan, **kw)
171
+ def dispatch_iq4_nl(plan, **kw): return dispatch(plan, **kw)
172
+ def dispatch_iq4_xs(plan, **kw): return dispatch(plan, **kw)
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Torch op entry points — the production path used by transformers (no PyObjC,
177
+ # no runtime compile). Only available in builds produced by kernel-builder,
178
+ # which generates `_ops.py` exporting `ops = torch.ops._gguf_dequant_<rev>`.
179
+ # In a dev-mode checkout (running from torch-ext/ directly) `_ops` isn't
180
+ # present, so the torch_* entry points raise on call.
181
+ # ---------------------------------------------------------------------------
182
+
183
+ try:
184
+ from ._ops import ops as _ops # generated by kernel-builder
185
+ except ImportError:
186
+ _ops = None
187
+
188
+
189
+ def _require_ops():
190
+ if _ops is None:
191
+ raise RuntimeError(
192
+ "torch op binding not available — this build was loaded from the dev tree. "
193
+ "Use the *_bytes / *_plan APIs (PyObjC path) or install a kernel-builder build."
194
+ )
195
+ return _ops
196
+
197
+
198
+ def torch_dequantize_q4_0(input, output): _require_ops().dequantize_q4_0(input, output)
199
+ def torch_dequantize_q8_0(input, output): _require_ops().dequantize_q8_0(input, output)
200
+ def torch_dequantize_q4_K(input, output): _require_ops().dequantize_q4_K(input, output)
201
+ def torch_dequantize_q5_K(input, output): _require_ops().dequantize_q5_K(input, output)
202
+ def torch_dequantize_q6_K(input, output): _require_ops().dequantize_q6_K(input, output)
203
+ def torch_dequantize_iq4_nl(input, output): _require_ops().dequantize_iq4_nl(input, output)
204
+ def torch_dequantize_iq4_xs(input, output): _require_ops().dequantize_iq4_xs(input, output)
205
+
206
+
207
+ # =============================================================================
208
+ # Q4_K × fp32 matmul — `GgufLinear` ceiling experiment.
209
+ # =============================================================================
210
+ # Layout convention (matches llama.cpp's kernel_mul_mm):
211
+ # A (Q4_K weights): shape (M, K) M = out_features, K = in_features.
212
+ # M and K must be multiples of 64 and 256 respectively for
213
+ # the simdgroup tile geometry to land cleanly.
214
+ # B (fp32 input): shape (N, K) N = batch * seqlen, contiguous in K.
215
+ # C (fp32 output): shape (M, N) column-major-ish (matches llama.cpp).
216
+
217
+ _BLOCK_SIZE_M = 64
218
+ _BLOCK_SIZE_N = 32
219
+ _THREAD_PER_BLOCK = 128
220
+ _SHARED_BYTES = 8192
221
+
222
+
223
+ class MatMulQ4KPlan:
224
+ __slots__ = ("M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
225
+
226
+ def __init__(self, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
227
+ self.M = M
228
+ self.N = N
229
+ self.K = K
230
+ self.a_buf = a_buf
231
+ self.b_buf = b_buf
232
+ self.c_buf = c_buf
233
+ self._dims_bytes = dims_bytes
234
+
235
+
236
+ def allocate_matmul_q4_K_plan(M: int, N: int, K: int) -> MatMulQ4KPlan:
237
+ """Pre-allocate weight (Q4_K), input (fp32), output (fp32) MTLBuffers."""
238
+ import Metal
239
+
240
+ _ensure_metal()
241
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
242
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
243
+ assert K % 256 == 0, f"K must be multiple of 256, got {K}"
244
+
245
+ a_bytes = M * (K // 256) * 144 # M rows × (K/QK_K) super-blocks × 144 bytes
246
+ b_bytes = N * K * 4 # fp32
247
+ c_bytes = M * N * 4 # fp32
248
+
249
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
250
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
251
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
252
+
253
+ # Pack ne00, ne01, ne11, nb01, nb11 into a uniforms buffer.
254
+ nb01 = (K // 256) * 144 # bytes per A row
255
+ nb11 = K * 4 # bytes per B row
256
+ dims = np.array([K], dtype=np.int64).tobytes() # ne00
257
+ dims += np.array([M], dtype=np.int64).tobytes() # ne01
258
+ dims += np.array([N], dtype=np.int64).tobytes() # ne11
259
+ dims += np.array([nb01], dtype=np.uint64).tobytes()
260
+ dims += np.array([nb11], dtype=np.uint64).tobytes()
261
+
262
+ return MatMulQ4KPlan(M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
263
+
264
+
265
+ def dispatch_matmul_q4_K(plan: MatMulQ4KPlan, sync: bool = True):
266
+ """Dispatch mul_mat_q4_K_f32 on the pre-allocated buffers."""
267
+ import Metal
268
+
269
+ pso = _pipeline("mul_mat_q4_K_f32")
270
+ cmd = _COMMAND_QUEUE.commandBuffer()
271
+ enc = cmd.computeCommandEncoder()
272
+ enc.setComputePipelineState_(pso)
273
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
274
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
275
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
276
+ # ne00 (K), ne01 (M), ne11 (N), nb01, nb11 via setBytes — 5 scalars
277
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
278
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
279
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
280
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
281
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
282
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
283
+
284
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
285
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
286
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
287
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
288
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
289
+ )
290
+ enc.endEncoding()
291
+ cmd.commit()
292
+ if sync:
293
+ cmd.waitUntilCompleted()
294
+ return cmd
295
+
296
+
297
+ # =============================================================================
298
+ # mul_mat_vec_q4_K — batch-1 decode (memory-bandwidth-bound regime).
299
+ # =============================================================================
300
+
301
+ _N_DST = 4
302
+ _SIMD_WIDTH = 32
303
+
304
+
305
+ class MatVecQ4KPlan:
306
+ __slots__ = ("M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
307
+
308
+ def __init__(self, M, K, a_buf, b_buf, c_buf, dims_bytes):
309
+ self.M = M
310
+ self.K = K
311
+ self.a_buf = a_buf
312
+ self.b_buf = b_buf
313
+ self.c_buf = c_buf
314
+ self._dims_bytes = dims_bytes
315
+
316
+
317
+ def allocate_matvec_q4_K_plan(M: int, K: int) -> MatVecQ4KPlan:
318
+ import Metal
319
+
320
+ _ensure_metal()
321
+ assert M % _N_DST == 0, f"M must be a multiple of {_N_DST}, got {M}"
322
+ assert K % 256 == 0, f"K must be a multiple of 256, got {K}"
323
+
324
+ a_bytes = M * (K // 256) * 144
325
+ b_bytes = K * 4
326
+ c_bytes = M * 4
327
+
328
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
329
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
330
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
331
+
332
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
333
+ return MatVecQ4KPlan(M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
334
+
335
+
336
+ def dispatch_matvec_q4_K(plan: MatVecQ4KPlan, sync: bool = True):
337
+ import Metal
338
+
339
+ pso = _pipeline("mul_mat_vec_q4_K_f32")
340
+ cmd = _COMMAND_QUEUE.commandBuffer()
341
+ enc = cmd.computeCommandEncoder()
342
+ enc.setComputePipelineState_(pso)
343
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
344
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
345
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
346
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3) # ne00 = K
347
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4) # ne01 = M
348
+
349
+ grid_x = (plan.M + _N_DST - 1) // _N_DST
350
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
351
+ Metal.MTLSizeMake(grid_x, 1, 1),
352
+ Metal.MTLSizeMake(_SIMD_WIDTH, 1, 1),
353
+ )
354
+ enc.endEncoding()
355
+ cmd.commit()
356
+ if sync:
357
+ cmd.waitUntilCompleted()
358
+ return cmd
359
+
360
+
361
+ # =============================================================================
362
+ # Q4_0 / Q8_0 matmul + matvec — same layout as Q4_K, smaller blocks (32 elems).
363
+ # =============================================================================
364
+
365
+ _N_SIMDGROUP = 2 # Q4_0/Q8_0 matvec uses 2 simdgroups per threadgroup
366
+
367
+ # Per quant type for the mat path: (block bytes, block elems, matvec_M_alignment).
368
+ # block_elems = 32 for Q4_0/Q8_0/IQ4_NL, 256 for all K-quants and IQ4_XS.
369
+ # matvec_M_alignment:
370
+ # Q4_0/Q8_0 → 2 simdgroups × N_DST(4) = 8 rows per threadgroup
371
+ # Q4_K → 1 simdgroup × N_DST(4) = 4 rows per threadgroup
372
+ # Q5_K → 2 simdgroups × 2 rows = 4 rows per threadgroup
373
+ # Q6_K → 2 simdgroups × 1 row = 2 rows per threadgroup
374
+ _MAT_FORMATS = {
375
+ "q4_0": (18, 32, 8),
376
+ "q8_0": (34, 32, 8),
377
+ "q4_K": (144, 256, 4),
378
+ "q5_K": (176, 256, 4),
379
+ "q6_K": (210, 256, 2),
380
+ "iq4_nl": (18, 32, 4), # 2 sg × 2 rows/sg = 4 rows/threadgroup
381
+ "iq4_xs": (136, 256, 4),
382
+ }
383
+
384
+
385
+ class MatMulPlan:
386
+ """Pre-allocated I/O buffers for `mul_mat_<fmt>_f32`. Block element size
387
+ depends on the format (32 for Q4_0/Q8_0, 256 for Q4_K)."""
388
+
389
+ __slots__ = ("fmt", "M", "N", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
390
+
391
+ def __init__(self, fmt, M, N, K, a_buf, b_buf, c_buf, dims_bytes):
392
+ self.fmt = fmt
393
+ self.M = M
394
+ self.N = N
395
+ self.K = K
396
+ self.a_buf = a_buf
397
+ self.b_buf = b_buf
398
+ self.c_buf = c_buf
399
+ self._dims_bytes = dims_bytes
400
+
401
+
402
+ def _quant_block_elems(fmt: str) -> int:
403
+ return _MAT_FORMATS[fmt][1]
404
+
405
+
406
+ def _quant_block_bytes(fmt: str) -> int:
407
+ return _MAT_FORMATS[fmt][0]
408
+
409
+
410
+ def _matvec_row_alignment(fmt: str) -> int | None:
411
+ return _MAT_FORMATS[fmt][2]
412
+
413
+
414
+ def allocate_matmul_plan(fmt: str, M: int, N: int, K: int) -> MatMulPlan:
415
+ import Metal
416
+
417
+ _ensure_metal()
418
+ assert fmt in _MAT_FORMATS, f"unsupported fmt {fmt}"
419
+ block_elems = _quant_block_elems(fmt)
420
+ block_bytes = _quant_block_bytes(fmt)
421
+ assert M % _BLOCK_SIZE_M == 0, f"M must be multiple of {_BLOCK_SIZE_M}, got {M}"
422
+ assert N % _BLOCK_SIZE_N == 0, f"N must be multiple of {_BLOCK_SIZE_N}, got {N}"
423
+ assert K % block_elems == 0, f"K must be multiple of {block_elems} for {fmt}, got {K}"
424
+
425
+ a_bytes = M * (K // block_elems) * block_bytes
426
+ b_bytes = N * K * 4
427
+ c_bytes = M * N * 4
428
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
429
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
430
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
431
+
432
+ nb01 = (K // block_elems) * block_bytes
433
+ nb11 = K * 4
434
+ dims = (
435
+ np.array([K], dtype=np.int64).tobytes()
436
+ + np.array([M], dtype=np.int64).tobytes()
437
+ + np.array([N], dtype=np.int64).tobytes()
438
+ + np.array([nb01], dtype=np.uint64).tobytes()
439
+ + np.array([nb11], dtype=np.uint64).tobytes()
440
+ )
441
+ return MatMulPlan(fmt=fmt, M=M, N=N, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
442
+
443
+
444
+ def dispatch_matmul(plan: MatMulPlan, sync: bool = True):
445
+ import Metal
446
+
447
+ pso = _pipeline(f"mul_mat_{plan.fmt}_f32")
448
+ cmd = _COMMAND_QUEUE.commandBuffer()
449
+ enc = cmd.computeCommandEncoder()
450
+ enc.setComputePipelineState_(pso)
451
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
452
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
453
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
454
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
455
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
456
+ enc.setBytes_length_atIndex_(plan._dims_bytes[16:24], 8, 5)
457
+ enc.setBytes_length_atIndex_(plan._dims_bytes[24:32], 8, 6)
458
+ enc.setBytes_length_atIndex_(plan._dims_bytes[32:40], 8, 7)
459
+ enc.setThreadgroupMemoryLength_atIndex_(_SHARED_BYTES, 0)
460
+
461
+ grid_x = (plan.N + _BLOCK_SIZE_N - 1) // _BLOCK_SIZE_N
462
+ grid_y = (plan.M + _BLOCK_SIZE_M - 1) // _BLOCK_SIZE_M
463
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
464
+ Metal.MTLSizeMake(grid_x, grid_y, 1),
465
+ Metal.MTLSizeMake(_THREAD_PER_BLOCK, 1, 1),
466
+ )
467
+ enc.endEncoding()
468
+ cmd.commit()
469
+ if sync:
470
+ cmd.waitUntilCompleted()
471
+ return cmd
472
+
473
+
474
+ class MatVecPlan:
475
+ __slots__ = ("fmt", "M", "K", "a_buf", "b_buf", "c_buf", "_dims_bytes")
476
+
477
+ def __init__(self, fmt, M, K, a_buf, b_buf, c_buf, dims_bytes):
478
+ self.fmt = fmt
479
+ self.M = M
480
+ self.K = K
481
+ self.a_buf = a_buf
482
+ self.b_buf = b_buf
483
+ self.c_buf = c_buf
484
+ self._dims_bytes = dims_bytes
485
+
486
+
487
+ def allocate_matvec_plan(fmt: str, M: int, K: int) -> MatVecPlan:
488
+ """Allocate I/O for `mul_mat_vec_<fmt>_f32`. Format-specific row alignment:
489
+ Q4_0/Q8_0: 8, Q4_K: 4, Q5_K: 4, Q6_K: 2."""
490
+ import Metal
491
+
492
+ _ensure_metal()
493
+ block_elems = _quant_block_elems(fmt)
494
+ block_bytes = _quant_block_bytes(fmt)
495
+ align = _matvec_row_alignment(fmt)
496
+ if align is None:
497
+ raise ValueError(f"mul_mat_vec not implemented for {fmt}")
498
+ assert M % align == 0, f"M must be a multiple of {align} for {fmt}, got {M}"
499
+ assert K % block_elems == 0, f"K must be a multiple of {block_elems} for {fmt}, got {K}"
500
+
501
+ a_bytes = M * (K // block_elems) * block_bytes
502
+ b_bytes = K * 4
503
+ c_bytes = M * 4
504
+ a_buf = _DEVICE.newBufferWithLength_options_(a_bytes, Metal.MTLResourceStorageModeShared)
505
+ b_buf = _DEVICE.newBufferWithLength_options_(b_bytes, Metal.MTLResourceStorageModeShared)
506
+ c_buf = _DEVICE.newBufferWithLength_options_(c_bytes, Metal.MTLResourceStorageModeShared)
507
+ dims = np.array([K], dtype=np.int64).tobytes() + np.array([M], dtype=np.int64).tobytes()
508
+ return MatVecPlan(fmt=fmt, M=M, K=K, a_buf=a_buf, b_buf=b_buf, c_buf=c_buf, dims_bytes=dims)
509
+
510
+
511
+ def encode_matvec(plan: MatVecPlan, enc):
512
+ """Encode a single matvec dispatch onto an existing MTLComputeCommandEncoder.
513
+ Caller is responsible for committing the parent command buffer. Threadgroup
514
+ memory for the IQ4 lookup table is set via setThreadgroupMemoryLength here."""
515
+ import Metal
516
+
517
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
518
+ enc.setComputePipelineState_(pso)
519
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
520
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
521
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
522
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
523
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
524
+
525
+ if plan.fmt == "q4_K":
526
+ rows_per_tg = _N_DST
527
+ tg_threads = _SIMD_WIDTH
528
+ elif plan.fmt in ("q4_0", "q8_0"):
529
+ rows_per_tg = _N_SIMDGROUP * _N_DST
530
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
531
+ elif plan.fmt == "q5_K":
532
+ rows_per_tg = _N_SIMDGROUP * 2
533
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
534
+ elif plan.fmt == "q6_K":
535
+ rows_per_tg = _N_SIMDGROUP * 1
536
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
537
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
538
+ rows_per_tg = _N_SIMDGROUP * 2
539
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
540
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0)
541
+ else:
542
+ raise ValueError(f"matvec not wired for {plan.fmt}")
543
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
544
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
545
+ Metal.MTLSizeMake(grid_x, 1, 1),
546
+ Metal.MTLSizeMake(tg_threads, 1, 1),
547
+ )
548
+
549
+
550
+ def new_command_buffer():
551
+ """Start a fresh MTLCommandBuffer. Caller batches `encode_matvec(plan, enc)`
552
+ onto its encoder, then calls `cb.commit()` and `cb.waitUntilCompleted()`
553
+ once for the whole batch — matching llama.cpp's per-step semantics."""
554
+ _ensure_metal()
555
+ return _COMMAND_QUEUE.commandBuffer()
556
+
557
+
558
+ def dispatch_matvec(plan: MatVecPlan, sync: bool = True):
559
+ import Metal
560
+
561
+ pso = _pipeline(f"mul_mat_vec_{plan.fmt}_f32")
562
+ cmd = _COMMAND_QUEUE.commandBuffer()
563
+ enc = cmd.computeCommandEncoder()
564
+ enc.setComputePipelineState_(pso)
565
+ enc.setBuffer_offset_atIndex_(plan.a_buf, 0, 0)
566
+ enc.setBuffer_offset_atIndex_(plan.b_buf, 0, 1)
567
+ enc.setBuffer_offset_atIndex_(plan.c_buf, 0, 2)
568
+ enc.setBytes_length_atIndex_(plan._dims_bytes[0:8], 8, 3)
569
+ enc.setBytes_length_atIndex_(plan._dims_bytes[8:16], 8, 4)
570
+
571
+ needs_shared = plan.fmt in ("iq4_nl", "iq4_xs")
572
+ if plan.fmt == "q4_K":
573
+ rows_per_tg = _N_DST
574
+ tg_threads = _SIMD_WIDTH
575
+ elif plan.fmt in ("q4_0", "q8_0"):
576
+ rows_per_tg = _N_SIMDGROUP * _N_DST
577
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
578
+ elif plan.fmt == "q5_K":
579
+ rows_per_tg = _N_SIMDGROUP * 2
580
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
581
+ elif plan.fmt == "q6_K":
582
+ rows_per_tg = _N_SIMDGROUP * 1
583
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
584
+ elif plan.fmt in ("iq4_nl", "iq4_xs"):
585
+ rows_per_tg = _N_SIMDGROUP * 2 # 2 sg × 2 rows/sg
586
+ tg_threads = _N_SIMDGROUP * _SIMD_WIDTH
587
+ else:
588
+ raise ValueError(f"matvec not wired for {plan.fmt}")
589
+ grid_x = (plan.M + rows_per_tg - 1) // rows_per_tg
590
+ if needs_shared:
591
+ enc.setThreadgroupMemoryLength_atIndex_(256, 0) # 64 floats per simdgroup × 2 sg
592
+ enc.dispatchThreadgroups_threadsPerThreadgroup_(
593
+ Metal.MTLSizeMake(grid_x, 1, 1),
594
+ Metal.MTLSizeMake(tg_threads, 1, 1),
595
+ )
596
+ enc.endEncoding()
597
+ cmd.commit()
598
+ if sync:
599
+ cmd.waitUntilCompleted()
600
+ return cmd
build/torch29-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2345a3efd557e8757ae50488e360d5bbf79e5163c658a497f918d2e9727082a0
3
+ size 236288
build/torch29-metal-aarch64-darwin/_ops.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from . import _gguf_dequant_530e374
3
+ ops = torch.ops._gguf_dequant_530e374
4
+
5
+ def add_op_namespace_prefix(op_name: str):
6
+ """
7
+ Prefix op by namespace.
8
+ """
9
+ return f"_gguf_dequant_530e374::{op_name}"
build/torch29-metal-aarch64-darwin/gguf_dequant/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import sys
3
+
4
+ import importlib
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch29-metal-aarch64-darwin/metadata.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "python-depends": []
4
+ }
build/torch29-metal-aarch64-darwin/nn.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GgufLinear — drop-in nn.Linear replacement that runs quantized matmul directly.
2
+
3
+ Forward pass:
4
+ - If the input has effective batch size 1 (single token), use mul_mat_vec
5
+ (memory-bandwidth-bound, faster than bf16 mat-vec on MPS).
6
+ - Otherwise use mul_mat (compute-bound, similar throughput to bf16 GEMM but
7
+ keeps weights at native quantization → 3–4× less RAM than bf16).
8
+
9
+ Supported quant types today: Q4_0, Q8_0, Q4_K. Adding more is just a matter of
10
+ porting the candle kernels — the dispatch logic is generic over `quant_type`.
11
+
12
+ Prototype design: weights are stored as raw uint8 bytes in `qweight`. The
13
+ forward path copies activations into a shared MTLBuffer, runs the Metal
14
+ kernel, reads the fp32 output back. Production path (kernels-community +
15
+ PR #767) skips the copies via direct MTLBuffer handoff.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import Optional
20
+
21
+ import numpy as np
22
+ import torch
23
+
24
+ from . import (
25
+ allocate_matmul_plan,
26
+ allocate_matvec_plan,
27
+ dispatch_matmul,
28
+ dispatch_matvec,
29
+ )
30
+
31
+
32
+ # Per-quant-type geometry: (block_bytes, block_elems, matvec_M_alignment)
33
+ _QUANT_INFO = {
34
+ "Q4_0": (18, 32, 8), # matvec uses 2 simdgroups × N_DST=4 = 8 rows/threadgroup
35
+ "Q8_0": (34, 32, 8),
36
+ "Q4_K": (144, 256, 4), # matvec uses 1 simdgroup × N_DST=4 = 4 rows/threadgroup
37
+ }
38
+
39
+ # Map our Python-side quant name to the Metal kernel suffix.
40
+ _KERNEL_FMT = {
41
+ "Q4_0": "q4_0",
42
+ "Q8_0": "q8_0",
43
+ "Q4_K": "q4_K",
44
+ }
45
+
46
+
47
+ class GgufLinear(torch.nn.Module):
48
+ """Linear layer backed by GGUF-quantized weights.
49
+
50
+ Args:
51
+ in_features: K dimension. Must be a multiple of the format's block size
52
+ (32 for Q4_0/Q8_0, 256 for Q4_K).
53
+ out_features: M dimension. Must be a multiple of the format's matvec
54
+ row alignment (8 for Q4_0/Q8_0, 4 for Q4_K) and of 64 for matmul.
55
+ quant_type: one of "Q4_0", "Q8_0", "Q4_K".
56
+ bias: whether to add a learnable bias after the matmul.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ in_features: int,
62
+ out_features: int,
63
+ quant_type: str = "Q4_K",
64
+ bias: bool = False,
65
+ device: Optional[torch.device | str] = None,
66
+ ):
67
+ super().__init__()
68
+ if quant_type not in _QUANT_INFO:
69
+ raise NotImplementedError(
70
+ f"GgufLinear supports {sorted(_QUANT_INFO)} today, got {quant_type}"
71
+ )
72
+ block_bytes, block_elems, matvec_align = _QUANT_INFO[quant_type]
73
+ if in_features % block_elems != 0:
74
+ raise ValueError(
75
+ f"in_features must be a multiple of {block_elems} for {quant_type}, got {in_features}"
76
+ )
77
+ if out_features % matvec_align != 0:
78
+ raise ValueError(
79
+ f"out_features must be a multiple of {matvec_align} for {quant_type}, got {out_features}"
80
+ )
81
+
82
+ self.in_features = in_features
83
+ self.out_features = out_features
84
+ self.quant_type = quant_type
85
+ self._fmt = _KERNEL_FMT[quant_type]
86
+ self._block_bytes = block_bytes
87
+ self._block_elems = block_elems
88
+
89
+ nblocks_per_row = in_features // block_elems
90
+ qweight_nbytes = out_features * nblocks_per_row * block_bytes
91
+ self.register_buffer(
92
+ "qweight",
93
+ torch.empty(qweight_nbytes, dtype=torch.uint8, device=device or "cpu"),
94
+ persistent=True,
95
+ )
96
+
97
+ if bias:
98
+ self.bias = torch.nn.Parameter(
99
+ torch.zeros(out_features, dtype=torch.float32, device=device or "cpu"),
100
+ requires_grad=False,
101
+ )
102
+ else:
103
+ self.register_parameter("bias", None)
104
+
105
+ def extra_repr(self) -> str:
106
+ return (
107
+ f"in_features={self.in_features}, out_features={self.out_features}, "
108
+ f"quant_type={self.quant_type}, bias={self.bias is not None}"
109
+ )
110
+
111
+ @torch.no_grad()
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ """x: shape (..., K). Returns (..., M) fp32."""
114
+ if x.shape[-1] != self.in_features:
115
+ raise ValueError(f"Last dim of x must be {self.in_features}, got {x.shape[-1]}")
116
+
117
+ batch_shape = x.shape[:-1]
118
+ x_flat = x.reshape(-1, self.in_features).contiguous().to(torch.float32)
119
+ N = x_flat.shape[0]
120
+
121
+ # CPU view of qweight for the bytes copy into shared MTLBuffers.
122
+ # Production path skips this once the kernels-community build is wired.
123
+ qweight_cpu = self.qweight.detach().to("cpu", copy=False)
124
+ x_cpu = x_flat.to("cpu", copy=False)
125
+
126
+ if N == 1:
127
+ out = self._forward_matvec(qweight_cpu, x_cpu.reshape(-1))
128
+ else:
129
+ out = self._forward_matmul(qweight_cpu, x_cpu)
130
+
131
+ out = out.reshape(*batch_shape, self.out_features)
132
+ if x.device != out.device:
133
+ out = out.to(x.device)
134
+ if self.bias is not None:
135
+ out = out + self.bias.to(out.device).to(out.dtype)
136
+ return out
137
+
138
+ def _forward_matvec(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
139
+ M, K = self.out_features, self.in_features
140
+ plan = allocate_matvec_plan(self._fmt, M=M, K=K)
141
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
142
+ a_view[:] = qweight_cpu.numpy()
143
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(K * 4), dtype=np.float32)
144
+ b_view[:] = x_cpu.numpy()
145
+ dispatch_matvec(plan, sync=True)
146
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * 4), dtype=np.float32)
147
+ return torch.from_numpy(c_view.copy())
148
+
149
+ def _forward_matmul(self, qweight_cpu: torch.Tensor, x_cpu: torch.Tensor) -> torch.Tensor:
150
+ N, K = x_cpu.shape
151
+ M = self.out_features
152
+ plan = allocate_matmul_plan(self._fmt, M=M, N=N, K=K)
153
+ a_view = np.frombuffer(plan.a_buf.contents().as_buffer(qweight_cpu.numel()), dtype=np.uint8)
154
+ a_view[:] = qweight_cpu.numpy()
155
+ b_view = np.frombuffer(plan.b_buf.contents().as_buffer(N * K * 4), dtype=np.float32)
156
+ b_view[:] = x_cpu.numpy().reshape(-1)
157
+ dispatch_matmul(plan, sync=True)
158
+ c_view = np.frombuffer(plan.c_buf.contents().as_buffer(M * N * 4), dtype=np.float32)
159
+ # Kernel output layout is (N, M) in row-major; we want (N, M).
160
+ return torch.from_numpy(c_view.reshape(N, M).copy())
161
+
162
+ @classmethod
163
+ def from_quantized_bytes(
164
+ cls,
165
+ quant_type: str,
166
+ qweight_bytes: bytes,
167
+ in_features: int,
168
+ out_features: int,
169
+ bias: Optional[torch.Tensor] = None,
170
+ device: Optional[torch.device | str] = None,
171
+ ) -> "GgufLinear":
172
+ """Convenience for tests/loaders: hydrate from raw GGUF block bytes."""
173
+ layer = cls(in_features=in_features, out_features=out_features,
174
+ quant_type=quant_type, bias=bias is not None, device=device)
175
+ expected = layer.qweight.numel()
176
+ if len(qweight_bytes) != expected:
177
+ raise ValueError(
178
+ f"qweight_bytes length {len(qweight_bytes)} != expected {expected} "
179
+ f"for {quant_type} M={out_features} K={in_features}"
180
+ )
181
+ layer.qweight.copy_(torch.frombuffer(bytearray(qweight_bytes), dtype=torch.uint8))
182
+ if bias is not None:
183
+ layer.bias.copy_(bias.to(torch.float32))
184
+ return layer
185
+
186
+ # Back-compat shim for the existing Q4_K-only test
187
+ @classmethod
188
+ def from_q4_K_bytes(cls, qweight_bytes, in_features, out_features, bias=None, device=None):
189
+ return cls.from_quantized_bytes(
190
+ "Q4_K", qweight_bytes, in_features, out_features, bias=bias, device=device
191
+ )
build/torch29-metal-aarch64-darwin/transformers_glue.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Glue for plugging `GgufLinear` into a transformers model.
2
+
3
+ The flow is intentionally non-invasive: we read raw block bytes directly from
4
+ the GGUF file via `gguf.GGUFReader`, walk the model, and for every `nn.Linear`
5
+ whose corresponding GGUF tensor is one of our supported quant types we
6
+ in-place replace the module with `GgufLinear` and copy the bytes into
7
+ `qweight`. Tensors that don't map to a Linear (or are unsupported quants like
8
+ F32 norms, embeddings) get dequantized via `gguf.dequantize` and copied into
9
+ the existing parameter as fp32.
10
+
11
+ This avoids modifying the transformers loader. The trade-off is that the
12
+ model has to be constructed first with regular `nn.Linear` shells (which we
13
+ then mutate), but those shells are on the meta device or freshly initialised
14
+ and don't carry the dequantized weight at the time of swap — so peak memory
15
+ during load is bounded by `(model_size_fp32) + (model_size_quantized_qweights)`,
16
+ which fits in RAM for the small models we care about.
17
+
18
+ Typical use:
19
+
20
+ from transformers import AutoConfig, AutoModelForCausalLM
21
+ cfg = AutoConfig.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct-GGUF",
22
+ gguf_file="qwen2.5-0.5b-instruct-q4_0.gguf")
23
+ model = AutoModelForCausalLM.from_config(cfg).eval()
24
+ load_gguf_into_model_with_gguf_linear(
25
+ model,
26
+ gguf_path="…/qwen2.5-0.5b-instruct-q4_0.gguf",
27
+ )
28
+ out = model.generate(input_ids=…, max_new_tokens=10)
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+ import gguf
36
+ import numpy as np
37
+ import torch
38
+ import torch.nn as nn
39
+
40
+ from .nn import GgufLinear, _QUANT_INFO
41
+
42
+
43
+ # Map gguf.GGMLQuantizationType.name → our GgufLinear quant_type string.
44
+ _GGUF_NAME_TO_GGUF_LINEAR = {
45
+ "Q4_0": "Q4_0",
46
+ "Q8_0": "Q8_0",
47
+ "Q4_K": "Q4_K",
48
+ }
49
+
50
+
51
+ def _quant_supported_by_gguf_linear(qt: gguf.GGMLQuantizationType) -> bool:
52
+ return qt.name in _GGUF_NAME_TO_GGUF_LINEAR
53
+
54
+
55
+ # --- Tensor-name mapping ------------------------------------------------------
56
+ # GGUF tensors use llama.cpp-style names like `blk.0.attn_q.weight`. HF models
57
+ # use `model.layers.0.self_attn.q_proj.weight`. We use a tiny version of the
58
+ # transformers rename table here so the swap util can stay self-contained.
59
+ # (Transformers' own GGUF loader has a much more thorough version of this.)
60
+
61
+ _BASIC_RENAMES = [
62
+ (r"^blk\.", "model.layers."),
63
+ (r"\.attn_q\.weight$", ".self_attn.q_proj.weight"),
64
+ (r"\.attn_k\.weight$", ".self_attn.k_proj.weight"),
65
+ (r"\.attn_v\.weight$", ".self_attn.v_proj.weight"),
66
+ (r"\.attn_output\.weight$", ".self_attn.o_proj.weight"),
67
+ (r"\.ffn_gate\.weight$", ".mlp.gate_proj.weight"),
68
+ (r"\.ffn_up\.weight$", ".mlp.up_proj.weight"),
69
+ (r"\.ffn_down\.weight$", ".mlp.down_proj.weight"),
70
+ (r"\.attn_norm\.weight$", ".input_layernorm.weight"),
71
+ (r"\.ffn_norm\.weight$", ".post_attention_layernorm.weight"),
72
+ (r"\.attn_(q|k|v)\.bias$", r".self_attn.\1_proj.bias"),
73
+ (r"^token_embd\.weight$", "model.embed_tokens.weight"),
74
+ (r"^output_norm\.weight$", "model.norm.weight"),
75
+ (r"^output\.weight$", "lm_head.weight"),
76
+ ]
77
+
78
+
79
+ def _gguf_name_to_hf_name(gguf_name: str) -> str:
80
+ """Apply our minimal rename table to map a GGUF tensor name to an HF parameter name."""
81
+ import re
82
+
83
+ out = gguf_name
84
+ for pat, repl in _BASIC_RENAMES:
85
+ out = re.sub(pat, repl, out)
86
+ return out
87
+
88
+
89
+ def _is_attn_qk(gguf_name: str) -> bool:
90
+ return ".attn_q.weight" in gguf_name or ".attn_k.weight" in gguf_name
91
+
92
+
93
+ def _reverse_permute_qk(w: np.ndarray, n_head: int, n_kv: int | None) -> np.ndarray:
94
+ """GGUF Q/K weights are permuted on quantize; undo before loading into HF.
95
+ Reference: https://github.com/ggerganov/llama.cpp/blob/.../convert_hf_to_gguf.py."""
96
+ if n_kv is not None and n_head != n_kv:
97
+ n_head = n_kv
98
+ dim = w.shape[0] // n_head // 2
99
+ w = w.reshape(n_head, dim, 2, *w.shape[1:])
100
+ return w.swapaxes(2, 1).reshape(*w.shape[:1], *w.shape[3:]) if False else w.reshape(-1, *w.shape[3:]).swapaxes(2, 1).reshape(n_head * dim * 2, *w.shape[3:])
101
+
102
+
103
+ def _set_module_param(model: nn.Module, qualified_name: str, value: torch.Tensor) -> bool:
104
+ """Set a parameter or buffer on `model` by dotted name. Returns True on success."""
105
+ parts = qualified_name.split(".")
106
+ mod = model
107
+ for p in parts[:-1]:
108
+ if not hasattr(mod, p):
109
+ return False
110
+ mod = getattr(mod, p)
111
+ leaf = parts[-1]
112
+ if leaf in mod._parameters:
113
+ param = mod._parameters[leaf]
114
+ if param is None:
115
+ return False
116
+ with torch.no_grad():
117
+ mod._parameters[leaf] = nn.Parameter(value.to(dtype=param.dtype, device=param.device),
118
+ requires_grad=param.requires_grad)
119
+ return True
120
+ if leaf in mod._buffers:
121
+ with torch.no_grad():
122
+ mod._buffers[leaf] = value
123
+ return True
124
+ return False
125
+
126
+
127
+ def _get_module_by_name(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
128
+ """Return the leaf module matching `qualified_name` minus its terminal attribute,
129
+ or None if anything along the path is missing."""
130
+ parts = qualified_name.split(".")
131
+ mod = model
132
+ for p in parts[:-1]:
133
+ if not hasattr(mod, p):
134
+ return None
135
+ mod = getattr(mod, p)
136
+ return mod
137
+
138
+
139
+ def _get_attr_by_path(model: nn.Module, qualified_name: str) -> Optional[nn.Module]:
140
+ """Walk dotted `qualified_name` on `model` and return the final attribute,
141
+ or None if anything along the path is missing."""
142
+ if not qualified_name:
143
+ return model
144
+ mod = model
145
+ for p in qualified_name.split("."):
146
+ if not hasattr(mod, p):
147
+ return None
148
+ mod = getattr(mod, p)
149
+ return mod
150
+
151
+
152
+ def _swap_linear_to_gguf(
153
+ parent: nn.Module, attr: str, in_features: int, out_features: int, quant_type: str,
154
+ bias_tensor: Optional[torch.Tensor], device: Optional[torch.device | str] = None
155
+ ) -> GgufLinear:
156
+ """Replace `parent.attr` (an nn.Linear) with a new GgufLinear of the same shape."""
157
+ new = GgufLinear(in_features=in_features, out_features=out_features,
158
+ quant_type=quant_type, bias=bias_tensor is not None, device=device)
159
+ if bias_tensor is not None and new.bias is not None:
160
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
161
+ setattr(parent, attr, new)
162
+ return new
163
+
164
+
165
+ def load_gguf_into_model_with_gguf_linear(
166
+ model: nn.Module,
167
+ gguf_path: str | Path,
168
+ skip_modules: Optional[set[str]] = None,
169
+ force_dequant: bool = False,
170
+ ) -> dict:
171
+ """Read a GGUF file and populate `model` in-place.
172
+
173
+ Two-phase load to keep the byte path simple and the bias path correct:
174
+
175
+ Phase 1 — full dequant: every tensor (linear weights, norms, embeddings,
176
+ biases) is dequantized to fp32 and written into the matching HF
177
+ parameter. After this pass the model would already produce correct
178
+ outputs (this is what `force_dequant=True` stops at).
179
+ Phase 2 — swap linears: walk every `nn.Linear` that maps to a tensor
180
+ whose GGUF quant type is supported by `GgufLinear`; swap it for a
181
+ `GgufLinear` and re-attach raw block bytes (preserving the bias that
182
+ Phase 1 just loaded).
183
+
184
+ Returns a dict with diagnostic counts.
185
+ """
186
+ reader = gguf.GGUFReader(str(gguf_path))
187
+
188
+ # Pull a few config bits for the Q/K reverse-permute.
189
+ def _read_int(field: str, default: int = 0) -> int:
190
+ f = reader.fields.get(field)
191
+ if f is None:
192
+ return default
193
+ return int(f.parts[f.data[0]][0]) if f.data else default
194
+
195
+ arch = ""
196
+ arch_field = reader.fields.get("general.architecture")
197
+ if arch_field is not None and arch_field.data:
198
+ arch = bytes(arch_field.parts[arch_field.data[0]]).decode("utf-8", errors="ignore")
199
+ n_head = _read_int(f"{arch}.attention.head_count")
200
+ n_kv_head = _read_int(f"{arch}.attention.head_count_kv") or n_head
201
+
202
+ skip_modules = skip_modules or set()
203
+ stats = {"as_gguf_linear": 0, "dequantized": 0, "skipped": 0, "unmatched": []}
204
+
205
+ # Track which (hf_name → quant_type) pairs are eligible for GgufLinear swap
206
+ # after Phase 1. We'll re-quantize the loaded fp32 weight in Phase 2.
207
+ swap_candidates: list[tuple[str, str]] = []
208
+
209
+ # ---------------- Phase 1: dequant everything into the model. -------------
210
+ for t in reader.tensors:
211
+ hf_name = _gguf_name_to_hf_name(t.name)
212
+ if any(skip in hf_name for skip in skip_modules):
213
+ stats["skipped"] += 1
214
+ continue
215
+
216
+ parent = _get_module_by_name(model, hf_name)
217
+ if parent is None or not hasattr(parent, hf_name.rsplit(".", 1)[-1]):
218
+ stats["unmatched"].append(t.name)
219
+ continue
220
+ leaf = hf_name.rsplit(".", 1)[-1]
221
+ cur = getattr(parent, leaf)
222
+
223
+ fp32 = gguf.dequantize(
224
+ np.frombuffer(np.ascontiguousarray(t.data).tobytes(), dtype=np.uint8),
225
+ t.tensor_type,
226
+ ).astype(np.float32)
227
+ if _is_attn_qk(t.name) and n_head > 0:
228
+ shape = list(t.shape)
229
+ M, K = int(shape[1]), int(shape[0])
230
+ heads = n_head if ".attn_q." in t.name else n_kv_head
231
+ head_dim = M // heads
232
+ fp32 = fp32.reshape(M, K).reshape(heads, head_dim // 2, 2, K).swapaxes(2, 1).reshape(M, K)
233
+ target_shape = tuple(cur.shape) if hasattr(cur, "shape") else (fp32.size,)
234
+ if np.prod(target_shape) == fp32.size:
235
+ fp32 = fp32.reshape(target_shape)
236
+ tensor = torch.from_numpy(fp32)
237
+ if not _set_module_param(model, hf_name, tensor):
238
+ stats["unmatched"].append(t.name)
239
+ continue
240
+ stats["dequantized"] += 1
241
+
242
+ # Record candidacy for swap in Phase 2.
243
+ is_linear_weight = isinstance(parent, nn.Linear) and leaf == "weight"
244
+ if is_linear_weight and _quant_supported_by_gguf_linear(t.tensor_type) and not force_dequant:
245
+ swap_candidates.append((hf_name, _GGUF_NAME_TO_GGUF_LINEAR[t.tensor_type.name]))
246
+
247
+ # ---------------- Phase 2: swap Linear → GgufLinear. ----------------------
248
+ # Re-quantize the loaded fp32 weight via gguf-py (works for Q4_0/Q8_0;
249
+ # K-quants don't have a Python quantizer yet — those will skip swap).
250
+ for hf_name, quant_type in swap_candidates:
251
+ # hf_name ends in `.weight`; the Linear module is at hf_name[:-len(".weight")]
252
+ linear_path = hf_name[: -len(".weight")]
253
+ grandparent_path, _, linear_attr = linear_path.rpartition(".")
254
+ grandparent = model if not grandparent_path else _get_attr_by_path(model, grandparent_path)
255
+ linear = _get_attr_by_path(model, linear_path)
256
+ if grandparent is None or not isinstance(linear, nn.Linear):
257
+ continue
258
+
259
+ # Re-quantize the (now permuted-if-applicable) fp32 weight.
260
+ try:
261
+ qt = getattr(gguf.GGMLQuantizationType, quant_type)
262
+ qbytes = bytes(
263
+ gguf.quantize(linear.weight.detach().cpu().numpy().astype(np.float32), qt).tobytes()
264
+ )
265
+ except (NotImplementedError, Exception) as e:
266
+ # K-quants etc. — leave as Linear (fp32 weight is already loaded).
267
+ stats.setdefault("re_quantize_failed", []).append((hf_name, str(e)))
268
+ continue
269
+
270
+ bias_tensor = linear.bias.detach().clone() if linear.bias is not None else None
271
+ new = GgufLinear(
272
+ in_features=linear.in_features,
273
+ out_features=linear.out_features,
274
+ quant_type=quant_type,
275
+ bias=bias_tensor is not None,
276
+ )
277
+ if bias_tensor is not None:
278
+ new.bias.data.copy_(bias_tensor.to(torch.float32))
279
+ new.qweight.copy_(torch.frombuffer(bytearray(qbytes), dtype=torch.uint8))
280
+ setattr(grandparent, linear_attr, new)
281
+ stats["as_gguf_linear"] += 1
282
+ stats["dequantized"] -= 1 # this one's no longer "just" dequant — swapped
283
+
284
+ return stats