Metal ESCHA_LINEAR kernel + dense qwen3.5 escha support: 0.037 to 6 t/s - #1
Open
Manojbhat09 wants to merge 1 commit into
Open
Metal ESCHA_LINEAR kernel + dense qwen3.5 escha support: 0.037 to 6 t/s#1Manojbhat09 wants to merge 1 commit into
Manojbhat09 wants to merge 1 commit into
Conversation
…ngl=99 - Metal GGML_OP_ESCHA_LINEAR kernel (k2/k3, fused staging+decode-GEMV+output-H) verified vs CPU/numpy at all stages; env hooks: GGML_ESCHA_DBG/PERF - instrumentation kept: GGML_METAL_SKIP_OPS, GGML_METAL_PROFILE(1/2/3), GGML_SCHED_TIMING, GGML_METAL_DENY_OPS - qwen35 dense escha support, loader placement via supports_op - tools/: converter, numpy parity, eval-diff helpers - state of success: Qwen38-27B-Escha-W2-q4head.gguf + '-ngl 99 -np 1' => pp 8.7 t/s / tg ~6.0 t/s on M4 16GB (vs 0.037 baseline)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a native Metal compute kernel for
GGML_OP_ESCHA_LINEAR— the dense(linear) escha projection used by Qwen3.5 W2-dense models — and wires it through
the ggml backend, model loader, and graph builder. Includes a Q4_K head
conversion path, several hard-won infrastructure fixes, and a verification
toolkit. End-to-end decode goes from 0.037 t/s to ~6 t/s on M4 16GB.
What changed
Core kernel (
ggml-metal.metal)kernel_escha_linear— a single Metal compute kernel that fuses:(rout*xT) * rinoutput as f16 for the downstream RoPE / attention graphThe kernel is dispatched from the Metal backend (
GGML_OP_ESCHA_LINEARcase inggml-metal-ops.cpp), registered in the Metal device'ssupports_op, anddeclared in the ggml op enum +
ggml_cpu_supports_opfallback.The decode GEMV path (M=1) is the hot path for token generation. Staging and
output-H are sub-kernels that run for M>=1.
Graph builder (
src/models/qwen35.cpp)build_escha_mm()— new helper: whenllm_escha_denseis populated, callsggml_escha_linear(code, rin, rout, s_in, s_out, bias, input); falls back tostandard
ggml_mul_matwhen the sidecar is absent.wq,wk,wv,wo,wg,wu,wd,wqkv,wqkv_gate,ssm_out) now route throughbuild_escha_mm().Loader (
src/llama-model-loader.cpp,src/models/qwen35.cpp)llm_escha_densestruct:code,rin,rout,s_in,s_out,bias.load_escha_linear()— loads the 6 sidecar tensors per projection; readsne[0]of the code tensor to determine bit-width (32 or 48).GGML_OP_ESCHA_LINEAR(dense) vsGGML_OP_ESCHA_MOE(routed) based on arch name.
mmap span guard (
src/llama-model.cpp)When mixed code/dense models place GPU tensors far apart in the file with
CPU-only weights in between,
BytesNoCopywas wrapping a multi-gigabyte spanfor a few hundred MB of actual weights — exhausting the unified-memory wired
limit. The guard now compares the mmap span against the actual tensor bytes and
falls back to a real allocation when the span exceeds the tensors by >64 MiB.
Q4_K head quantization
output.weight(2.4 GB F16) is read every token. Quantizing it to Q4_K saves~1.8 GB of reads per decode step. The escha codes are byte-identical; logits
change by ~0.1-0.45% — within expected quantization noise. Achieved via:
Output logits verified against f16 baseline via eval-callback checksums.
Instrumentation (env-gated, zero overhead when unset)
GGML_ESCHA_DBG=1GGML_ESCHA_PERF=1GGML_METAL_SKIP_OPS=X,YGGML_METAL_PROFILE=1GGML_SCHED_TIMING=1ggml_backend_sched_graph_computeGGML_METAL_DENY_OPS=X,YGGML_SCHED_TIMING=1is what ultimately found the root cause of the 9 s/tokregression (see below).
Benchmark
Mac mini M4 16GB,
Qwen38-27B-Escha-W2-q4head.gguf(10 GB),-ngl 99 -np 1 -c 2048:Cross-check with
GGML_ESCHA_DBG=1(escha approx. free):The 9-second mystery (solved)
With
-ngl 48(the default set by the previous fit-ladder), every token took~9.5 s.
GGML_SCHED_TIMING=1revealed the decode graph had two splits:The 1041 CPU nodes were the last 16 layers + output head, which
-ngl 48neveroffloaded. Switching to
-ngl 99puts everything on the GPU. This single flagaccounts for the vast majority of the speedup.
Verification
tests/test-escha-linear.cpp): compares Metaloutput against C reference at every stage (decode, staging, output-H) for 8
cases varying IC, OC, M, and code width. All pass; residual norms are fp16
quantization noise only.
llama-eval-callback:result_norm = -105.259483result_norm = -105.252533(6.6e-5 relative)-432089vs q4head GPU-431636(0.1%)tools/escha-numpy-forward.py): stage-wisereference at fp32, used during kernel development to catch wire-ups and
broadcast bugs.
Files changed
ggml-metal.metal,ggml-metal-ops.cpp/h,ggml-metal-context.m,ggml-metal-device.cpp/m/h,ggml-metal.cppggml.h,ggml.c,ggml-backend.cppggml-cpu/ops.cpp/h,ggml-cpu.cllama-graph.h,models/qwen35.cpp,models/qwen3next.cpp,models/qwen3moe.cpp,models/models.hllama-model-loader.cpp,llama-model.cpp,llama-model.htests/test-escha-linear.cpp,tests/CMakeLists.txttools/escha-convert-dense.py,tools/escha-numpy-forward.py,tools/escha-numpy-goodvar.py,tools/escha-dump-linear-cases.py,tools/diff-cb-numpy.py