llama.cpp Metal (MPS) End-to-End Inference Path — Reference Baseline

This document records, line-by-line, llama.cpp's end-to-end GPU inference implementation on Apple Silicon (Metal / MPS backend), as the reference starting point for comparing minfer against llama.cpp. Each row gives the execution order, purpose, and source location (file:line), plus a minfer equivalent comparison column.

Scope: the full chain from "model weight loading" to "next-token logits readback" (including batch preparation, graph build, scheduler, Metal execution, KV cache), with the Metal backend internals recorded at the finest granularity. The CPU-side sampler is not expanded (non-Metal path).

0. Version & reading conventions

  • llama.cpp baseline: master @ 8a832e4bf (2026-08-20). This revision uses the per-arch graph-build API (src/models/ + llm_graph_context); the Metal backend spans 6 files under ggml/src/ggml-metal/.
  • minfer baseline: master @ ad040ff (2026-08-20).
  • Paths are relative to each repo root (llama.cpp / minfer).
  • Comparison-column value convention:
    • Match = minfer has an equivalent implementation;
    • N/A = minfer has no such step (architectural difference);
    • Similar = functionally equivalent but structurally different (noted).
FileRole
ggml/src/ggml-metal/ggml-metal.cppMetal backend interface (buffer types, set/get tensor, graph_compute entry)
ggml/src/ggml-metal/ggml-metal-device.mLow-level MTLDevice/MTLCommandQueue, encoder, buffer allocation, kernel library loading
ggml/src/ggml-metal/ggml-metal-device.cppPipeline (kernel instance) lookup/compilation, op support table
ggml/src/ggml-metal/ggml-metal-context.mMulti-command-buffer scheduling, graph compute, tensor set/get
ggml/src/ggml-metal/ggml-metal-ops.cppPer-op encoding (encoder setup + kernel dispatch + fusion)
ggml/src/ggml-metal/ggml-metal.metalMetal kernel source
ggml/src/ggml-metal/ggml-metal-impl.hQuantized block structs, dequant functions, threadgroup constants, function-constant offsets
ggml/src/ggml-backend.cppBackend scheduler (split / alloc / compute)
src/llama-graph.cppGraph-build helpers (build_*, llm_graph_context)
src/llama-context.cppdecode / process_ubatch / graph_compute / logits readback
src/llama-kv-cache.cppKV cache (allocation, slot lookup, in-graph write/read)
src/llama-model.cpp / src/llama-model-loader.cppModel loading & weight registration
src/models/qwen2.cppQwen2 architecture graph build

1. High-level overview (12 phases)

#PhasePurposellama.cpp locationminfer equivalent
P0Backend & scheduler initCreate Metal backend, MTLCommandQueue, kernel library, scheduler; pre-reserve compute buffersggml-metal.cpp:689 ggml-metal-context.m:84 llama-context.cpp:581 ggml-backend.cpp:1792src/metal/ops.rs (MpsState::try_new)
P1Model loading / weight registrationAllocate Metal buffers per tensor and upload quantized weightsllama-model.cpp:1401 llama-model-loader.cpp:1426 ggml-metal-device.m:1631src/models/qwen2/loader.rs + src/metal/ops.rs (register_weight)
P2Batch preparation & microbatchingSplit the API batch into micro-batches, reserve host output buffersllama-context.cpp:1635 llama-batch.cpp:25src/main.rs (single batch, no split) "N/A"
P3Compute graph build (Qwen2)Build the ggml compute graph (DFS topological order)src/models/qwen2.cpp:53 llama-graph.cpp ggml.c:7188src/models/qwen2/graph.rs:438 (declarative graph build)
P4Scheduler split & allocationAssign nodes to backends, split into runs, gallocr allocationggml-backend.cpp:1936→:1055"N/A" (single MPS backend, static buffers)
P5Scheduler computePer-split: copy inputs, call backend graph_computeggml-backend.cpp:1594 ggml-metal.cpp:535forward.rs:88-134 (single CB, all layers)
P6Metal graph computeMulti-command-buffer encode (main thread + n_cb workers)ggml-metal-context.m:438 :663src/metal/ops.rs (submit, single CB)
P7Per-op encoding & concurrency/barrierFilter empty nodes, concurrency check, insert memoryBarrierggml-metal-ops.cpp:175 device.m:513src/metal/ (barrier), :327 (dispatch_2d)
P8Per-op kernel dispatchPer op type: set pipeline + args + threadgroupsggml-metal-ops.cpp:265-497src/metal/ (quant_matmul_f32_on_gpu_buf) et al.
P9GPU kernel executionMetal shader computeggml-metal.metalsrc/metal/kernels/
P10KV cacheIn-graph write (set_rows) & read (flash/matmul direct)llama-kv-cache.cpp:1301 llama-graph.cpp:2800src/metal/ops.rs (store_kv) + src/cache.rs
P11logits/embd readbackGPU→host copyggml-metal-context.m:351 llama-context.cpp:1854src/metal/runtime.rs (output_norm_gpu then download_logits, now n_out rows / 608 KB)

