marcsun13 HF Staff commited on
Commit
fd5d3aa
·
1 Parent(s): 3b07566

Minimal README: ops, devices, source and how to refresh it

Browse files
.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
- A GGUF weight is stored as blocks of 32 or 256 values that share a scale. These kernels read that
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
- Ported from [llama.cpp](https://github.com/ggml-org/llama.cpp)'s `ggml-cuda`. `vendor/UPSTREAM`
19
- pins the revision the sources come from; `vendor.py` refreshes them.
20
-
21
- ## Usage
22
-
23
- ```python
24
- import torch
25
- from kernels import get_kernel
26
-
27
- gguf_kernels = get_kernel("marcsun13/gguf-kernels")
28
 
29
- # `blocks` is a GGUF weight exactly as stored: (out_features, bytes_per_row) uint8
30
- out = gguf_kernels.mul_mat_vec(blocks, x, ggml_type=12, out_features=4096) # x @ blocks.T
31
- dense = gguf_kernels.dequantize(blocks, ggml_type=12, rows=4096, cols=2560, dtype=torch.bfloat16)
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
- Both ops carry `register_fake` implementations, so they trace under `torch.compile` instead of
42
- breaking the graph — which matters, since a break at every quantized linear costs more than the
43
- kernels save.
 
44
 
45
- ## Backends
46
 
47
- CUDA today. The ops are named for what they do rather than for a backend: the schema is declared
48
- once in `torch-ext/torch_binding.cpp` and each backend registers its own implementation of it, so
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
- `mmq` (quantized gemm) and `mmf` (dense gemm) are deliberately not ported: both take a
53
- `ggml_backend_cuda_context` for their pool allocator, which would pull in the whole ggml backend.
54
- Above the gemv's row limit, unpacking and using cuBLAS measures at parity with llama.cpp anyway.
55
 
56
- ## Building
57
 
58
  ```bash
59
- nix run .#build-and-copy -L # every variant, into build/
60
- nix build .#ci # just this system's variant
61
- pytest tests/ # against a built variant on PYTHONPATH
62
  ```
63
 
64
- `flake.nix` tracks the builder's default branch, so CI builds the current variant matrix. The
65
- `build/` directory here was produced against the builder release matching the `kernels` client it was
66
- verified with (torch 2.11/2.12): both ops tested against `gguf.quants.dequantize` for Q4_K/Q5_K/Q6_K/
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}")