Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
a505a75
llama: load glm5next hparams and tensors
Aug 26, 2026
9e1cc20
llama: add glm5next mHC wide residual
Aug 26, 2026
20a8077
llama: add glm5next KDA linear attention
danielhanchen Aug 26, 2026
1d99a5a
llama: add glm5next MoE feed-forward with clamped SwiGLU
danielhanchen Aug 26, 2026
f320a28
llama: add glm5next dense DSA attention
danielhanchen Aug 26, 2026
7f2560e
llama: save the indexer kpool and hyper-connection keys
danielhanchen Aug 26, 2026
cd69d60
test-llama-archs: enable glm5next
danielhanchen Aug 26, 2026
6a58359
llama: assert the glm5next indexer selection width
danielhanchen Aug 26, 2026
3841592
glm5next: trim DSA dense comments
danielhanchen Aug 26, 2026
2db8295
llama: third kv cache for the glm5next lightning indexer
danielhanchen Aug 26, 2026
839597c
glm5next: trim pooled indexer comments
danielhanchen Aug 26, 2026
8c49836
glm5next: lightning indexer graph, pooled sparse selection
Aug 26, 2026
fe95953
mtmd: don't normalize patch embeddings when norm_embd is absent
danielhanchen Aug 26, 2026
582fe81
convert: refuse to write a GGUF with zero tensors
danielhanchen Aug 26, 2026
9901ab4
mtmd: add glm5next (GLM-5.3-Flash) vision tower
danielhanchen Aug 26, 2026
41cd923
mtmd: implement the glm5next 0826 image preprocessor
danielhanchen Aug 26, 2026
02fa4a4
mtmd: resample glm5next images with bicubic, matching the reference
danielhanchen Aug 26, 2026
81e3e67
llama: enable ignore_merges for the glm4 pre-tokenizer
Aug 26, 2026
2d9570d
llama: shorten comment
danielhanchen Aug 26, 2026
6c59f9b
glm5next: repair conflict resolution from the rebase onto master
danielhanchen Aug 26, 2026
eab9ee9
glm5next: do not inherit deepseek4's n_embd_out
danielhanchen Aug 27, 2026
869e878
glm5next: keep precision-sensitive tensors at source precision
danielhanchen Aug 27, 2026
282ef61
glm5next: restore the vision preprocessor tests
danielhanchen Aug 27, 2026
cadbe97
glm5next: fix E301 lint in gguf_writer
danielhanchen Aug 27, 2026
204fa70
glm5next: pool the indexer per sequence so a unified KV cache works
danielhanchen Aug 27, 2026
ef531f8
glm5next: test the per-sequence pool runs instead of the old refusal
danielhanchen Aug 27, 2026
e88c92d
glm5next: store the indexer selection masks in f16
Aug 27, 2026
2e0e57f
glm5next : use the fused lightning indexer for the pool scores
Aug 27, 2026
cfa63e8
glm5next: drop the test suite for the slim variant
danielhanchen Aug 28, 2026
6dd9186
glm5next: cut comment volume by 72%
danielhanchen Aug 28, 2026
1f0a36a
Merge remote-tracking branch 'ggml/master' into glm5next/upstream-slim
danielhanchen Aug 28, 2026
b5517b1
tests : add the glm5next fixture
danielhanchen Aug 28, 2026
f30bed8
kv-cache : group the context accessors after type_v
danielhanchen Aug 28, 2026
0069971
Faster inference
danielhanchen Aug 30, 2026
d07e71e
Add MTP support
danielhanchen Aug 30, 2026
238f880
tests: add glm5next --depth-sweep mode (CPU-vs-device logit divergenc…
Sep 1, 2026
ed1313c
tests: depth-sweep gains --topk override and same-backend ubatch A/B …
Sep 1, 2026
2841c7b
tests: depth-sweep --iheads/--iklen overrides (real glm5next: 32 head…
Sep 1, 2026
40f7a34
tests: depth-sweep --layers/--dlead (glm5next layer count + leading d…
Sep 1, 2026
f5d5216
ggml-metal: 64-bit output offsets in mul_mm batched dst indexing
Sep 1, 2026
c31aa2d
llama-context: env-gated alloc-map dump (LLAMA_ALLOCDUMP_*) for wrap-…
Sep 1, 2026
b439462
metal: gpuAddress diagnostic (codex patch) + allocdump gpu_data/gpu_b…
Sep 1, 2026
42b9e5e
allocdump: LLAMA_ALLOCDUMP_REQ_TOKENS full-ubatch filter, bidirection…
Sep 1, 2026
b5d5264
metal: LLAMA_METAL_CAPTURE_ARM_FILE one-shot capture arming + tensor-…
Sep 1, 2026
b5fe910
graph: env-gated packed-MQA decode transform (speed-conf 019 Patch A)…
Sep 1, 2026
d33b0ab
graph: split packed-MQA modes (kq/kqv/both) + f32 prec on packed KQV,…
Sep 1, 2026
19446fe
tests: scope depth-sweep FA-off via arg (other arch tests keep AUTO);…
Sep 1, 2026
5f3d764
graph: strict packed-MQA mode parse - numeric 2 no longer aliases to …
Sep 1, 2026
37a483c
kv-cache/graph: Patch B — derived transposed MLA-V mirror (LLAMA_MLA_…
Sep 1, 2026
c4cc25e
graph/kv-cache: Patch B hardening (grokk 033 2.1/2.3/2.4 + codex 034 …
Sep 1, 2026
f86a0da
graph: exact v_idxs width check in attn_k sub-input; name mirror upda…
Sep 1, 2026
0519ed8
kv-cache: scope V-mirror opt-in to nope-only GLM5Next (codex 051 s3)
Sep 1, 2026
dd80b05
tests: single-token decode step in depth-sweep so packed decode branc…
Sep 2, 2026
92177ae
tests: --dump-1tok writes the decode-1 device+cpu logits for cross-ru…
Sep 2, 2026
fbc3542
glm5next: D0 gathered DSA decode (speed conference 056/057) — LLAMA_G…
Sep 2, 2026
4cbe31e
glm5next/tests: D0 P0 batch (codex 059 / grokk 060)
Sep 2, 2026
6056d05
glm5next: PAD32 — pad gathered slots to the mul_mm tile (codex 063 / …
Sep 2, 2026
c4ec35f
graph: Patch C view-only — drop redundant conts on packed-path single…
Sep 2, 2026
048799f
graph/context: R0 — kpool can_reuse contract + split host timers (cod…
Sep 2, 2026
1f9064b
graph/context: E0a — live certificate-subsumption invariant, host-sid…
Sep 2, 2026
37447da
context/kpool: timer + reuse-contract repairs (grokk 076/078, codex 0…
Sep 2, 2026
03dc115
context: rebuild event ledger replaces nonzero percentiles (grokk 081…
Sep 2, 2026
18dcfb3
glm5next: E0b+c MASKLESS gathered decode (codex 066, grokk 067) + dia…
Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@
"Glm4MoeLiteForCausalLM": "glm",
"Glm4vForConditionalGeneration": "glm",
"Glm4vMoeForConditionalGeneration": "glm",
"Glm5NextForCausalLM": "glm5next",
"Glm5NextForConditionalGeneration": "glm5next",
"GlmForCausalLM": "chatglm",
"GlmMoeDsaForCausalLM": "glm",
"GlmOcrForConditionalGeneration": "glm",
Expand Down Expand Up @@ -296,6 +298,7 @@
"Gemma4UnifiedForConditionalGeneration": "gemma",
"Glm4vForConditionalGeneration": "qwen3vl",
"Glm4vMoeForConditionalGeneration": "qwen3vl",
"Glm5NextForConditionalGeneration": "glm5next",
"Glm5vForConditionalGeneration": "kimivl",
"GlmOcrForConditionalGeneration": "qwen3vl",
"GlmasrModel": "ultravox",
Expand Down
14 changes: 14 additions & 0 deletions conversion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,20 @@ def set_gguf_parameters(self):
if not self.has_vision_encoder and not self.has_audio_encoder:
raise ValueError("MmprojModel must have either vision or audio encoder")

def prepare_tensors(self):
super().prepare_tensors()

# an mmproj has no vocab-only mode, so an empty one is always a silent mapping failure
# rather than a supported output; count and size are both checked because a tensor map
# that produces only zero-sized entries is just as broken as one that produces none
n_tensors = sum(len(t) for t in self.gguf_writer.tensors)
n_bytes = sum(ti.nbytes for t in self.gguf_writer.tensors for ti in t.values())
if n_tensors == 0 or n_bytes == 0:
raise ValueError(
f"refusing to write an mmproj with no tensor data (n_tensors = {n_tensors}, n_bytes = {n_bytes}); "
"check that the model's vision/audio tensors are named as the tensor map expects"
)

def write_vocab(self):
raise ValueError("MmprojModel does not support vocab writing")

Expand Down
273 changes: 273 additions & 0 deletions conversion/glm5next.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
from __future__ import annotations

import re

from typing import Callable, Iterable, TYPE_CHECKING

import torch

if TYPE_CHECKING:
from torch import Tensor

from .base import LazyTorchTensor, ModelBase, TextModel, gguf
from .qwen3vl import Glm4VVisionModel


@ModelBase.register("Glm5NextForConditionalGeneration", "Glm5NextForCausalLM")
# [TAG_HF_EXAMPLE_MISSING]
class Glm5NextModel(TextModel):
"""GLM-5.3-Flash text tower: hybrid KDA + DSA attention, nope-only MLA, mHC
hyper-connections, and a NextN block with its own DSA attention and indexer.
"""

model_arch = gguf.MODEL_ARCH.GLM5NEXT
supports_mtp_export = True

_experts: list[dict[str, Tensor]] | None = None
_main_layers: int | None = None

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

nextn_layers = 0 if self.no_mtp else (self.hparams.get("num_nextn_predict_layers", 0) or 0)
self.block_count = self.hparams["num_hidden_layers"] + nextn_layers
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)

# two independent spellings of the same partition; disagreement means an
# unexpected config
from_types = {il for il, t in enumerate(self.hparams["layer_types"]) if t == "deepseek_sparse_attention"}
from_list = set(self.hparams["linear_attn_config"]["full_attn_layers"])
if from_types != from_list:
raise ValueError(f"layer_types picks DSA layers {sorted(from_types)} but full_attn_layers says {sorted(from_list)}")
self._full_attn_layers = from_types

dense_lead = self.hparams["first_k_dense_replace"]
expected = ["dense"] * dense_lead + ["sparse"] * (self.hparams["num_hidden_layers"] - dense_lead)
if self.hparams["mlp_layer_types"] != expected:
raise ValueError("mlp_layer_types does not match first_k_dense_replace")

def index_tensors(self, remote_hf_model_id: str | None = None):
# runs before TextModel.__init__ has hoisted text_config to the root
hp = self.hparams.get("text_config", self.hparams)
type(self)._main_layers = hp["num_hidden_layers"]
return super().index_tensors(remote_hf_model_id=remote_hf_model_id)

def set_vocab(self):
self._set_vocab_glm()

def is_full_attention(self, bid: int) -> bool:
return bid >= self.hparams["num_hidden_layers"] or bid in self._full_attn_layers

# -- metadata ---------------------------------------------------------

def set_gguf_parameters(self):
hp = self.hparams
linear_cfg = hp["linear_attn_config"]

# checked here, not in the loader: head_count_kv is overwritten below with
# the per-layer 1/0 recurrence marker
if hp["num_attention_heads"] != hp.get("num_key_value_heads"):
raise ValueError("glm5next expects MHA-shaped head counts before MLA absorption")
if hp["qk_rope_head_dim"] != 0 or not hp.get("mla_use_nope"):
raise ValueError("glm5next is nope-only: qk_rope_head_dim must be 0 and mla_use_nope true")
if linear_cfg["num_heads"] != hp["num_attention_heads"]:
raise ValueError("glm5next KDA and full attention are expected to share a head count")
if not hp.get("mhc"):
raise ValueError("glm5next without mHC is not supported")
if hp["index_topk"] % hp["index_kpool"] != 0:
raise ValueError("glm5next index_topk must be a whole number of kpool pools")

# no GGUF key carries these and the graph cannot express them off, so refuse
# rather than write a silently wrong file
if not hp.get("index_kpool_compress"):
raise ValueError("glm5next without the indexer kpool compressor is not supported")
if not hp.get("index_kpool_always_select_tail"):
raise ValueError("glm5next without always-select-tail kpool is not supported")
if not hp.get("indexer_rope_interleave"):
raise ValueError("glm5next without interleaved indexer rope is not supported")
if set(hp["indexer_types"]) != {"full"}:
raise ValueError("glm5next expects every indexer to be full")

# drop both: head_dim is 0 in the config, head_count_kv is written as a
# per-layer array below
hp.pop("head_dim", None)
hp.pop("num_key_value_heads", None)

super().set_gguf_parameters()

self.gguf_writer.add_vocab_size(hp["vocab_size"])

# n_head_kv == 0 marks a KDA (recurrent) layer, as in kimi-k3 and bailingmoe3
self.gguf_writer.add_head_count_kv(
[1 if self.is_full_attention(il) else 0 for il in range(self.block_count)])

# --- MLA ---
kv_lora_rank = hp["kv_lora_rank"]
qk_rope_head_dim = hp["qk_rope_head_dim"]
self.gguf_writer.add_q_lora_rank(hp["q_lora_rank"])
self.gguf_writer.add_kv_lora_rank(kv_lora_rank)
self.gguf_writer.add_rope_dimension_count(qk_rope_head_dim)
self.gguf_writer.add_key_length(kv_lora_rank + qk_rope_head_dim)
self.gguf_writer.add_value_length(kv_lora_rank)
self.gguf_writer.add_key_length_mla(hp["qk_nope_head_dim"] + qk_rope_head_dim)
self.gguf_writer.add_value_length_mla(hp["v_head_dim"])

# indexer k_norm is a LayerNorm with bias at a fixed 1e-6, not the model's
# RMS eps. glm-dsa omits this key and runs that norm at eps 0
self.gguf_writer.add_layer_norm_eps(1e-6)

# --- KDA ---
self.gguf_writer.add_ssm_conv_kernel(linear_cfg["short_conv_kernel_size"])
self.gguf_writer.add_kda_head_dim(linear_cfg["head_dim"])
# not a clamp: scales the sigmoid decay gate. required, a missing key
# silently selects the softplus branch instead
self.gguf_writer.add_kda_gate_lower_bound(linear_cfg["gate_lower_bound"])

# --- DSA indexer ---
self.gguf_writer.add_indexer_head_count(hp["index_n_heads"])
self.gguf_writer.add_indexer_key_length(hp["index_head_dim"])
self.gguf_writer.add_indexer_top_k(hp["index_topk"])
self.gguf_writer.add_indexer_kpool(hp["index_kpool"])

# --- mHC ---
self.gguf_writer.add_hyper_connection_count(hp["hc_mult"])
self.gguf_writer.add_hyper_connection_sinkhorn_iterations(hp["hc_sinkhorn_iters"])
self.gguf_writer.add_hyper_connection_epsilon(hp["hc_eps"])

# --- MoE ---
n_ff_exp = hp["moe_intermediate_size"]
self.gguf_writer.add_expert_feed_forward_length(n_ff_exp)
self.gguf_writer.add_expert_shared_feed_forward_length(n_ff_exp * hp["n_shared_experts"])
self.gguf_writer.add_expert_shared_count(hp["n_shared_experts"])
self.gguf_writer.add_leading_dense_block_count(hp["first_k_dense_replace"])
self.gguf_writer.add_expert_weights_scale(hp["routed_scaling_factor"])
self.gguf_writer.add_expert_weights_norm(hp["norm_topk_prob"])

# one limit for the whole model. no dense-FFN clamp key exists, so the
# expert arrays are sized for every layer to cover the leading dense ones
swiglu_limit = float(hp["swiglu_limit"])
self.gguf_writer.add_swiglu_clamp_exp([swiglu_limit] * self.block_count)
self.gguf_writer.add_swiglu_clamp_shexp([swiglu_limit] * self.block_count)

if not self.no_mtp and (nextn_layers := hp.get("num_nextn_predict_layers", 0)):
self.gguf_writer.add_nextn_predict_layers(nextn_layers)

# -- tensors ----------------------------------------------------------

@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
if (titem := super().filter_tensors(item)) is None:
return None
name, gen = titem

assert cls._main_layers is not None
is_mtp = (m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._main_layers

if is_mtp and cls.no_mtp:
return None
if cls.mtp_only and not is_mtp and name not in (
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
):
return None

return name, gen

def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# --- KDA conv1d: HF [d_inner, 1, d_conv] -> ggml ne [d_conv, 1, d_inner, 1] ---
if name.endswith((".q_conv1d.weight", ".k_conv1d.weight", ".v_conv1d.weight")):
d_inner = data_torch.shape[0]
d_conv = data_torch.shape[-1]
data_torch = data_torch.reshape(1, d_inner, 1, d_conv)

# ssm_a holds -exp(A_log), the kimi-k3 convention (bailingmoe3 stores
# +exp(A_log)); the wrong sign turns decay into an unchecked growing state
if name.endswith(".A_log"):
# eager: the sign is the point of the check, and A_log is one per head
decay = LazyTorchTensor.to_eager(torch.exp(data_torch.float()))
if not bool(torch.isfinite(decay).all() and (decay > 0).all()):
raise ValueError(f"{name}: exp(A_log) must be finite and positive")
data_torch = -decay

# dt_bias -> the name SSM_DT's mapping expects
if name.endswith(".dt_bias"):
name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"

# bare tensors in the checkpoint, but the GGUF names carry .weight
if re.search(r"\.hc_(attn|ffn)_(fn|base|scale)$", name) or name.endswith(
(".index_kpool_compress_gate", ".index_kpool_compress_ape")):
name += ".weight"

# --- routed experts ---
if ".mlp.experts." in name:
n_experts = self.hparams["n_routed_experts"]
assert bid is not None

if self._experts is None:
self._experts = [{} for _ in range(self.block_count)]
self._experts[bid][name] = data_torch

if len(self._experts[bid]) < n_experts * 3:
return

for weight_name in ("down_proj", "gate_proj", "up_proj"):
tensors = []
for expert_id in range(n_experts):
expert_name = f"model.layers.{bid}.mlp.experts.{expert_id}.{weight_name}.weight"
tensors.append(self._experts[bid].pop(expert_name))
merged_name = f"model.layers.{bid}.mlp.experts.{weight_name}.weight"
yield from super().modify_tensors(torch.stack(tensors, dim=0), merged_name, bid)
return

# --- MLA absorption ---
if name.endswith(".kv_b_proj.weight"):
assert bid is not None
n_head = self.hparams["num_attention_heads"]
v_head_dim = self.hparams["v_head_dim"]
qk_nope_head_dim = self.hparams["qk_nope_head_dim"]
assert data_torch.shape[0] == n_head * (v_head_dim + qk_nope_head_dim)
kv_b = data_torch.view(n_head, v_head_dim + qk_nope_head_dim, data_torch.shape[-1])
k_b, v_b = torch.split(kv_b, [qk_nope_head_dim, v_head_dim], dim=1)
yield from super().modify_tensors(k_b.transpose(1, 2), self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K_B, bid), bid)
yield from super().modify_tensors(v_b, self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_V_B, bid), bid)
return

yield from super().modify_tensors(data_torch, name, bid)

def tensor_force_quant(self, name, new_name, bid, n_dims):
# learned position table, one row per pooled key; pinned for the same
# reason POS_EMBD is in base.py
if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.INDEXER_COMPRESSOR_APE, bid):
return gguf.GGMLQuantizationType.F32
return super().tensor_force_quant(name, new_name, bid, n_dims)

def prepare_tensors(self):
super().prepare_tensors()
if self._experts is not None:
experts = [name for layer in self._experts for name in layer]
if experts:
raise ValueError(f"Unprocessed experts: {experts}")


@ModelBase.register("Glm5NextForConditionalGeneration")
# [TAG_HF_EXAMPLE_MISSING]
class Glm5NextVisionModel(Glm4VVisionModel):
"""The vision tower is the GLM-OCR ViT under a `model.visual.` prefix.

Every tensor already maps through the GLM-4V entries. The one structural
difference is a clamp on the SwiGLU gate and up projections, applied in the
per-block MLP and again in the merger, so it gets its own projector type
rather than a flag on glm4v.
"""

clip_projector_type = gguf.VisionProjectorType.GLM5NEXT

def set_gguf_parameters(self):
super().set_gguf_parameters()
assert self.hparams_vision is not None

# Glm4VVisionModel bypasses Qwen3VLVisionModel entirely, which is also where
# the merge size is written, so no GLM4V-family mmproj carries this key and
# clip.cpp falls back to a hardcoded 2. Write it rather than rely on that
self.gguf_writer.add_vision_spatial_merge_size(int(self.hparams_vision["spatial_merge_size"]))

self.gguf_writer.add_vision_swiglu_limit(float(self.hparams_vision["swiglu_limit"]))
5 changes: 4 additions & 1 deletion conversion/qwen3vl.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,13 @@ class Qwen3ASRMmprojModel(Qwen3OmniMmprojModel):
@ModelBase.register("Glm4vForConditionalGeneration", "Glm4vMoeForConditionalGeneration", "GlmOcrForConditionalGeneration")
@ModelBase.example("zai-org/GLM-4.1V-9B-Thinking", "zai-org/GLM-4.5V")
class Glm4VVisionModel(Qwen3VLVisionModel):
# subclasses that share this tower but need their own clip graph override this
clip_projector_type = gguf.VisionProjectorType.GLM4V

def set_gguf_parameters(self):
MmprojModel.set_gguf_parameters(self) # skip Qwen3VLVisionModel parameters
assert self.hparams_vision is not None
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GLM4V)
self.gguf_writer.add_clip_projector_type(self.clip_projector_type)

hidden_act = str(self.hparams_vision.get("hidden_act", "")).lower()
if hidden_act == "gelu":
Expand Down
5 changes: 5 additions & 0 deletions examples/embedding/embedding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ int main(int argc, char ** argv) {
return 1;
}

if (ctx == NULL) {
LOG_ERR("%s: unable to create context\n", __func__);
return 1;
}

const llama_vocab * vocab = llama_model_get_vocab(model);

const int n_ctx_train = llama_model_n_ctx_train(model);
Expand Down
5 changes: 5 additions & 0 deletions ggml/include/ggml-metal.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ GGML_BACKEND_API bool ggml_backend_metal_supports_family(ggml_backend_t backend,
// capture all command buffers committed the next time `ggml_backend_graph_compute` is called
GGML_BACKEND_API void ggml_backend_metal_capture_next_compute(ggml_backend_t backend);

// Returns tensor's Metal GPU virtual address, or 0 for a non-Metal buffer.
// This is a diagnostic interface; ordinary callers should not depend on the
// relative placement of separately allocated MTLBuffers.
GGML_BACKEND_API uint64_t ggml_backend_metal_buffer_get_gpu_address(ggml_backend_buffer_t buffer, const struct ggml_tensor * tensor);

GGML_BACKEND_API ggml_backend_reg_t ggml_backend_metal_reg(void);

#ifdef __cplusplus
Expand Down
17 changes: 17 additions & 0 deletions ggml/src/ggml-metal/ggml-metal-context.m
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,23 @@ enum ggml_status ggml_metal_graph_compute(ggml_metal_t ctx, struct ggml_cgraph *

ctx->n_nodes_per_cb = (ctx->n_nodes_1 + ctx->n_cb - 1) / ctx->n_cb;

// fabley (speed conference 008/009): env-gated arm-file — when
// LLAMA_METAL_CAPTURE_ARM_FILE is set and the file exists, consume it
// and capture THIS compute. Production without the env pays nothing.
{
static const char * arm_path = NULL;
static bool arm_checked = false;
if (!arm_checked) {
arm_checked = true;
arm_path = getenv("LLAMA_METAL_CAPTURE_ARM_FILE");
}
if (arm_path && access(arm_path, F_OK) == 0) {
unlink(arm_path);
ctx->capture_compute = 1;
GGML_LOG_WARN("%s: capture armed via %s\n", __func__, arm_path);
}
}

if (ctx->capture_compute >= 0) {
ctx->capture_compute--;
}
Expand Down
1 change: 1 addition & 0 deletions ggml/src/ggml-metal/ggml-metal-device.h
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ ggml_metal_buffer_t ggml_metal_buffer_map (ggml_metal_device_t dev, void * ptr,

void ggml_metal_buffer_free (ggml_metal_buffer_t buf);
void * ggml_metal_buffer_get_base (ggml_metal_buffer_t buf);
uint64_t ggml_metal_buffer_get_gpu_address(ggml_metal_buffer_t buf, const struct ggml_tensor * tensor);
bool ggml_metal_buffer_is_shared(ggml_metal_buffer_t buf);

void ggml_metal_buffer_memset_tensor(ggml_metal_buffer_t buf, struct ggml_tensor * tensor, uint8_t value, size_t offset, size_t size);
Expand Down
10 changes: 10 additions & 0 deletions ggml/src/ggml-metal/ggml-metal-device.m
Original file line number Diff line number Diff line change
Expand Up @@ -2141,6 +2141,16 @@ void ggml_metal_buffer_free(ggml_metal_buffer_t buf) {
return buf->all_data;
}

uint64_t ggml_metal_buffer_get_gpu_address(ggml_metal_buffer_t buf, const struct ggml_tensor * tensor) {
struct ggml_metal_buffer_id bid = ggml_metal_buffer_get_id(buf, tensor);
if (bid.metal == nil) {
return 0;
}

id<MTLBuffer> metal = bid.metal;
return metal.gpuAddress + bid.offs;
}

bool ggml_metal_buffer_is_shared(ggml_metal_buffer_t buf) {
return buf->is_shared;
}
Expand Down
Loading