2. Detailed step table (end-to-end execution order)

P0 Backend & scheduler init (once per process)

#StepPurposellama.cpp locationminfer equivalent
0.1Register Metal backendConstruct backend per device, call ggml_metal_initggml-metal.cpp:689—
0.2Create struct ggml_metal contextMTLDevice + shared MTLCommandQueue, load kernel library, create concurrent dispatch queue, fusion/concurrency flagsggml-metal-context.m:84-175src/metal/ops.rs (MpsState singleton) — 2026-08-21: kernel library is now a build-time precompiled .metallib embedded in the binary (build.rs, llama's -O3 flags, newLibraryWithData), with a runtime newLibraryWithSource fallback when the toolchain is absent
0.3Init low-level deviceMTLCreateSystemDefaultDevice + newCommandQueue, probe capabilities (simdgroup_mm / unified_memory / bfloat / tensor)ggml-metal-device.m:714-760src/metal/ (new_device capability probe)
0.4tensor-API gatehas_tensor defaults to OFF for pre-M5/M6/A19/A20 (disabled on M4)ggml-metal-device.m:753-760"N/A" (llama itself doesn't use the tensor API on M4)
0.5Create schedulerggml_backend_sched_new (backend array + gallocr + events)ggml-backend.cpp:1792"N/A"
0.6Pre-reserve worst-case graphsReserve pp (prefill) and tg (decode) graphsllama-context.cpp:630-657"N/A" (static buffers)

P1 Model loading / weight registration (once per process)

#StepPurposellama.cpp locationminfer equivalent
1.1Load architecture tensorsQwen2's load_arch_tensors creates all weight tensorssrc/models/qwen2.cpp:19-47src/models/qwen2/loader.rs
1.2Per-layer device splitLayers 0..i_gpu_start-1 stay on CPU; the rest split to GPU devices by free memoryllama-model.cpp:1314-1323"N/A" (all-on-GPU or MINFER_DISABLE_MPS all-CPU)
1.3Allocate Metal buffersggml_backend_alloc_ctx_tensors_from_buft → ggml_metal_buffer_init; mmap path ggml_metal_buffer_mapllama-model.cpp:1637 ggml-metal-device.m:1631,1701src/metal/ (register_part: ONE page-aligned newBufferWithBytesNoCopy per mmap'd part) — 2026-08-21: weights are (buffer, byte-offset) into the part buffer, llama's exact design
1.4Buffer storage modeshared = newBufferWithBytesNoCopy (mmap/weights); private = newBufferWithLengthggml-metal-device.m:1668,1673src/metal/ (mmap parts StorageModeShared NoCopy; scratch/KV buffers StorageModeShared copies)
1.5Mark weight buffersGGML_BACKEND_BUFFER_USAGE_WEIGHTSllama-model.cpp:1657—
1.6Upload weight datammap direct reference; non-mmap uses blit set_tensor_asyncllama-model-loader.cpp:1548,1558 ggml-metal-context.m:3072026-08-21: zero-copy — weights are Borrowed slices of the mmap'd GGUF (Tensor.data: Cow<'static,[u8]>) wrapped by newBufferWithBytesNoCopy at the part level; no memcpy anywhere

P2 Batch preparation & microbatching (per decode)

#StepPurposellama.cpp locationminfer equivalent
2.1Init batch allocatorllama_batch_allocr::initsrc/llama-batch.cpp:25—
2.2Split micro-batchesmemory->init_batch, retry on failure (cache optimization)llama-context.cpp:1828-1856 llama-kv-cache.cpp:698"N/A" (minfer single batch; -n 0 = whole-segment prefill)
2.3Reserve host outputoutput_reserve fixed-size logits/embd buffersllama-context.cpp:2032forward.rs:141 (new logits vec each call)
2.4Microbatch loopCall process_ubatch per ubatchllama-context.cpp:1879-1900—

P3 Compute graph build (Qwen2)

#StepPurposellama.cpp locationminfer equivalent
3.1Entryllama_model::build_graph → build_arch_graphllama-model.cpp:2457src/models/qwen2/graph.rs:438 (forward)
3.2Input embdbuild_inp_embd: token ids + ggml_get_rows(tok_embd, inp_tokens)llama-graph.cpp:2284src/metal/ (embed_tokens_gpu, get_rows — 2026-08-21: all minfer quants on GPU + dispatched into the MAIN command buffer, llama-graph-style single submit, #38/#39)
3.3Position inputbuild_inp_posllama-graph.cpp:2373src/metal/ops.rs (upload_positions)
3.4KV graph inputsbuild_attn_inp_kv (k_idxs/v_idxs, mask, rotation tensors)llama-graph.cpp:2729src/metal/ (store_kv uses pos_buf)
3.5Per layer: attn_normbuild_norm (RMSNorm + Mul + optional Add)llama-graph.cpp:1556src/metal/ops.rs (rms_norm) + :648 (add)
3.6Per layer: QKVbuild_qkv (3× build_lora_mm = ggml_mul_mat for wq/wk/wv)llama-graph.cpp:1592src/metal/ (3× quant_matmul)
3.7Per layer: RoPEggml_rope_ext (once each for Q, K)src/models/qwen2.cpp:86-96src/metal/ops.rs (rope_f32 ×2)
3.8Per layer: KV writemctx_cur->cpy_k/cpy_v → ggml_set_rowsllama-graph.cpp:2800-2801 llama-kv-cache.cpp:1301,1336src/metal/ops.rs (store_kv)
3.9Per layer: attentionbuild_attn → build_attn_mha: flash path ggml_flash_attn_ext; non-flash mul_mat(k,q)+soft_max_ext+mul_mat(v,kq)llama-graph.cpp:2517,2557src/metal/ops.rs (attn_flash_prefill) / :741 (gqa_attn_f32)
3.10Per layer: wo + residualbuild_attn inner build_lora_mm(wo) + ggml_addllama-graph.cpp:2677 src/models/qwen2.cpp:110src/metal/ (wo matmul) + :648 (add)
3.11Per layer: ffn_normbuild_normsrc/models/qwen2.cpp:114rms_norm
3.12Per layer: FFNbuild_ffn: SILU-gated mul(gate,up) + mul_mat(down)llama-graph.cpp:1669src/metal/ops.rs (swiglu) + :392 (down matmul) — 2026-08-21: last layer runs on n_out rows only (llama's get_rows reduction, #34)
3.13Per layer: residualggml_addsrc/models/qwen2.cpp:127add_f32 — 2026-08-21: last layer's both residuals on the tail n_out rows (add_f32_off, #34)
3.14Output normbuild_norm (result_norm)src/models/qwen2.cpp:137src/metal/runtime.rs (rms_norm inside output_norm_gpu) — 2026-08-21: also output-rows-only (n_out), matching llama
3.15lm_headbuild_lora_mm(model.output) + optional biassrc/models/qwen2.cpp:145-150src/metal/runtime.rs (output GEMM) — 2026-08-21: now output-rows-only (n_out), matching llama
3.16Node orderingggml_build_forward_expand → ggml_build_forward_impl → ggml_visit_parents_graph (DFS, parents before children)ggml.c:7188,7120"N/A" (minfer encodes imperatively in layer order)

P4 Scheduler split & allocation

#StepPurposellama.cpp locationminfer equivalent
4.1Split graphggml_backend_sched_split_graph: 5-pass node→backend assignment (weight's backend decides MUL_MAT), build splits, insert cross-backend tensor_copyggml-backend.cpp:1055-1443"N/A" (all Metal)
4.2Allocate memoryggml_gallocr_alloc_graph (retry via reserve_n on failure)ggml-backend.cpp:1562-1585"N/A" (static buffers)

P5 Scheduler compute (per split)

#StepPurposellama.cpp locationminfer equivalent
5.1Copy split inputsCopy cross-backend srcs to the split's backend (INPUT flag → sync copy; MoE copies only used experts; else async)ggml-backend.cpp:1555-1671"N/A"
5.2Call backend computeggml_backend_graph_compute_async → Metal's ggml_backend_metal_graph_computeggml-backend.cpp:1678 ggml-metal.cpp:535forward.rs:134 (cb.submit)
5.3Event recordMTLEvent signal for multi-copy scenariosggml-backend.cpp:1717-1721"N/A"

P6 Metal graph compute (multi-command-buffer scheme)

#StepPurposellama.cpp locationminfer equivalent
6.1Split workn_main = MAX(64, 0.1*n_nodes); first n_nodes_0 nodes encoded by main thread, rest split evenly by n_cbggml-metal-context.m:445-466—
6.2Main-thread encodeCreate cmd_bufs[n_cb], enqueue, encode_async(n_cb)ggml-metal-context.m:510-523—
6.3Worker encodedispatch_apply(n_cb, d_queue, encode_async) encodes remaining CBs concurrentlyggml-metal-context.m:530-550—
6.4encode_async blockPer CB: compute node range, ggml_metal_op_init → loop ggml_metal_op_encode → ggml_metal_op_free → commitggml-metal-context.m:676-721—
6.5Async returngraph_compute returns immediately (only capture mode waits + checks status)ggml-metal-context.m:557-611src/metal/ops.rs (submit blocks + 10 s cap)
6.6Synchronizeggml_metal_synchronize: wait + check all CB statuses, set has_error on failureggml-metal-context.m:239-295submit() MTLCommandBufferStatus check

n_cb value: ggml_backend_metal_set_n_cb(backend, 1) (ggml-metal.cpp:612,707), ggml_metal_set_n_cb caps at GGML_METAL_MAX_COMMAND_BUFFERS (context.m:665). That is 2 CBs (1 main + 1 worker). minfer uses a single CB for all layers (forward.rs:76-134).

P7 Per-op encoding & concurrency/barrier model

#StepPurposellama.cpp locationminfer equivalent
7.1Create encoderggml_metal_encoder_init: MTLDispatchTypeConcurrent (when use_concurrency) or serialggml-metal-ops.cpp:42 ggml-metal-device.m:464src/metal/ops.rs (cmd_buffer: new_compute_command_encoder, serial)
7.2Filter empty nodesSkip empty / no-op nodesggml-metal-ops.cpp:55-62"N/A" (minfer has no empty-node concept)
7.3Op support checkggml_metal_device_supports_op big switchggml-metal-ops.cpp:201 ggml-metal-device.m:1086quant-type checks in layer_gpu
7.4Concurrency checkIf the current node's read/write ranges conflict with existing mem_ranges, insert memoryBarrierWithScope:MTLBarrierScopeBuffers and clear ranges; else record ranges and run concurrentlyggml-metal-ops.cpp:159-173,220-225 ggml-metal-device.m:513src/metal/ (barrier: after every dispatch) + :333
7.5Op dispatch switchDispatch to ggml_metal_op_* by node->op; returns fusion count n_fuseggml-metal-ops.cpp:265-497encode in fixed sequence inside layer_gpu

Key difference: llama uses mem_ranges for dependency-aware barriers (non-conflicting adjacent ops run concurrently in the same encoder, MTLDispatchTypeConcurrent); minfer inserts an unconditional barrier after every dispatch (dispatch_2d → barrier(), src/metal/).

P8 Per-op kernel dispatch (forward-path ops)

ggml_opllama.cpp encoderSelected kernel (variants)llama.cpp locationminfer equivalent
MUL_MATggml_metal_op_mul_mat (3-way selection, §3.2)kernel_mul_mm_* / kernel_mul_mv_ext_* / kernel_mul_mv_*ops.cpp:2299-2541src/metal/ (quant_matmul_f32_on_gpu_buf) + :352 (gemm_dispatch)
FLASH_ATTN_EXTggml_metal_op_flash_attn_ext_kv_f16 / _pad / _blk / main kernel / _vec / _vec_reduceops.cpp:2990-3492src/metal/ops.rs (attn_flash_prefill)
RMS_NORMggml_metal_op_norm (fuses Mul+Add)kernel_rms_norm_fuse_implops.cpp:3887-4006 ggml-src/metal/kernels/qkv_fused.metalsrc/metal/ops.rs (rms_norm) + separate :648 (add)
ROPEggml_metal_op_ropekernel_rope_norm/neox/multi/visionops.cpp:4025-4126 ggml-src/metal/kernels/fa_prefill.metalsrc/metal/ops.rs (rope_f32)
ADD/SUB/MUL/DIVggml_metal_op_bin (ADD fusion ×8)kernel_add / kernel_mul (n_fuse specialization)ops.cpp:3578src/metal/ops.rs (add_f32 single op)
GET_ROWSggml_metal_op_get_rowskernel_get_rows_q/_fops.cpp:1165 ggml-src/metal/kernels/src/metal/ops.rs (embed_tokens_gpu → 2026-08-21: all minfer-supported quants — Q4_0/Q4_1/Q5_0/Q5_1/Q8_0 (32-elem) + Q4_K/Q6_K/Q5_K (256-elem), matching llama's template coverage)
SET_ROWSggml_metal_op_set_rowskernel_set_rows_*ops.cpp:1210 ggml-src/metal/kernels/src/metal/ops.rs (store_kv dedicated kernel)
CPY/DUP/CONTggml_metal_op_cpykernel_cpy_t_t/_f32_q/_q_f32ops.cpp:2078 ggml-src/metal/kernels/"N/A" (f16 KV converted directly by store_kv)

P9 GPU kernel execution (key kernels)

kernelPurposellama.cpp locationminfer equivalent
kernel_mul_mm<...>simdgroup/tensor matmul (64×32 tile, §3.2 variants)ggml-src/metal/kernels/ (template + instantiations)src/metal/kernels/mul_mm.metal (kernel_q4_0_mm_f32) and 7 more mm kernels
kernel_mul_mv_*mat-vec (decode, per quant type)ggml-src/metal/kernels/fa_decode.metal (q4_0), :8498 (q4_K), etc.src/metal/kernels/ *_f32_matmul kernels
kernel_mul_mv_ext_*small-batch (ne11∈[2,8]) mat-mvggml-src/metal/kernels/fa_prefill.metal"N/A"
kernel_flash_attn_ext_kv_f16quantized KV → f16 dequant pre-pass (Q4_0/1, Q5_0/1, Q8_0)ggml-src/metal/kernels/"N/A" (minfer KV stores f32/f16 raw, MINFER_CACHE_TYPE=f16; a packed Q8_0 cache is also stored, and mechanism B is the analogous per-window dequant stage — not llama's kv_f16 variant)
kernel_flash_attn_ext_padpad pre-pass for partial KV blocksggml-src/metal/kernels/src/metal/kernels/: kernel_kv_tail_pad (equivalent)
kernel_flash_attn_ext_blkmask pre-pass (nqptg/ncpsg blocks)ggml-src/metal/kernels/inline causal mask (kernel_flash_attn_blk_f32)
kernel_flash_attn_ext / _implflash attention main kernel (half8x8)ggml-src/metal/kernels/,7184src/metal/kernels/: kernel_flash_attn_blk_f32
kernel_flash_attn_ext_vec / _vec_reducedecode small-batch flash (half4x4, ne01<20)ggml-src/metal/kernels/,7980src/metal/kernels/: kernel_flash_attn_ext_f32
kernel_rms_norm_fuse_implRMSNorm + Mul + Add fusionggml-src/metal/kernels/qkv_fused.metalrms_norm_256 + separate add
kernel_soft_max*non-flash path softmaxggml-src/metal/kernels/mul_f32act_kquant.metal,2117"N/A" (inlined in flash; or a dedicated softmax kernel)
kernel_rope_*RoPEggml-src/metal/kernels/fa_prefill.metalsrc/metal/kernels/: kernel_rope_f32
kernel_get_rows_*embedding lookupggml-src/metal/kernels/,10092src/metal/kernels/: kernel_get_rows_q4_0/q4_1/q5_0/q5_1/q8_0/q4_k/q6_k/q5_k (templates kernel_get_rows_q32/_q256)

P10 KV cache

#StepPurposellama.cpp locationminfer equivalent
10.1KV tensor creationggml_new_tensor_3d(ctx, type_k/v, n_embd_k_gqa, kv_size, n_stream), default F16llama-kv-cache.cpp:231-232src/cache.rs (GPU KV f16 auto for 7B class / f32 for small, MINFER_CACHE_TYPE overrides)
10.2Per-layer device allocationper-layer backend buft, allocate + clearllama-kv-cache.cpp:299,307src/metal/ (KV buffer allocation)
10.3Slot lookupfind_slot: ring-buffer cell range + k/v idx tensorsllama-kv-cache.cpp:894src/metal/: store_kv writes by pos_buf
10.4In-graph writecpy_k/cpy_v → ggml_set_rows (K always cache-row indexed; V per FA/non-FA layout)llama-kv-cache.cpp:1301-1389src/metal/ops.rs (store_kv, nkt/nt strides)
10.5In-graph readflash reads cache tensor directly; non-flash mul_mat(k,q)/mul_mat(v,kq)llama-graph.cpp:2807-2808,2491,2535attn_flash_prefill / gqa_attn_f32 read KV buffers directly

Layout: llama KV = f16 [nkv][nk*hd], token stride nk*hd*elem (after llama-graph.cpp permute, flash receives nb11=nk*hd*elem). minfer uses the same layout but f32 by default, f16 optional (MINFER_CACHE_TYPE=f16).

P11 logits/embd readback

#StepPurposellama.cpp locationminfer equivalent
11.1Locate backendggml_backend_sched_get_tensor_backend(t_logits)llama-context.cpp:1948—
11.2Async readbackggml_backend_tensor_get_async → ggml_metal_get_tensor_async: newBufferWithBytesNoCopy wraps host memory + blit encoder GPU→host, queued into cmd_bufs_extllama-context.cpp:1854 ggml-metal-context.m:351-391src/metal/ (copy_from_gpu: Shared buffer direct memcpy, no blit) — 2026-08-21: now n_out×nv (608 KB for single output; was 301 MB)
11.3Synchronizebefore the next decode, ggml_backend_sched_synchronize waits for the blitggml-backend.cppsubmit() blocks + download_logits

3. Supplementary mapping tables

3.1 Per-op dispatch switch (ggml-metal-ops.cpp:265-497)

Complete forward-path mapping (non-forward ops omitted):

ggml_ophandlerfusion
CONCATggml_metal_op_concat—
ADD/SUB/MUL/DIVggml_metal_op_binADD ×N (up to 8 consecutive ADDs → 1 dispatch); Snake/GEGLU specialization
ADD_IDggml_metal_op_add_id—
SOFT_MAXggml_metal_op_soft_max—
MUL_MATggml_metal_op_mul_mat—
MUL_MAT_IDggml_metal_op_mul_mat_id (MoE)—
GET_ROWS / SET_ROWSop_get_rows / op_set_rows—
NORM / RMS_NORMggml_metal_op_normRMSNorm + Mul(weight) + Add(bias) in 1 kernel
ROPE / ROPE_BACKggml_metal_op_rope—
FLASH_ATTN_EXTggml_metal_op_flash_attn_extQK^T + softmax + PV single kernel (+aux pad/blk/kv_f16/vec_reduce)
DUP / CPY / CONTggml_metal_op_cpy—
SILU_BACK / GLUop_silu_back / op_glu (training/gating)—

3.2 MUL_MAT 3-way kernel selection (ggml-metal-ops.cpp:2336-2538)

BranchTrigger conditionkernel / pipelinethreadgroupsllama.cpp location
① mat-mv extsrc1=f32, ne00%128==0, src0 type in supported set, ne11∈[2,8] (K-quants need ne11∈[4,8])kernel_mul_mv_ext_* (nsg=2, nxpsg per ne00: 16/8/4)(ne01/r0ptg, ne11/r1ptg, ne12·ne13), 32×nsgops.cpp:2340-2439 device.cpp:706
② simdgroup MMnon-transposed, has_simdgroup_mm, ne00>=64, ne11>8 — **minfer #40: GEMM now dispatches for `nt≥2 && (od≥2048nt≥9)` (was nt≥16), closing the nt∈[9,15] gap (7B pp12 16.6→124 t/s ≈ llama)**kernel_mul_mm_<t0>_<t1> (function-constants bc_inp/bc_out/ne12/ne13/r2/r3)
③ mat-vecotherwisekernel_mul_mv_* (per quant type)nsg/nr0 per typeops.cpp:2491-2538 device.cpp:801+

Function-constant offsets (ggml-metal-impl.h:99-100): FC_MUL_MV=600, FC_MUL_MM=700; MM uses 700-705 (bc_inp/bc_out/ne12/ne13/r2/r3).

On M4, llama disables the tensor API (§0.4) and actually uses branch ②'s legacy simdgroup_matrix path — which is level-for-level equivalent to minfer's kernel_q4_k_mm_f32 (src/metal/kernels/fa_prefill.metal, 64×32 tile, 32×4 threads, 8192 B smem) (see minfer docs/METAL_OPTIMIZATIONS.md §3.6).

3.3 FLASH_ATTN_EXT variant selection (ggml-metal-ops.cpp:2990-3492)

TestVariantTrigger condition
use_vec_vec + _vec_reducene01 < 20 && ne00 % 32 == 0 (decode small batch, half4x4)
use_kv_f16first kernel_flash_attn_ext_kv_f16 dequantizes KV→f16KV type ∈ {Q4_0,Q4_1,Q5_0,Q5_1,Q8_0} (new in #27390)
has_kvpadfirst kernel_flash_attn_ext_padne11 % ncpsg != 0 (KV not a multiple of ncpsg)
has_maskfirst kernel_flash_attn_ext_blkmask present (block pre-pass)
main kernelkernel_flash_attn_ext (half8x8, nqptg=8/ncpsg=64, nsg=ne00>=512?8:4)prefill (non-vec path)

Middle-buffer layout (ops.cpp:3055-3065): after dst, in order pad → blk → tmp → kv_f16 (sizes computed by ggml_metal_op_flash_attn_ext_extra_*).

3.4 Fusion rule summary

Fusionllama.cppminfer
RMSNorm + Mul(weight) + Add(bias)kernel_rms_norm_fuse_impl (ops.cpp:3929-3974)not fused: rms_norm + separate add_f32
Consecutive ADD ×Nop_bin (ops.cpp:3195+)single add_f32
flash attention (QK^T+softmax+PV)single kernel + aux passesattn_flash_prefill (src/metal/ops.rs)
KV write + RoPERoPE writes into the KV path (graph k/v expanded together)store_kv dedicated kernel
GLU/SiLUop_glu / op_snake_fusedswiglu_f32 (single kernel)
mul_mat + bias / residualnot fused (bias/residual is a separate kernel_add)same (separate add_f32 after wo)

3.5 Multi-CB and encoder/barrier model comparison

Itemllama.cppminfer
CB countn_cb=1 → 2 CBs (main thread 64 nodes + 1 worker)1 CB (all 28 layers + output)
Encode parallelismdispatch_apply multi-thread concurrent encodesingle-threaded sequential encode
encoder dispatch typeMTLDispatchTypeConcurrent (on by default, GGML_METAL_CONCURRENCY_DISABLE to off)serial (new_compute_command_encoder)
barrierdependency-aware (mem_ranges conflict only inserts memoryBarrierWithScope)unconditional memoryBarrierWithScope after every dispatch
commit/waitmain thread returns async, synchronize waits explicitly; 10 s timeout guardsubmit() blocks on completed handler (10 s timeout + status check)

4. Key data structures

StructDefined atPurpose / key fields
struct ggml_metal (ggml_metal_t)ggml-metal-context.m:26device, library, d_queue, n_cb, cmd_bufs[] (cmd_bufs[n_cb+1]), encode_async block, cmd_bufs_ext, cmd_buf_last, has_error
struct ggml_metal_deviceggml-metal-device.m:521mtl_device, mtl_queue (globally shared), rsets (residency sets), library, props, addr_virt
struct ggml_metal_encoderggml-metal-device.m:460wraps MTLComputeCommandEncoder
struct ggml_metal_libraryggml-metal-device.m:97MTLLibrary + cached MTLComputePipelineState map + lock; newLibraryWithSource (device.m:234)
struct ggml_metal_bufferggml-metal-device.m (buffer_init:1631)buffers[] ({id<MTLBuffer>, offs}), is_shared, rset
struct ggml_metal_opggml-metal-ops.cpp:28per-CB encode state: enc, mem_ranges, filtered idxs[], fusion flags
struct ggml_backend_schedggml-backend.cpp (sched_new:1792)backend array, splits[], galloc, node/leaf_backend_ids[], events[b][c], graph_copy
struct ggml_backend_sched_splitggml-backend.cpp:1055+{backend_id, i_start, i_end, n_inputs, inputs[], graph}
llm_graph_contextllama-graph.hctx0, gf, hparams/cparams, sched, res
llm_graph_resultllama-graph.ht_inp_tokens, t_logits, t_embd, inputs[], compute ctx + ggml_cgraph
ggml_metal_pipeline_with_paramsggml-metal-ops.h{pipeline, nr0, nr1, nsg, smem} — everything a single kernel dispatch needs

5. minfer comparison notes (for later comparison work)

  1. Architecture difference: llama.cpp = declarative ggml graph (topological nodes → scheduler → backend); minfer = imperative (forward.rs encodes layer-by-layer directly into a single MPS command buffer). No scheduler/allocator layer; src/cache.rs holds KV directly. 1b. Output-rows reduction (2026-08-21): llama shrinks the graph to n_outputs rows after the last attention (get_rows(cur, inp_out_ids) + get_rows(inpSA, inp_out_ids), qwen2.cpp:106-108) → the last layer's FFN + both residuals + final norm + lm_head all run on 1 row. minfer mirrors the FULL reduction: final norm + lm_head via n_out (#32), and the last layer's FFN + residuals via layer_gpu(n_out, is_last) (#34) — minfer's total graph work (≈6.26 TFLOP) now exactly equals llama's.
  2. Level-for-level equivalence proven (minfer docs/METAL_OPTIMIZATIONS.md §3.4/§3.6): the prefill GEMM kernels (kernel_mul_mm vs kernel_q*_mm_f32) match at source/IR/smem/dispatch/runtime-compile level; this table's P8/P9 rows are the comparison anchors.
  3. Fusion gap: llama's RMSNorm+Mul+Add, ADD×N, single-kernel flash fusion vs minfer's mostly-separate dispatches (§3.4) — the source of the per-layer dispatch-count difference in decode/prefill.
  4. KV format: llama defaults f16 + optional quantized KV (since #27390, a kv_f16 dequant pass); minfer auto-selects f16 for the 7B class / f32 for small models (#37, MINFER_CACHE_TYPE=f16/f32 overrides), and since C4 also has a packed Q8_0 cache — CUDA since C4 S2b, Metal since #310, MINFER_CACHE_TYPE=q8_0.
  5. Multi-CB: llama 2-CB concurrent encode; minfer single CB all layers. Measured (minfer §3.6): llama's 2-CB split is slower in a pure-GEMM replay — not a speed source.
  6. Barrier: llama dependency-aware; minfer barriers after every dispatch. Measured free (§3.6) — not a gap source.