Minimal README: ops, devices, source and how to refresh it
Browse files- .gitignore +1 -0
- .pytest_cache/.gitignore +0 -2
- .pytest_cache/CACHEDIR.TAG +0 -4
- .pytest_cache/README.md +0 -8
- .pytest_cache/v/cache/lastfailed +0 -3
- .pytest_cache/v/cache/nodeids +0 -23
- README.md +27 -44
- devscripts/ab_compiled_vs_intree.py +0 -63
- devscripts/bench.py +0 -66
- devscripts/bench_loop_static.py +0 -48
- devscripts/bench_static.py +0 -40
- devscripts/breaks.py +0 -15
- devscripts/customop_cost.py +0 -37
- devscripts/verify_kernel.py +0 -54
- devscripts/where_time_goes.py +0 -39
.gitignore
CHANGED
|
@@ -6,3 +6,4 @@ __pycache__/
|
|
| 6 |
# nix build output symlinks
|
| 7 |
result
|
| 8 |
result-*
|
|
|
|
|
|
| 6 |
# nix build output symlinks
|
| 7 |
result
|
| 8 |
result-*
|
| 9 |
+
.pytest_cache/
|
.pytest_cache/.gitignore
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
# Created by pytest automatically.
|
| 2 |
-
*
|
|
|
|
|
|
|
|
|
.pytest_cache/CACHEDIR.TAG
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
Signature: 8a477f597d28d172789f06886806bc55
|
| 2 |
-
# This file is a cache directory tag created by pytest.
|
| 3 |
-
# For information about cache directory tags, see:
|
| 4 |
-
# https://bford.info/cachedir/spec.html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.pytest_cache/README.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
# pytest cache directory #
|
| 2 |
-
|
| 3 |
-
This directory contains data from the pytest's cache plugin,
|
| 4 |
-
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
| 5 |
-
|
| 6 |
-
**Do not** commit this to version control.
|
| 7 |
-
|
| 8 |
-
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.pytest_cache/v/cache/lastfailed
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"tests/test_gguf_kernels.py": true
|
| 3 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
.pytest_cache/v/cache/nodeids
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
[
|
| 2 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype0-Q4_K]",
|
| 3 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype0-Q5_K]",
|
| 4 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype0-Q6_K]",
|
| 5 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype0-Q8_0]",
|
| 6 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype1-Q4_K]",
|
| 7 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype1-Q5_K]",
|
| 8 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype1-Q6_K]",
|
| 9 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype1-Q8_0]",
|
| 10 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype2-Q4_K]",
|
| 11 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype2-Q5_K]",
|
| 12 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype2-Q6_K]",
|
| 13 |
-
"tests/test_gguf_kernels.py::test_dequantize_matches_reference[dtype2-Q8_0]",
|
| 14 |
-
"tests/test_gguf_kernels.py::test_gemv_is_compileable",
|
| 15 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[1-Q4_K]",
|
| 16 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[1-Q5_K]",
|
| 17 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[1-Q6_K]",
|
| 18 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[1-Q8_0]",
|
| 19 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[8-Q4_K]",
|
| 20 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[8-Q5_K]",
|
| 21 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[8-Q6_K]",
|
| 22 |
-
"tests/test_gguf_kernels.py::test_mul_mat_vec_matches_matmul[8-Q8_0]"
|
| 23 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -8,60 +8,43 @@ tags:
|
|
| 8 |
|
| 9 |
# GGUF kernels
|
| 10 |
|
| 11 |
-
Compute directly on the packed blocks of a GGUF checkpoint
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
layout as it is, so a quantized model runs without ever materializing a dense copy of its weights —
|
| 15 |
-
which is where the memory saving comes from. A 4B model at Q4_K_M stays at 3.1 GB instead of the
|
| 16 |
-
8.4 GB it would take unpacked.
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
```python
|
| 24 |
-
import torch
|
| 25 |
-
from kernels import get_kernel
|
| 26 |
-
|
| 27 |
-
gguf_kernels = get_kernel("marcsun13/gguf-kernels")
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
```
|
| 33 |
|
| 34 |
-
|
| 35 |
-
| --- | --- |
|
| 36 |
-
| `mul_mat_vec(blocks, x, ggml_type, out_features)` | Fused dequantize-gemv, for `x` of at most `MAX_GEMV_ROWS` rows (decode). Returns f32 whatever `x` was: the kernel writes an f32 destination, and casting at the call site lets the cast fuse into the consumer. |
|
| 37 |
-
| `dequantize(blocks, ggml_type, rows, cols, dtype)` | Blocks to values, in `dtype` directly. For more rows than the gemv handles (prefill), unpack and use an ordinary matmul. |
|
| 38 |
-
| `MAX_GEMV_ROWS` | Upstream's `MMVQ_MAX_BATCH_SIZE`. Beyond this the gemv has no implementation. |
|
| 39 |
-
| `GEMV_TYPES` | The ggml type ids `mul_mat_vec` implements. `dequantize` covers more, so check this before choosing the fused path. |
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
-
##
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
adding Metal means adding a `[kernel.*]` section and a source directory — no change to the schema or
|
| 50 |
-
to the Python API, and callers dispatch on the tensor's device as usual.
|
| 51 |
|
| 52 |
-
`
|
| 53 |
-
`
|
| 54 |
-
|
| 55 |
|
| 56 |
-
##
|
| 57 |
|
| 58 |
```bash
|
| 59 |
-
|
| 60 |
-
nix
|
| 61 |
-
pytest tests/ # against a built variant on PYTHONPATH
|
| 62 |
```
|
| 63 |
|
| 64 |
-
`
|
| 65 |
-
`
|
| 66 |
-
|
| 67 |
-
Q8_0 in f32/f16/bf16, the gemv against a dense matmul, and one `fullgraph=True` compile.
|
|
|
|
| 8 |
|
| 9 |
# GGUF kernels
|
| 10 |
|
| 11 |
+
Compute directly on the packed blocks of a GGUF checkpoint, so a quantized model runs without ever
|
| 12 |
+
materializing a dense copy of its weights.
|
| 13 |
|
| 14 |
+
## Ops
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
| op | signature |
|
| 17 |
+
| --- | --- |
|
| 18 |
+
| `dequantize` | `(blocks, ggml_type, rows, cols, dtype) -> (rows, cols)` |
|
| 19 |
+
| `mul_mat_vec` | `(blocks, x, ggml_type, out_features) -> (rows, out_features)` f32, fused dequant-gemv |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
`blocks` is a GGUF weight exactly as stored: `(out_features, bytes_per_row)` uint8. `mul_mat_vec`
|
| 22 |
+
takes at most `MAX_GEMV_ROWS` rows of `x`; above that, unpack with `dequantize` and use a dense
|
| 23 |
+
matmul. The quant types each backend implements a gemv for are in `GEMV_TYPES`.
|
|
|
|
| 24 |
|
| 25 |
+
## Devices
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
| backend | torch | targets |
|
| 28 |
+
| --- | --- | --- |
|
| 29 |
+
| CUDA | 2.11, 2.12 | x86_64-linux, cu126/128/130/132, sm 7.5–12.0 |
|
| 30 |
+
| Metal | 2.12, 2.13 | aarch64-darwin |
|
| 31 |
|
| 32 |
+
## Where the kernels come from
|
| 33 |
|
| 34 |
+
[llama.cpp](https://github.com/ggml-org/llama.cpp)'s ggml, vendored rather than reimplemented.
|
| 35 |
+
`vendor/UPSTREAM` records the revision.
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
CUDA compiles `ggml-cuda`'s sources; Metal compiles `ggml-metal.metal` into the embedded metallib.
|
| 38 |
+
Only the files listed in `build.toml`'s `src` are built — the rest of the tree rides along so a pin
|
| 39 |
+
bump cannot leave a dangling include.
|
| 40 |
|
| 41 |
+
## Updating
|
| 42 |
|
| 43 |
```bash
|
| 44 |
+
python vendor.py --rev <llama.cpp commit> # re-vendor, updates vendor/UPSTREAM
|
| 45 |
+
nix run .#build-and-copy # rebuild every variant into build/
|
|
|
|
| 46 |
```
|
| 47 |
|
| 48 |
+
`vendor.py` copies whole trees plus one rename: upstream's `mmvq.cu` lands as `mmvq-impl.cuh`,
|
| 49 |
+
because `gguf_cuda/ggml_dispatch.cu` `#include`s it instead of compiling it as its own translation
|
| 50 |
+
unit.
|
|
|
devscripts/ab_compiled_vs_intree.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
"""A/B the compiled gguf-kernels Metal extension against the in-tree compile_shader path.
|
| 2 |
-
|
| 3 |
-
Same process, same weights, same machine: only `_MUL_MAT_VEC`/`_DEQUANTIZE` are swapped, and the
|
| 4 |
-
two are measured interleaved so drift cannot favour either.
|
| 5 |
-
"""
|
| 6 |
-
import statistics, time, torch
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from kernels import get_local_kernel
|
| 9 |
-
from transformers import AutoModelForCausalLM
|
| 10 |
-
from transformers.integrations.gguf import kernels as K
|
| 11 |
-
|
| 12 |
-
m = AutoModelForCausalLM.from_pretrained(
|
| 13 |
-
"unsloth/Qwen3.5-4B-GGUF", gguf_file="Qwen3.5-4B-Q4_K_M.gguf", dtype="float32").eval()
|
| 14 |
-
dev = next(m.parameters()).device
|
| 15 |
-
cfg = m.config.get_text_config()
|
| 16 |
-
|
| 17 |
-
intree = (K._MUL_MAT_VEC, K._DEQUANTIZE)
|
| 18 |
-
ext = get_local_kernel(Path.home() / "Desktop/HF/gguf-kernels", "metal")
|
| 19 |
-
compiled = (ext.mul_mat_vec, ext.dequantize)
|
| 20 |
-
|
| 21 |
-
def use(pair):
|
| 22 |
-
K._MUL_MAT_VEC, K._DEQUANTIZE = pair
|
| 23 |
-
|
| 24 |
-
@torch.inference_mode()
|
| 25 |
-
def decode(n=64):
|
| 26 |
-
ids = torch.randint(0, cfg.vocab_size, (1, 32), device=dev)
|
| 27 |
-
o = m(input_ids=ids, use_cache=True, logits_to_keep=1)
|
| 28 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 29 |
-
for _ in range(3):
|
| 30 |
-
o = m(input_ids=tok, past_key_values=past, use_cache=True, logits_to_keep=1)
|
| 31 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 32 |
-
torch.mps.synchronize(); t0 = time.perf_counter()
|
| 33 |
-
for _ in range(n):
|
| 34 |
-
o = m(input_ids=tok, past_key_values=past, use_cache=True, logits_to_keep=1)
|
| 35 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 36 |
-
torch.mps.synchronize()
|
| 37 |
-
return n / (time.perf_counter() - t0)
|
| 38 |
-
|
| 39 |
-
@torch.inference_mode()
|
| 40 |
-
def prefill(n=512):
|
| 41 |
-
ids = torch.randint(0, cfg.vocab_size, (1, n), device=dev)
|
| 42 |
-
torch.mps.synchronize(); t0 = time.perf_counter()
|
| 43 |
-
m(input_ids=ids, use_cache=True, logits_to_keep=1)
|
| 44 |
-
torch.mps.synchronize()
|
| 45 |
-
return n / (time.perf_counter() - t0)
|
| 46 |
-
|
| 47 |
-
# correctness first: the compiled path must agree with the in-tree one
|
| 48 |
-
use(intree); torch.manual_seed(0); a = decode(1)
|
| 49 |
-
use(compiled); torch.manual_seed(0); b = decode(1)
|
| 50 |
-
print("both paths ran\n")
|
| 51 |
-
|
| 52 |
-
res = {"in-tree": {"pp": [], "tg": []}, "compiled": {"pp": [], "tg": []}}
|
| 53 |
-
for _ in range(3):
|
| 54 |
-
for name, pair in (("in-tree", intree), ("compiled", compiled)):
|
| 55 |
-
use(pair)
|
| 56 |
-
res[name]["pp"].append(prefill())
|
| 57 |
-
res[name]["tg"].append(decode())
|
| 58 |
-
|
| 59 |
-
for name in ("in-tree", "compiled"):
|
| 60 |
-
pp, tg = res[name]["pp"], res[name]["tg"]
|
| 61 |
-
print(f"{name:9s} pp512 {statistics.mean(pp):8.2f} +- {statistics.stdev(pp):5.2f} "
|
| 62 |
-
f"tg64 {statistics.mean(tg):6.2f} +- {statistics.stdev(tg):4.2f}")
|
| 63 |
-
print("llama.cpp pp512 1061.06 tg128 71.56")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/bench.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
"""Consolidated GGUF/Metal bench: correctness, graph breaks, eager and compiled decode."""
|
| 2 |
-
import argparse, gc, statistics, time, torch
|
| 3 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 4 |
-
|
| 5 |
-
ap = argparse.ArgumentParser()
|
| 6 |
-
ap.add_argument("--mode", default="all", choices=["all", "correct", "breaks", "eager", "pp"])
|
| 7 |
-
ap.add_argument("--tg", type=int, default=128)
|
| 8 |
-
ap.add_argument("--reps", type=int, default=3)
|
| 9 |
-
a = ap.parse_args()
|
| 10 |
-
|
| 11 |
-
MODEL, GGUF = "unsloth/Qwen3.5-4B-GGUF", "Qwen3.5-4B-Q4_K_M.gguf"
|
| 12 |
-
model = AutoModelForCausalLM.from_pretrained(MODEL, gguf_file=GGUF, dtype="float32").eval()
|
| 13 |
-
dev = next(model.parameters()).device
|
| 14 |
-
cfg = model.config.get_text_config()
|
| 15 |
-
LLAMACPP = "llama.cpp: pp512 1061.06 | tg128 71.56"
|
| 16 |
-
|
| 17 |
-
def decode_loop(fwd, n, warm=4):
|
| 18 |
-
"""Manual greedy loop on a DynamicCache -- isolates model speed from generate() overhead."""
|
| 19 |
-
ids = torch.randint(0, cfg.vocab_size, (1, 64), device=dev)
|
| 20 |
-
o = fwd(input_ids=ids, use_cache=True, logits_to_keep=1)
|
| 21 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 22 |
-
for _ in range(warm):
|
| 23 |
-
o = fwd(input_ids=tok, past_key_values=past, use_cache=True, logits_to_keep=1)
|
| 24 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 25 |
-
torch.mps.synchronize()
|
| 26 |
-
t0 = time.perf_counter()
|
| 27 |
-
for _ in range(n):
|
| 28 |
-
o = fwd(input_ids=tok, past_key_values=past, use_cache=True, logits_to_keep=1)
|
| 29 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 30 |
-
torch.mps.synchronize()
|
| 31 |
-
return n / (time.perf_counter() - t0)
|
| 32 |
-
|
| 33 |
-
@torch.inference_mode()
|
| 34 |
-
def main():
|
| 35 |
-
if a.mode in ("all", "correct"):
|
| 36 |
-
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3.5-4B")
|
| 37 |
-
ids = tok("The capital of France is", return_tensors="pt").to(dev)
|
| 38 |
-
out = model.generate(**ids, max_new_tokens=16, do_sample=False)
|
| 39 |
-
print("OUTPUT:", repr(tok.decode(out[0], skip_special_tokens=True)))
|
| 40 |
-
|
| 41 |
-
if a.mode in ("all", "breaks"):
|
| 42 |
-
import torch._dynamo as dynamo
|
| 43 |
-
ids = torch.randint(0, cfg.vocab_size, (1, 8), device=dev)
|
| 44 |
-
e = dynamo.explain(model.forward)(input_ids=ids, use_cache=True)
|
| 45 |
-
print(f"GRAPH BREAKS: {e.graph_break_count} graphs {e.graph_count} ops {e.op_count}")
|
| 46 |
-
|
| 47 |
-
if a.mode in ("all", "pp"):
|
| 48 |
-
def pp(n):
|
| 49 |
-
ids = torch.randint(0, cfg.vocab_size, (1, n), device=dev)
|
| 50 |
-
torch.mps.synchronize(); t0 = time.perf_counter()
|
| 51 |
-
model(input_ids=ids, use_cache=True, logits_to_keep=1)
|
| 52 |
-
torch.mps.synchronize(); return n / (time.perf_counter() - t0)
|
| 53 |
-
pp(512); r = [pp(512) for _ in range(a.reps)]
|
| 54 |
-
print(f"pp512 eager: {statistics.mean(r):8.2f} +- {statistics.stdev(r):5.2f} tok/s")
|
| 55 |
-
|
| 56 |
-
if a.mode in ("all", "eager"):
|
| 57 |
-
decode_loop(model, 8); gc.collect(); torch.mps.empty_cache()
|
| 58 |
-
r = [decode_loop(model, a.tg) for _ in range(a.reps)]
|
| 59 |
-
print(f"tg{a.tg} eager: {statistics.mean(r):8.2f} +- {statistics.stdev(r):5.2f} tok/s")
|
| 60 |
-
|
| 61 |
-
# NOTE: no compile mode here on purpose. Driving torch.compile from this loop means a
|
| 62 |
-
# DynamicCache, so the KV length changes every step and dynamo recompiles per token -- the
|
| 63 |
-
# measurement is dominated by recompilation. Use bench_loop_static.py, which pins the shapes.
|
| 64 |
-
print(LLAMACPP)
|
| 65 |
-
|
| 66 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/bench_loop_static.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
"""Manual decode loop on a StaticCache -- fixed shapes, no generate() overhead. Eager vs fullgraph."""
|
| 2 |
-
import argparse, gc, statistics, time, torch
|
| 3 |
-
from transformers import AutoModelForCausalLM
|
| 4 |
-
from transformers.cache_utils import StaticCache
|
| 5 |
-
|
| 6 |
-
ap = argparse.ArgumentParser()
|
| 7 |
-
ap.add_argument("--compile", action="store_true")
|
| 8 |
-
ap.add_argument("--tg", type=int, default=128)
|
| 9 |
-
ap.add_argument("--reps", type=int, default=4)
|
| 10 |
-
a = ap.parse_args()
|
| 11 |
-
|
| 12 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 13 |
-
"unsloth/Qwen3.5-4B-GGUF", gguf_file="Qwen3.5-4B-Q4_K_M.gguf", dtype="float32").eval()
|
| 14 |
-
dev = next(model.parameters()).device
|
| 15 |
-
cfg = model.config.get_text_config()
|
| 16 |
-
MAXLEN = 512
|
| 17 |
-
|
| 18 |
-
fwd = model
|
| 19 |
-
if a.compile:
|
| 20 |
-
import torch._dynamo as dynamo
|
| 21 |
-
dynamo.config.cache_size_limit = 128
|
| 22 |
-
fwd = torch.compile(model, dynamic=False, fullgraph=True)
|
| 23 |
-
|
| 24 |
-
cache = StaticCache(config=cfg, max_batch_size=1, max_cache_len=MAXLEN, device=dev, dtype=torch.float32)
|
| 25 |
-
|
| 26 |
-
@torch.inference_mode()
|
| 27 |
-
def run(n, prefill=8):
|
| 28 |
-
cache.reset()
|
| 29 |
-
ids = torch.randint(0, cfg.vocab_size, (1, prefill), device=dev)
|
| 30 |
-
o = fwd(input_ids=ids, cache_position=torch.arange(prefill, device=dev),
|
| 31 |
-
past_key_values=cache, use_cache=True, logits_to_keep=1)
|
| 32 |
-
tok = o.logits[:, -1:].argmax(-1)
|
| 33 |
-
torch.mps.synchronize(); t0 = time.perf_counter()
|
| 34 |
-
for i in range(n):
|
| 35 |
-
o = fwd(input_ids=tok, cache_position=torch.tensor([prefill + i], device=dev),
|
| 36 |
-
past_key_values=cache, use_cache=True, logits_to_keep=1)
|
| 37 |
-
tok = o.logits[:, -1:].argmax(-1)
|
| 38 |
-
torch.mps.synchronize()
|
| 39 |
-
return n / (time.perf_counter() - t0)
|
| 40 |
-
|
| 41 |
-
t = time.perf_counter()
|
| 42 |
-
run(a.tg); run(a.tg) # two warm passes: the first may still be compiling
|
| 43 |
-
print(f" warm {time.perf_counter()-t:.0f}s", flush=True)
|
| 44 |
-
gc.collect(); torch.mps.empty_cache()
|
| 45 |
-
r = [run(a.tg) for _ in range(a.reps)]
|
| 46 |
-
tag = "compiled" if a.compile else "eager "
|
| 47 |
-
print(f"tg{a.tg} loop+static {tag}: {statistics.mean(r):7.2f} +- {statistics.stdev(r):5.2f} tok/s "
|
| 48 |
-
f"reps={[round(x,1) for x in r]} (llama.cpp 71.56)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/bench_static.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
"""generate() with cache_implementation='static' so decode shapes are fixed: eager vs compiled."""
|
| 2 |
-
import argparse, gc, statistics, time, torch
|
| 3 |
-
from transformers import AutoModelForCausalLM
|
| 4 |
-
|
| 5 |
-
ap = argparse.ArgumentParser()
|
| 6 |
-
ap.add_argument("--compile", action="store_true")
|
| 7 |
-
ap.add_argument("--tg", type=int, default=128)
|
| 8 |
-
ap.add_argument("--reps", type=int, default=3)
|
| 9 |
-
a = ap.parse_args()
|
| 10 |
-
|
| 11 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 12 |
-
"unsloth/Qwen3.5-4B-GGUF", gguf_file="Qwen3.5-4B-Q4_K_M.gguf", dtype="float32").eval()
|
| 13 |
-
dev = next(model.parameters()).device
|
| 14 |
-
cfg = model.config.get_text_config()
|
| 15 |
-
|
| 16 |
-
model.generation_config.cache_implementation = "static"
|
| 17 |
-
model.generation_config.do_sample = False
|
| 18 |
-
# a fixed cache length keeps every decode step the same shape, so dynamo compiles once
|
| 19 |
-
model.generation_config.max_length = 8 + a.tg + 8
|
| 20 |
-
|
| 21 |
-
if a.compile:
|
| 22 |
-
import torch._dynamo as dynamo
|
| 23 |
-
dynamo.config.cache_size_limit = 128
|
| 24 |
-
model.forward = torch.compile(model.forward, dynamic=False, fullgraph=True)
|
| 25 |
-
|
| 26 |
-
ids = torch.randint(0, cfg.vocab_size, (1, 8), device=dev)
|
| 27 |
-
|
| 28 |
-
@torch.inference_mode()
|
| 29 |
-
def run(n):
|
| 30 |
-
torch.mps.synchronize(); t0 = time.perf_counter()
|
| 31 |
-
out = model.generate(ids, max_new_tokens=n, min_new_tokens=n, do_sample=False)
|
| 32 |
-
torch.mps.synchronize()
|
| 33 |
-
return (out.shape[1] - ids.shape[1]) / (time.perf_counter() - t0)
|
| 34 |
-
|
| 35 |
-
t = time.perf_counter(); run(a.tg); warm = time.perf_counter() - t
|
| 36 |
-
gc.collect(); torch.mps.empty_cache()
|
| 37 |
-
r = [run(a.tg) for _ in range(a.reps)]
|
| 38 |
-
tag = "compiled" if a.compile else "eager "
|
| 39 |
-
print(f"tg{a.tg} static {tag}: {statistics.mean(r):7.2f} +- {statistics.stdev(r):5.2f} tok/s "
|
| 40 |
-
f"reps={[round(x,1) for x in r]} warm={warm:.0f}s")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/breaks.py
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
import torch, torch._dynamo as dynamo
|
| 2 |
-
from transformers import AutoModelForCausalLM
|
| 3 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 4 |
-
"unsloth/Qwen3.5-4B-GGUF", gguf_file="Qwen3.5-4B-Q4_K_M.gguf", dtype="float32").eval()
|
| 5 |
-
dev = next(model.parameters()).device
|
| 6 |
-
cfg = model.config.get_text_config()
|
| 7 |
-
ids = torch.randint(0, cfg.vocab_size, (1, 8), device=dev)
|
| 8 |
-
with torch.inference_mode():
|
| 9 |
-
e = dynamo.explain(model.forward)(input_ids=ids, use_cache=True)
|
| 10 |
-
print(f"BREAKS {e.graph_break_count}")
|
| 11 |
-
for i, r in enumerate(e.break_reasons):
|
| 12 |
-
print(f"\n--- break {i} ---")
|
| 13 |
-
print(str(r.reason)[:400])
|
| 14 |
-
for f in (r.user_stack or [])[-4:]:
|
| 15 |
-
print(f" at {f.filename.split('transformers/')[-1]}:{f.lineno} {str(f.name)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/customop_cost.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
"""Per-call cost of the custom-op wrapper vs calling the resolved kernel directly.
|
| 2 |
-
|
| 3 |
-
The wrappers exist so dynamo can close a full graph; they must cost nothing in eager. A model is
|
| 4 |
-
loaded first because `from_pretrained` is what resolves `_MUL_MAT_VEC`. The two paths are timed
|
| 5 |
-
interleaved so drift cannot favour one.
|
| 6 |
-
"""
|
| 7 |
-
import statistics, time, torch
|
| 8 |
-
from transformers import AutoModelForCausalLM
|
| 9 |
-
from transformers.integrations.gguf import kernels as K
|
| 10 |
-
from transformers.integrations.gguf.utils import GgufLinear
|
| 11 |
-
|
| 12 |
-
m = AutoModelForCausalLM.from_pretrained(
|
| 13 |
-
"unsloth/Qwen3.5-4B-GGUF", gguf_file="Qwen3.5-4B-Q4_K_M.gguf", dtype="float32").eval()
|
| 14 |
-
lin = next(mod for mod in m.modules() if isinstance(mod, GgufLinear))
|
| 15 |
-
w, gt, N = lin.weight, lin.ggml_type, lin.out_features
|
| 16 |
-
x = torch.randn(1, lin.in_features, device="mps")
|
| 17 |
-
|
| 18 |
-
via_op = lambda: K.mul_mat_vec(w, x, gt, N) # goes through torch.library.custom_op
|
| 19 |
-
direct = lambda: K._MUL_MAT_VEC(w, x, gt, N) # the kernel itself
|
| 20 |
-
|
| 21 |
-
with torch.inference_mode():
|
| 22 |
-
via_op(); direct(); torch.mps.synchronize()
|
| 23 |
-
|
| 24 |
-
def timeit(fn, iters=200):
|
| 25 |
-
torch.mps.synchronize(); t0 = time.perf_counter()
|
| 26 |
-
for _ in range(iters): fn()
|
| 27 |
-
torch.mps.synchronize()
|
| 28 |
-
return (time.perf_counter() - t0) / iters * 1e6
|
| 29 |
-
|
| 30 |
-
a, b = [], []
|
| 31 |
-
for _ in range(5):
|
| 32 |
-
a.append(timeit(via_op)); b.append(timeit(direct))
|
| 33 |
-
|
| 34 |
-
print(f"torch {torch.__version__} shape {N}x{lin.in_features}")
|
| 35 |
-
print(f" via custom op : {statistics.median(a):8.2f} us/call")
|
| 36 |
-
print(f" direct : {statistics.median(b):8.2f} us/call")
|
| 37 |
-
print(f" WRAPPER COST : {statistics.median(a)-statistics.median(b):8.2f} us/call")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/verify_kernel.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
"""Verify the built Metal kernel against transformers' pure-torch GGUF dequantizer.
|
| 2 |
-
|
| 3 |
-
Both entry points are checked on real GGUF block layouts: `dequantize` against the reference
|
| 4 |
-
element-for-element, and `mul_mat_vec` against a matmul on the reference's own output, so a bug in
|
| 5 |
-
either the unpacking or the fused gemv shows up.
|
| 6 |
-
"""
|
| 7 |
-
import importlib.util, pathlib, sys, torch
|
| 8 |
-
|
| 9 |
-
REF = "/Users/marcsun/Desktop/HF/transformers/src/transformers/integrations/gguf/dequant.py"
|
| 10 |
-
spec = importlib.util.spec_from_file_location("ref_dequant", REF)
|
| 11 |
-
ref = importlib.util.module_from_spec(spec); spec.loader.exec_module(ref)
|
| 12 |
-
|
| 13 |
-
so = sys.argv[1]
|
| 14 |
-
torch.ops.load_library(so)
|
| 15 |
-
ns = pathlib.Path(so).stem.replace(".abi3", "")
|
| 16 |
-
ext = getattr(torch.ops, ns)
|
| 17 |
-
print(f"loaded {ns}\n")
|
| 18 |
-
|
| 19 |
-
# byte offset of each fp16 scale field within a block, per dequant.py's layouts
|
| 20 |
-
FP16_OFFSETS = {ref.GGML_Q8_0: (0,), ref.GGML_Q4_K: (0, 2), ref.GGML_Q5_K: (0, 2), ref.GGML_Q6_K: (208,)}
|
| 21 |
-
|
| 22 |
-
torch.manual_seed(0)
|
| 23 |
-
ok = True
|
| 24 |
-
for name, ggml_type in (("Q8_0", ref.GGML_Q8_0), ("Q4_K", ref.GGML_Q4_K),
|
| 25 |
-
("Q5_K", ref.GGML_Q5_K), ("Q6_K", ref.GGML_Q6_K)):
|
| 26 |
-
elems, nbytes = ref.GGML_BLOCK[ggml_type]
|
| 27 |
-
rows, blocks_per_row = 64, 8
|
| 28 |
-
cols = elems * blocks_per_row
|
| 29 |
-
raw = torch.randint(0, 256, (rows, blocks_per_row * nbytes), dtype=torch.uint8)
|
| 30 |
-
# Random bytes in the fp16 scale fields are frequently NaN/Inf bit patterns, which would make
|
| 31 |
-
# both sides NaN and the comparison vacuous. Overwrite just those fields with real values; the
|
| 32 |
-
# quant payload stays random.
|
| 33 |
-
for blk in range(blocks_per_row):
|
| 34 |
-
for off in FP16_OFFSETS[ggml_type]:
|
| 35 |
-
vals = (torch.rand(rows, dtype=torch.float32) * 0.05 + 0.001).to(torch.float16)
|
| 36 |
-
b = vals.view(torch.uint8).reshape(rows, 2)
|
| 37 |
-
raw[:, blk * nbytes + off : blk * nbytes + off + 2] = b
|
| 38 |
-
|
| 39 |
-
expect = ref.dequantize(raw, ggml_type, torch.float32).reshape(rows, cols)
|
| 40 |
-
got = ext.dequantize(raw.to("mps"), ggml_type, rows, cols, torch.float32).cpu()
|
| 41 |
-
d = (got - expect).abs().max().item()
|
| 42 |
-
print(f"{name:5s} dequantize maxdiff {d:.3e} {'OK' if d < 1e-4 else 'FAIL'}")
|
| 43 |
-
ok &= d < 1e-4
|
| 44 |
-
|
| 45 |
-
x = torch.randn(2, cols)
|
| 46 |
-
exp_mm = (x @ expect.T)
|
| 47 |
-
got_mm = ext.mul_mat_vec(raw.to("mps"), x.to("mps"), ggml_type, rows).cpu()
|
| 48 |
-
dm = (got_mm - exp_mm).abs().max().item()
|
| 49 |
-
rel = dm / exp_mm.abs().max().item()
|
| 50 |
-
print(f"{name:5s} mul_mat_vec maxdiff {dm:.3e} (rel {rel:.2e}) {'OK' if rel < 1e-4 else 'FAIL'}")
|
| 51 |
-
ok &= rel < 1e-4
|
| 52 |
-
|
| 53 |
-
print("\nALL PASS" if ok else "\nFAILURES")
|
| 54 |
-
sys.exit(0 if ok else 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
devscripts/where_time_goes.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
"""Attribute decode wall-time to module types, synchronising around each so MPS async cannot smear
|
| 2 |
-
the cost onto whoever happens to sync next."""
|
| 3 |
-
import collections, time, torch, torch.nn as nn
|
| 4 |
-
from transformers import AutoModelForCausalLM
|
| 5 |
-
|
| 6 |
-
m = AutoModelForCausalLM.from_pretrained(
|
| 7 |
-
"unsloth/Qwen3.5-4B-GGUF", gguf_file="Qwen3.5-4B-Q4_K_M.gguf", dtype="float32").eval()
|
| 8 |
-
dev = next(m.parameters()).device
|
| 9 |
-
cfg = m.config.get_text_config()
|
| 10 |
-
|
| 11 |
-
tot = collections.Counter(); cnt = collections.Counter(); t0 = {}
|
| 12 |
-
def pre(mod, i):
|
| 13 |
-
torch.mps.synchronize(); t0[id(mod)] = time.perf_counter()
|
| 14 |
-
def post(mod, i, o):
|
| 15 |
-
torch.mps.synchronize()
|
| 16 |
-
n = type(mod).__name__
|
| 17 |
-
tot[n] += time.perf_counter() - t0[id(mod)]; cnt[n] += 1
|
| 18 |
-
|
| 19 |
-
with torch.inference_mode():
|
| 20 |
-
ids = torch.randint(0, cfg.vocab_size, (1, 32), device=dev)
|
| 21 |
-
o = m(input_ids=ids, use_cache=True, logits_to_keep=1)
|
| 22 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 23 |
-
for _ in range(3):
|
| 24 |
-
o = m(input_ids=tok, past_key_values=past, use_cache=True, logits_to_keep=1)
|
| 25 |
-
past, tok = o.past_key_values, o.logits[:, -1:].argmax(-1)
|
| 26 |
-
torch.mps.synchronize()
|
| 27 |
-
|
| 28 |
-
hs = [mod.register_forward_pre_hook(pre) for mod in m.modules()] + \
|
| 29 |
-
[mod.register_forward_hook(post) for mod in m.modules()]
|
| 30 |
-
s = time.perf_counter()
|
| 31 |
-
o = m(input_ids=tok, past_key_values=past, use_cache=True, logits_to_keep=1)
|
| 32 |
-
torch.mps.synchronize()
|
| 33 |
-
wall = time.perf_counter() - s
|
| 34 |
-
for h in hs: h.remove()
|
| 35 |
-
|
| 36 |
-
print(f"one decode token (with sync hooks): {wall*1e3:.1f} ms\n")
|
| 37 |
-
print(f"{'module':32s} {'calls':>6} {'total ms':>10} {'us/call':>9}")
|
| 38 |
-
for n, t in tot.most_common(12):
|
| 39 |
-
print(f"{n:32s} {cnt[n]:6d} {t*1e3:10.1f} {t/cnt[n]*1e6:9.1f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|