Instructions to use FluidInference/d3-coreml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use FluidInference/d3-coreml with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download FluidInference/d3-coreml --local-dir d3-coreml
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
File size: 6,643 Bytes
78a1f66 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | """d3 text and video requests on MLX (Apple GPU): upstream prompt and answer code; mlx-vlm's Qwen3.5 vision tower and language
model; all questions of a request in ONE batched forward (right-padded rows, key-padding mask on the noncausal
full-attention layers, causal Gated DeltaNet); FP32 readout on the host.
m = D3MLXVideo("<d3 checkpoint>", "<mlx dir: renamed weights + config>", max_pixels=100_352)
m.system_one(state=..., questions={...}, videos=[frames]) # or videos=None for a text request"""
import sys, time
from pathlib import Path
import mlx.core as mx
import numpy as np
import torch
_KEY_MASK = None
_LAYER_TYPES = None
def _patch(layer_types):
"""Two fixes to mlx-vlm's Qwen3.5 for d3. (1) Layer kinds come from the checkpoint's `layer_types`: mlx-vlm
assumes full attention every `full_attention_interval` layers, but d3-edge has it at layers 2, 5, 8-11, so half
its layers were built as the wrong kind and ran with random weights. (2) d3's full-attention layers are noncausal
(`noncausal_full_attention`): every real token is visible, only padding keys are masked.
Imported late: importing mlx_vlm's qwen3_5 registers its own Qwen3-VL processor, which upstream d3's request
preparation must not get."""
global _LAYER_TYPES
_LAYER_TYPES = list(layer_types)
import mlx_vlm.models.qwen3_5.language as qlang
if not getattr(qlang, "_d3_patched", False):
init = qlang.Qwen3_5DecoderLayer.__init__
def layer_init(self, args, layer_idx):
if _LAYER_TYPES is not None:
kind = _LAYER_TYPES[layer_idx]
# the interval the stock __init__ consults; make it agree with this layer's kind
args.full_attention_interval = 1 if kind == "full_attention" else 10 ** 6
init(self, args, layer_idx)
causal = qlang._create_qwen3_5_attention_mask
def noncausal(h, cache):
return _KEY_MASK if _KEY_MASK is not None else causal(h, cache)
qlang.Qwen3_5DecoderLayer.__init__ = layer_init
qlang._create_qwen3_5_attention_mask = noncausal
qlang._d3_patched = True
class D3MLXVideo:
def __init__(self, root, mlx_dir, max_pixels=None):
root = Path(root)
sys.path.insert(0, str(root))
import d3_runtime as up
if max_pixels:
up.VIDEO_MAX_PIXELS = max_pixels
from safetensors.torch import load_file
self.up = up
self.prep = up.D3.from_pretrained(str(root), device="meta", verify="none") # prompts and positions only
self.processor, self.temperature, self.backbone = self.prep.processor, self.prep.temperature, self.prep.backbone
self.readout = load_file(str(root / "readout.safetensors"))["weight"].float().numpy()
self.video_token = self.backbone.config.video_token_id
import json
_patch(json.loads((root / "config.json").read_text())["text_config"]["layer_types"])
from mlx_vlm.utils import load_model
self.model = load_model(Path(mlx_dir), strict=True)
self.lm = self.model.language_model.model
self.model_name = root.name + "-mlx"
self.timing = {}
def system_one(self, *, state, questions, videos=None):
global _KEY_MASK
up, proc = self.up, self.processor
t0 = time.perf_counter()
prepared = self.prep.prepare(state, questions, videos=videos)
vis = None
if videos:
media = prepared.media
grids = media["video_grid_thw"]
pv = mx.array(media["pixel_values_videos"].float().numpy())
vis, _ = self.model.vision_tower(pv, mx.array(grids.numpy()))
mx.eval(vis) # MLX is lazy: time the vision tower here
t1 = time.perf_counter()
rows, keys = [], []
for key in prepared.runnable:
if videos:
expanded = proc.get_text_with_replacements([prepared.texts[key]], [], prepared.video_texts)[0][0]
enc = proc.tokenizer([expanded], return_token_type_ids=False, return_tensors="pt")
ids = enc["input_ids"]
types = torch.as_tensor(proc.create_mm_token_type_ids(ids))
with torch.no_grad():
pos = self.backbone.compute_3d_position_ids(
input_ids=ids, inputs_embeds=None, image_grid_thw=None, video_grid_thw=grids,
attention_mask=enc["attention_mask"], past_key_values=None, mm_token_type_ids=types)[:, 0]
rows.append((ids[0].numpy(), pos.numpy()))
else: # text: upstream's token ids, positions 0..n-1 on all three axes
row = np.array(prepared.sequences[key], np.int32)
rows.append((row, np.tile(np.arange(len(row), dtype=np.int32)[None], (3, 1))))
keys.append(key)
B, L = len(rows), max(len(r[0]) for r in rows)
ids = np.zeros((B, L), np.int32); pos = np.zeros((3, B, L), np.int32); valid = np.zeros((B, L), bool)
lengths = []
for b, (r, p) in enumerate(rows):
n = len(r); lengths.append(n)
ids[b, :n] = r; ids[b, n:] = r[-1]
pos[:, b, :n] = p; pos[:, b, n:] = p[:, -1:]
valid[b, :n] = True
emb = self.lm.embed_tokens(mx.array(ids))
if vis is not None:
is_video = mx.array(ids == self.video_token)
vis = vis.astype(emb.dtype)
per_row = int(is_video[0].sum().item())
# every row carries the same video tokens, in the same order
idx = mx.cumsum(is_video.astype(mx.int32), axis=1) - 1
emb = mx.where(is_video[..., None], vis[mx.clip(idx, 0, per_row - 1)], emb)
_KEY_MASK = mx.array(valid)[:, None, None, :]
try:
h = self.lm(None, inputs_embeds=emb, position_ids=mx.array(pos))
finally:
_KEY_MASK = None
last = mx.stack([h[b, lengths[b] - 1] for b in range(B)]).astype(mx.float32)
hs = np.array(last)
t2 = time.perf_counter()
answers, used = {}, 0
for b, key in enumerate(keys):
question = prepared.questions[key]
logits = self.readout[:len(question.keys)].astype(np.float64) @ hs[b].astype(np.float64) / self.temperature
p = np.exp(logits - logits.max())
answers[key] = up.product_answer(question, (p / p.sum()).tolist())
used += lengths[b]
self.timing = {"prepare+vision": round(t1 - t0, 3), "text": round(t2 - t1, 3)}
return {"model": self.model_name, "answers": answers, "usage": {"input_tokens": used, "output_tokens": 0}}
|