botp
/

Solomon / mlx /src /solomon_mlx /engine.py
orz99's picture ArcherHume's picture
Duplicate from DoccyHealth/Solomon
1d2de8a
Raw
History Blame Contribute Delete
15.4 kB
"""BF16 Metal execution with instance-owned adaptation and isolated question caches."""
import copy
import json
import threading
import time
from pathlib import Path
import mlx.core as mx
import numpy as np
from mlx import nn
from PIL import Image
from ._vendor.prompts import PAGE, SYSTEM
from .artifacts import ADAPTER_SHA, BASE_REVISION, HEADS_SHA, SOLOMON_REVISION, runtime_identity, sha256
class SwitchLoRA(nn.Module):
def __init__(self, linear, a, b, context):
super().__init__()
self.linear, self.lora_a, self.lora_b = linear, a, b
self._context = context
def __call__(self, x):
y = self.linear(x)
start = self._context["start"]
if start is None or start >= x.shape[1]:
return y
delta = (2.0 * ((x[:, start:].astype(mx.float32) @ self.lora_a) @ self.lora_b)).astype(y.dtype)
return y + delta if start == 0 else mx.concatenate([y[:, :start], y[:, start:] + delta], axis=1)
def fork_cache(caches):
"""New cache containers and array handles; MLX owns copy-on-write storage.
mx.array creates a distinct handle, so slice updates cannot change a prefix's
Python array. Recurrent/window updates replace the branch's private slots.
"""
from mlx_vlm.models.cache import ArraysCache, KVCache
result = []
for original in caches:
if isinstance(original, ArraysCache):
branch = ArraysCache(len(original.cache))
branch.cache = [None if x is None else mx.array(x) for x in original.cache]
elif isinstance(original, KVCache):
branch = KVCache()
branch.state = tuple(None if x is None else mx.array(x) for x in original.state)
else:
raise TypeError(f"Unsupported prefix cache: {type(original).__name__}")
result.append(branch)
return result
class Engine:
def __init__(self, directory, *, chunk_size=2048, max_tokens=40960):
from mlx_vlm.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
from mlx_vlm.utils import load_model
self.directory = Path(directory).resolve()
self.binding = json.loads((self.directory / "binding.json").read_text())
if (
self.binding.get("schema") != "solomon-mlx-binding-v1"
or self.binding.get("base_revision") != BASE_REVISION
or self.binding.get("solomon_revision") != SOLOMON_REVISION
):
raise ValueError("Unrecognized or unpinned Solomon MLX binding")
if self.binding["profile"] != "quality" or self.binding["dtype"] != "bfloat16":
raise ValueError("This runtime currently accepts only the BF16 quality profile")
for name, expected in self.binding["files"].items():
path = (self.directory / name).resolve()
if not path.is_relative_to(self.directory) or sha256(path) != expected:
raise ValueError(f"Model artifact checksum mismatch: {name}")
adapter, heads = self.directory / "adapter.safetensors", self.directory / "heads.npz"
if sha256(adapter) != ADAPTER_SHA or sha256(heads) != HEADS_SHA:
raise ValueError("Solomon checkpoint identity mismatch")
if not 1 <= chunk_size <= 2048 or not 1 <= max_tokens <= 40960:
raise ValueError("Invalid chunk size or context ceiling")
weight_bytes = sum(
(self.directory / name).stat().st_size
for name in self.binding["files"]
if name.endswith((".safetensors", ".npz"))
)
if weight_bytes + 4 * 2**30 > mx.device_info()["max_recommended_working_set_size"]:
raise MemoryError(
"Full BF16 weights and minimum workspace exceed this Mac’s recommended Metal working set"
)
self.chunk_size, self.max_tokens = chunk_size, max_tokens
self.lock = threading.RLock()
self.context = {"start": None}
self.model = load_model(self.directory / "backbone", lazy=True, strict=True)
self.processor = Qwen3VLProcessor.from_pretrained(
str(self.directory / "backbone"), trust_remote_code=False
)
self.lm, self.t = self.model.language_model, self.processor.tokenizer
self.pad = self.t.convert_tokens_to_ids("<|image_pad|>")
weights = mx.load(str(adapter))
for name in sorted({key.rsplit(".", 1)[0] for key in weights}):
parts = name.split(".")
if parts[:2] != ["model", "layers"]:
raise ValueError(f"Unexpected adapter target: {name}")
owner = self.lm.model.layers[int(parts[2])]
for part in parts[3:-1]:
owner = getattr(owner, part)
linear = getattr(owner, parts[-1])
a, b = weights[name + ".lora_a"].astype(mx.float32), weights[name + ".lora_b"].astype(mx.float32)
if a.shape != (linear.weight.shape[1], 64) or b.shape != (64, linear.weight.shape[0]):
raise ValueError(f"Adapter orientation/shape mismatch: {name}")
setattr(owner, parts[-1], SwitchLoRA(linear, a, b, self.context))
with np.load(heads, allow_pickle=False) as archive:
keys = {k[:-7] for k in archive.files if k.endswith("/weight")}
required_heads = {
"boolean/state4",
"entity/state4",
"multilabel/state4",
"ordered/threshold4",
"single/choiceR",
"single/choiceS",
"single/sufficiency3",
"ordered/choiceR",
"ordered/choiceS",
"ordered/sufficiency3",
}
if keys != required_heads:
raise ValueError("All ten semantic heads are required")
self.heads = {}
for key in keys:
w, b = archive[key + "/weight"], archive[key + "/bias"]
if (
w.shape != (10, 5120)
or b.shape != (10,)
or not np.isfinite(w).all()
or not np.isfinite(b).all()
):
raise ValueError("Invalid semantic head")
self.heads[key] = (mx.array(w, mx.float32), mx.array(b, mx.float32))
self.model.freeze()
self.model.eval()
mx.eval(self.model.parameters(), self.heads)
self.identity = runtime_identity(self.binding, chunk_size=self.chunk_size, max_tokens=self.max_tokens)
def render(self, parts, block):
content = ""
for i, p in enumerate(parts):
if "text" in p:
content += ("\n" if i and "image" in parts[i - 1] else "") + p["text"]
else:
content += ("\n" if i and "text" in parts[i - 1] else "") + PAGE
return self.t.apply_chat_template(
[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": "Document:\n" + content + "\n\n" + block},
],
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
def expand(self, ids, counts):
out, index = [], 0
for token in ids:
if token == self.pad:
if index >= len(counts):
raise ValueError("Unexpected image placeholder in document text")
out.extend([token] * counts[index])
index += 1
else:
out.append(token)
if index != len(counts):
raise ValueError("Image placeholder count mismatch")
return out
def positions(self, start, count):
return mx.broadcast_to(mx.arange(start, start + count)[None, None, :], (3, 1, count))
def admit(self, count):
if count < 1 or count > self.max_tokens:
raise ValueError(f"{count} tokens exceeds the {self.max_tokens}-token scope ceiling")
# Conservative allowance: BF16 attention KV + FP32 recurrent states and
# chunk intermediates. This supplements the token ceiling, not a promise
# of availability in the presence of other processes.
temporary = 4 * 2**30 + count * 16 * 2 * 4 * 256 * 2
limit = mx.device_info()["max_recommended_working_set_size"]
if mx.get_active_memory() + temporary > limit:
raise MemoryError("Insufficient recommended Metal working set for this request")
def forward(self, ids, positions, cache, *, embeds=None, adapter_from=None, taps=()):
hidden, captured = None, {}
try:
for start in range(0, len(ids), self.chunk_size):
end = min(start + self.chunk_size, len(ids))
self.context["start"] = None if adapter_from is None else max(0, adapter_from - start)
last = end == len(ids)
out = self.lm(
mx.array([ids[start:end]]),
cache=cache,
position_ids=positions[:, :, start:end],
inputs_embeds=None if embeds is None else embeds[:, start:end],
skip_logits=True,
return_hidden=last,
capture_layer_ids=list(taps) if last else None,
)
if last:
hidden = out.hidden_states[-1][0, -1].astype(mx.float32)
captured = {
str(i): self.lm.model.norm(h[:, -1:])[0, -1].astype(mx.float32)
for i, h in zip(sorted(set(taps)), out.hidden_states[:-1])
}
mx.eval(hidden, captured)
mx.eval([c.state for c in cache])
return hidden, captured
finally:
self.context["start"] = None
def prefill(self, parts):
with self.lock:
prefill_started = time.perf_counter()
text = self.render(parts, "X")
boundary = text.rfind("\n\nX")
if boundary < 0:
raise ValueError("Missing document boundary")
raw = self.t.encode(text[:boundary], add_special_tokens=False)
if "text" in parts[-1]:
raw = raw[:-1]
vision_started = time.perf_counter()
counts, grids, features = [], [], []
for part in parts:
if "image" not in part:
continue
with Image.open(part["image"]) as image:
processed = self.processor.image_processor(images=[image.convert("RGB")])
grid_np = np.asarray(processed["image_grid_thw"])
count = int(grid_np.prod()) // self.model.config.vision_config.spatial_merge_size**2
self.admit(len(raw) + sum(counts) + count - len(counts) - 1)
grid = mx.array(grid_np)
pixels = mx.array(np.asarray(processed["pixel_values"])).astype(
self.model.vision_tower.patch_embed.proj.weight.dtype
)
feature, _ = self.model.vision_tower(pixels, grid)
mx.eval(feature)
counts.append(count)
grids.append(grid)
features.append(feature)
vision_seconds = time.perf_counter() - vision_started if counts else 0.0
ids = self.expand(raw, counts)
self.admit(len(ids))
embeds, delta, feats, grid = None, 0, None, None
if counts:
feats, grid = mx.concatenate(features), mx.concatenate(grids)
f = self.model.get_input_embeddings(
mx.array([ids]), mx.zeros((1,)), image_grid_thw=grid, cached_image_features=feats
)
embeds, positions = f.inputs_embeds, f.position_ids
delta = int(np.asarray(f.rope_deltas).reshape(-1)[0])
if delta != int(mx.max(positions).item()) + 1 - len(ids):
raise ValueError("Multimodal RoPE offset mismatch")
else:
positions = self.positions(0, len(ids))
cache = self.lm.make_cache()
started = time.perf_counter()
self.forward(ids, positions, cache, embeds=embeds)
return {
"parts": copy.deepcopy(parts),
"prefix_ids": ids,
"cache": cache,
"counts": counts,
"rope_delta": delta,
"features": feats,
"grid": grid,
"positions": positions,
"prefill_seconds": time.perf_counter() - prefill_started,
"language_prefill_seconds": time.perf_counter() - started,
"vision_seconds": vision_seconds,
}
def ask(self, state, block, width, head, *, execution="cached", taps=()):
with self.lock:
if head not in self.heads or not 2 <= width <= 10:
raise ValueError("Unknown semantic head or invalid width")
ids = self.expand(
self.t.encode(self.render(state["parts"], block), add_special_tokens=False), state["counts"]
)
self.admit(len(ids))
p = len(state["prefix_ids"])
if ids[:p] != state["prefix_ids"]:
raise ValueError("Question token prefix differs from cached document")
started = time.perf_counter()
if execution == "cached":
hidden, captured = self.forward(
ids[p:],
self.positions(p + state["rope_delta"], len(ids) - p),
fork_cache(state["cache"]),
adapter_from=0,
taps=taps,
)
elif execution == "full":
embeds = None
if state["counts"]:
f = self.model.get_input_embeddings(
mx.array([ids]),
mx.zeros((1,)),
image_grid_thw=state["grid"],
cached_image_features=state["features"],
)
embeds, positions = f.inputs_embeds, f.position_ids
else:
positions = self.positions(0, len(ids))
hidden, captured = self.forward(
ids, positions, self.lm.make_cache(), embeds=embeds, adapter_from=p, taps=taps
)
else:
raise ValueError("Execution must be cached or full")
w, b = self.heads[head]
logits = (w @ hidden + b)[:width]
mx.eval(logits)
values = np.asarray(logits)
if not np.isfinite(values).all():
raise ValueError("Nonfinite trained-head output")
result = {
"letter_logits": values.tolist(),
"head_key": head,
"prompt_tokens": len(ids),
"branch_tokens": len(ids) - p,
"reused_prefix_tokens": p if execution == "cached" else 0,
"seconds": time.perf_counter() - started,
}
if taps:
result.update(
hidden=np.asarray(hidden).tolist(),
taps={k: np.asarray(v).tolist() for k, v in captured.items()},
token_ids=ids,
)
return result