Upload folder using huggingface_hub
Browse files- .gitattributes +4 -0
- build/torch210-metal-aarch64-darwin/__init__.py +600 -0
- build/torch210-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
- build/torch210-metal-aarch64-darwin/_ops.py +9 -0
- build/torch210-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
- build/torch210-metal-aarch64-darwin/metadata.json +4 -0
- build/torch210-metal-aarch64-darwin/nn.py +191 -0
- build/torch210-metal-aarch64-darwin/transformers_glue.py +284 -0
- build/torch211-metal-aarch64-darwin/__init__.py +600 -0
- build/torch211-metal-aarch64-darwin/__pycache__/__init__.cpython-313.pyc +0 -0
- build/torch211-metal-aarch64-darwin/__pycache__/_ops.cpython-313.pyc +0 -0
- build/torch211-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
- build/torch211-metal-aarch64-darwin/_ops.py +9 -0
- build/torch211-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
- build/torch211-metal-aarch64-darwin/metadata.json +4 -0
- build/torch211-metal-aarch64-darwin/nn.py +191 -0
- build/torch211-metal-aarch64-darwin/transformers_glue.py +284 -0
- build/torch28-metal-aarch64-darwin/__init__.py +600 -0
- build/torch28-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
- build/torch28-metal-aarch64-darwin/_ops.py +9 -0
- build/torch28-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
- build/torch28-metal-aarch64-darwin/metadata.json +4 -0
- build/torch28-metal-aarch64-darwin/nn.py +191 -0
- build/torch28-metal-aarch64-darwin/transformers_glue.py +284 -0
- build/torch29-metal-aarch64-darwin/__init__.py +600 -0
- build/torch29-metal-aarch64-darwin/_gguf_dequant_530e374.abi3.so +3 -0
- build/torch29-metal-aarch64-darwin/_ops.py +9 -0
- build/torch29-metal-aarch64-darwin/gguf_dequant/__init__.py +26 -0
- build/torch29-metal-aarch64-darwin/metadata.json +4 -0
- build/torch29-metal-aarch64-darwin/nn.py +191 -0
- 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
|