devin-lai's picture
Release v0.4.0: selective MLX model downloads
3da88e3 verified
Raw History Blame Contribute Delete
7.25 kB
from __future__ import annotations
import gc
import hashlib
import importlib.metadata
import json
import logging
import os
from pathlib import Path
import mlx.core as mx
from transformers import AutoTokenizer
from .ops import attention, rms_norm, rotate, silu
from .weights import Linear, WeightStore
log = logging.getLogger(__name__)
class TextEncoder:
"""Qwen3-VL's first fifty language layers, with no final RMS normalization.
Layers stream one at a time. Conditioning is cached by weights, exact token
presentation and precision policy; it is independent of diffusion seed.
"""
def __init__(
self, path, *, cache_dir="cache", bits=None, group_size=64, query_chunk=0
):
self.path = Path(path)
self.config = json.loads((self.path / "config.json").read_text())
self.text_config = self.config["text_config"]
self.store = WeightStore(self.path)
self.tokenizer = AutoTokenizer.from_pretrained(
str(self.path), local_files_only=True
)
self.cache_dir = Path(cache_dir)
self.bits, self.group_size, self.query_chunk = bits, group_size, query_chunk
def encode(self, prompt, *, images=(), on_layer=None):
self.cache_hit = False
if not prompt.strip():
raise ValueError("Prompt must be nonempty")
if images:
from .vision import image_presentation
ids, tags, positions, vision = image_presentation(self, prompt, images)
else:
raw = self.tokenizer.encode(prompt, add_special_tokens=False)[:4096]
ids = raw + [0] * (-len(raw) % 8)
tags = mx.ones(len(raw), mx.int32)
positions = None
vision = None
identity = {
"version": 5,
"weights": self.store.fingerprint(),
"config": self.config,
"versions": {
p: importlib.metadata.version(p)
for p in ("mlx", "numpy", "Pillow", "transformers")
},
"ids": ids,
"bits": self.bits,
"group_size": self.group_size,
"query_chunk": self.query_chunk,
"tap": 50,
"tf32": os.environ.get("MLX_ENABLE_TF32"),
"images": [
hashlib.sha256(im.tobytes()).hexdigest() + str(im.size) for im in images
],
}
if images:
identity["vision_layer_norm_revision"] = 2
key = hashlib.sha256(json.dumps(identity, sort_keys=True).encode()).hexdigest()[
:24
]
target = self.cache_dir / f"conditioning-{key}.safetensors"
if target.exists():
data = mx.load(str(target))
mx.eval(data)
self.cache_hit = True
log.info(
"Conditioning cache hit: %d effective tokens", data["hidden"].shape[0]
)
return data["hidden"], data["tags"]
prefix = "model.language_model"
x = self.store.get(prefix + ".embed_tokens.weight", rows=ids)[None]
mx.eval(x)
if vision is not None:
from .vision import encode_vision
features, deep = encode_vision(
self.store, self.config["vision_config"], vision
)
image_indices = mx.array(
[i for i, t in enumerate(ids) if t == self.config["image_token_id"]]
)
x[0, image_indices] = features
cfg = self.text_config
dim, nq, nk = (
cfg["head_dim"],
cfg["num_attention_heads"],
cfg["num_key_value_heads"],
)
if positions is None:
positions = mx.broadcast_to(
mx.arange(len(ids), dtype=mx.float32)[None], (3, len(ids))
)
else:
positions = mx.array(positions, mx.float32)
inv = 1 / (cfg["rope_theta"] ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim))
angles = positions[:, :, None] * inv[None, None]
# Qwen3-VL interleaves spatial axes into the temporal frequency stream.
sections = cfg["rope_scaling"]["mrope_section"]
freq = angles[0]
for axis, offset in ((1, 1), (2, 2)):
stop = sections[axis] * 3
freq[:, offset:stop:3] = angles[axis, :, offset:stop:3]
freq = mx.concatenate((freq, freq), axis=-1)
rope = (
mx.cos(freq).astype(mx.bfloat16)[None, :, None],
mx.sin(freq).astype(mx.bfloat16)[None, :, None],
)
mx.eval(rope)
for i in range(50):
layer = f"{prefix}.layers.{i}"
def linear(suffix, layer=layer):
return Linear.load(
self.store,
layer + "." + suffix,
bits=self.bits,
group_size=self.group_size,
)
qproj, kproj, vproj = (
linear("self_attn." + p + "_proj") for p in ("q", "k", "v")
)
oproj = linear("self_attn.o_proj")
h = rms_norm(
x,
self.store.get(layer + ".input_layernorm.weight"),
cfg["rms_norm_eps"],
rounded=True,
)
q = qproj(h).reshape(1, len(ids), nq, dim)
k = kproj(h).reshape(1, len(ids), nk, dim)
v = vproj(h).reshape(1, len(ids), nk, dim)
q = rms_norm(
q,
self.store.get(layer + ".self_attn.q_norm.weight"),
cfg["rms_norm_eps"],
rounded=True,
)
k = rms_norm(
k,
self.store.get(layer + ".self_attn.k_norm.weight"),
cfg["rms_norm_eps"],
rounded=True,
)
q, k = rotate(q, *rope), rotate(k, *rope)
h = attention(q, k, v, causal=True, query_chunk=self.query_chunk).reshape(
1, len(ids), -1
)
x = x + oproj(h)
mx.eval(x)
del qproj, kproj, vproj, oproj, q, k, v, h
gate, up, down = (
linear("mlp." + p + "_proj") for p in ("gate", "up", "down")
)
h = rms_norm(
x,
self.store.get(layer + ".post_attention_layernorm.weight"),
cfg["rms_norm_eps"],
rounded=True,
)
x = x + down(silu(gate(h)) * up(h))
if vision is not None and i < len(deep):
x = x.at[0, image_indices].add(deep[i])
mx.eval(x)
del gate, up, down, h
if on_layer:
on_layer(i, x)
if (i + 1) % 10 == 0:
log.info("Text encoder %d/50", i + 1)
mx.clear_cache()
hidden = x[0, : len(tags)].astype(mx.bfloat16)
mx.eval(hidden)
self.cache_dir.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(".tmp.safetensors")
mx.save_safetensors(
str(tmp),
{"hidden": hidden, "tags": tags},
metadata={"identity": json.dumps(identity)},
)
tmp.replace(target)
gc.collect()
mx.clear_cache()
return hidden, tags