marcsun13 HF Staff commited on
Commit
49187c6
·
1 Parent(s): 2113014

Encode on the MPS stream's own queue

Browse files

torch owns the command buffer from its serial dispatch queue. Asking for the
encoder on the calling thread races with torch committing that buffer, and
encoding into a committed buffer aborts with

failed assertion _status < MTLCommandBufferStatusCommitted

which reproduces reliably during model loading, where many copies are committing
concurrently. Isolated calls never hit it, which is why the op-level tests passed.

devscripts/ab_compiled_vs_intree.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")
gguf_metal/ggml_dispatch.mm CHANGED
@@ -146,15 +146,20 @@ extern "C" int gguf_metal_mul_mat(void *blocks, size_t blocks_off, void *x, size
146
  const int32_t ne12 = 1;
147
 
148
  // torch keeps one encoder open on its command buffer and coalesces into it; opening a second is
149
- // an error. Reusing it is also what puts these dispatches in the same submission as torch's own.
 
 
 
 
150
  at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
 
 
151
  id<MTLComputeCommandEncoder> enc = stream->commandEncoder();
152
 
153
  id<MTLBuffer> b_blocks = (__bridge id<MTLBuffer>)blocks;
154
  id<MTLBuffer> b_x = (__bridge id<MTLBuffer>)x;
155
  id<MTLBuffer> b_out = (__bridge id<MTLBuffer>)out;
156
 
157
- int rc = 0;
158
  if (M <= 8) {
159
  // gemv: one token (or a handful), the decode path
160
  const int nsg = info->mv_nsg, nr0 = info->mv_nr0;
@@ -242,6 +247,7 @@ extern "C" int gguf_metal_mul_mat(void *blocks, size_t blocks_off, void *x, size
242
  threadsPerThreadgroup:MTLSizeMake(32, nsg, 1)];
243
  }
244
  }
 
245
 
246
  return rc;
247
  }
@@ -280,7 +286,9 @@ extern "C" int gguf_metal_get_rows(void *blocks, size_t blocks_off, void *indice
280
  const NSUInteger nth = MIN((NSUInteger)ne00t, max_threads);
281
  const NSUInteger nw0 = (ne00t + nth - 1) / nth;
282
 
 
283
  at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
 
284
  id<MTLComputeCommandEncoder> enc = stream->commandEncoder();
285
  [enc setComputePipelineState:pso];
286
  [enc setBytes:&args length:sizeof(args) atIndex:0];
@@ -289,5 +297,6 @@ extern "C" int gguf_metal_get_rows(void *blocks, size_t blocks_off, void *indice
289
  [enc setBuffer:(__bridge id<MTLBuffer>)out offset:out_off atIndex:3];
290
  [enc dispatchThreadgroups:MTLSizeMake(nw0 * rows, 1, 1)
291
  threadsPerThreadgroup:MTLSizeMake(nth, 1, 1)];
 
292
  return 0;
293
  }
 
146
  const int32_t ne12 = 1;
147
 
148
  // torch keeps one encoder open on its command buffer and coalesces into it; opening a second is
149
+ // an error, so its encoder is reused -- which also puts these dispatches in the same submission
150
+ // as torch's own. That buffer is owned by the stream's serial queue though, so asking for the
151
+ // encoder from the calling thread races with torch committing it, and encoding into a committed
152
+ // buffer aborts (`_status < MTLCommandBufferStatusCommitted`). Everything below therefore runs on
153
+ // that queue, which is also what torch's own extension guidance prescribes.
154
  at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
155
+ __block int rc = 0;
156
+ dispatch_sync(stream->queue(), ^{
157
  id<MTLComputeCommandEncoder> enc = stream->commandEncoder();
158
 
159
  id<MTLBuffer> b_blocks = (__bridge id<MTLBuffer>)blocks;
160
  id<MTLBuffer> b_x = (__bridge id<MTLBuffer>)x;
161
  id<MTLBuffer> b_out = (__bridge id<MTLBuffer>)out;
162
 
 
163
  if (M <= 8) {
164
  // gemv: one token (or a handful), the decode path
165
  const int nsg = info->mv_nsg, nr0 = info->mv_nr0;
 
247
  threadsPerThreadgroup:MTLSizeMake(32, nsg, 1)];
248
  }
249
  }
250
+ });
251
 
252
  return rc;
253
  }
 
286
  const NSUInteger nth = MIN((NSUInteger)ne00t, max_threads);
287
  const NSUInteger nw0 = (ne00t + nth - 1) / nth;
288
 
289
+ // Same as the matmul: encode on the stream's own queue so this cannot race torch's commits.
290
  at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
291
+ dispatch_sync(stream->queue(), ^{
292
  id<MTLComputeCommandEncoder> enc = stream->commandEncoder();
293
  [enc setComputePipelineState:pso];
294
  [enc setBytes:&args length:sizeof(args) atIndex:0];
 
297
  [enc setBuffer:(__bridge id<MTLBuffer>)out offset:out_off atIndex:3];
298
  [enc dispatchThreadgroups:MTLSizeMake(nw0 * rows, 1, 1)
299
  threadsPerThreadgroup:MTLSizeMake(nth, 1, 1)];
300
+ });
301
  return 0;
302
  }