"""MLX implementation of the DiffusionGemma text model (encoder + block-diffusion decoder). Shape conventions: activations are [B, L, hidden]; attention is [B, heads, L, head_dim]; the KV cache keeps [B, heads, S, head_dim] per layer. Weight names follow the HF checkpoint (``model.decoder.*``), which is also where the encoder's weights live (in the checkpoint the encoder shares them). A linear weight is either a float array ``X.weight`` or NVFP4-packed ``X.weight`` (uint32, 8 nibbles per word) + ``X.scales`` (uint8, group 16). Batch size is 1: the port serves one request at a time, so no padding/batch masks are needed. """ from __future__ import annotations import math from typing import Any import mlx.core as mx from .config import DiffusionGemmaConfig, TextConfig NVFP4 = dict(group_size=16, bits=4, mode="nvfp4") # --------------------------------------------------------------------------- weights def _packed(sd: dict[str, mx.array], name: str) -> tuple[mx.array, mx.array | None, mx.array | None]: """(weight, scales, global_scale) for a checkpoint linear name ending in ``.weight``.""" stem = name[: -len(".weight")] if name.endswith(".weight") else name return sd[name], sd.get(stem + ".scales"), sd.get(stem + ".global_scale") class Linear: """``x @ W.T`` for a float or NVFP4-packed weight.""" def __init__(self, weight: mx.array, scales: mx.array | None = None, global_scale: mx.array | None = None): self.weight = weight self.scales = scales self.global_scale = global_scale def __call__(self, x: mx.array) -> mx.array: if self.scales is None: return x @ self.weight.T if self.global_scale is not None: # mlx 0.32.4 has no global_scale on quantized_matmul (only gather_qmm); no plain linear in # this checkpoint is packed, so this is a guard, not a path. raise TypeError("packed Linear with a global scale is not supported") return mx.quantized_matmul(x, self.weight, scales=self.scales, transpose=True, **NVFP4) class RMSNorm: """DiffusionGemma RMSNorm: ``x / sqrt(mean(x^2) + eps) * w``, computed in float32.""" def __init__(self, weight: mx.array | None, eps: float): self.weight = weight # None -> no scale (v_norm, router norm, self-conditioning post norm) self.eps = eps def __call__(self, x: mx.array) -> mx.array: dtype = x.dtype x = x.astype(mx.float32) x = x * mx.power(mx.mean(x * x, axis=-1, keepdims=True) + self.eps, -0.5) if self.weight is not None: x = x * self.weight.astype(mx.float32) return x.astype(dtype) def gelu_tanh(x: mx.array) -> mx.array: return 0.5 * x * (1.0 + mx.tanh(0.7978845608028654 * (x + 0.044715 * x * x * x))) # --------------------------------------------------------------------------- attention def rope_tables(text: TextConfig, layer_type: str, positions: mx.array) -> tuple[mx.array, mx.array]: """cos/sin of shape [L, head_dim/2]; both head halves rotate with the same angles (reference layout).""" params = text.rope_parameters[layer_type] head_dim = text.global_head_dim if layer_type == "full_attention" else text.head_dim base = params["rope_theta"] factor = params.get("partial_rotary_factor", 1.0) if params.get("rope_type", "default") == "proportional": # transformers' proportional RoPE: rotate the first dims, zero frequencies pad the rest. angles = int(factor * head_dim // 2) exponents = mx.arange(0, 2 * angles, 2, dtype=mx.float32) / head_dim inv_freq = mx.concatenate( [1.0 / (base**exponents), mx.zeros(head_dim // 2 - angles, dtype=mx.float32)] ) else: dim = int(factor * head_dim) inv_freq = 1.0 / (base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim)) freqs = positions.astype(mx.float32)[:, None] * inv_freq[None, :] return mx.cos(freqs), mx.sin(freqs) def apply_rope(x: mx.array, cos: mx.array, sin: mx.array) -> mx.array: """x [B, L, H, D], cos/sin [L, D/2].""" half = x.shape[-1] // 2 cos, sin = cos[None, :, None, :], sin[None, :, None, :] x1, x2 = x[..., :half], x[..., half:] return mx.concatenate([x1 * cos - x2 * sin, x1 * sin + x2 * cos], axis=-1) class KVCache: """Per-layer prefix cache. Sliding layers keep only ``sliding_window - 1`` tokens, like the reference.""" def __init__(self, text: TextConfig): n = text.num_hidden_layers self.text = text self.keys: list[mx.array | None] = [None] * n self.values: list[mx.array | None] = [None] * n self.seen = [0] * n # tokens encoded so far; can exceed the kept length on sliding layers def append(self, i: int, k: mx.array, v: mx.array) -> tuple[mx.array, mx.array]: """Append [B, H, L, D] states; returns all states visible to this forward.""" self.seen[i] += k.shape[2] if self.keys[i] is not None: k = mx.concatenate([self.keys[i], k], axis=2) v = mx.concatenate([self.values[i], v], axis=2) if self.text.is_sliding(i): keep = self.text.sliding_window - 1 self.keys[i] = k[:, :, -keep:, :] if k.shape[2] > keep else k self.values[i] = v[:, :, -keep:, :] if v.shape[2] > keep else v else: self.keys[i], self.values[i] = k, v return k, v def prefix(self, i: int) -> tuple[mx.array | None, mx.array | None, int]: """Cached K/V and the absolute position of the first cached token.""" if self.keys[i] is None: return None, None, 0 return self.keys[i], self.values[i], self.seen[i] - self.keys[i].shape[2] def copy(self) -> KVCache: """Shallow copy: ``append`` always replaces entries, so a copy can be extended without touching this one.""" other = KVCache.__new__(KVCache) other.text = self.text other.keys = list(self.keys) other.values = list(self.values) other.seen = list(self.seen) return other class Attention: def __init__(self, text: TextConfig, layer_idx: int, sd: dict[str, mx.array], prefix: str): self.layer_idx = layer_idx self.head_dim = text.layer_head_dim(layer_idx) self.num_heads = text.num_attention_heads self.num_kv_heads = text.layer_num_kv_heads(layer_idx) self.scaling = 1.0 # the reference does not scale by 1/sqrt(head_dim) self.q_proj = Linear(*_packed(sd, f"{prefix}.q_proj.weight")) self.k_proj = Linear(*_packed(sd, f"{prefix}.k_proj.weight")) v_proj = f"{prefix}.v_proj.weight" # Full-attention layers have no v_proj: V is K after k_norm + rope. self.v_proj = Linear(*_packed(sd, v_proj)) if v_proj in sd else None self.o_proj = Linear(*_packed(sd, f"{prefix}.o_proj.weight")) eps = text.rms_norm_eps self.q_norm = RMSNorm(sd[f"{prefix}.q_norm.weight"], eps) self.k_norm = RMSNorm(sd[f"{prefix}.k_norm.weight"], eps) self.v_norm = RMSNorm(None, eps) # no scale def __call__(self, x, cos, sin, mask, cache: KVCache, write: bool) -> mx.array: b, length, _ = x.shape hd, nh, nkv = self.head_dim, self.num_heads, self.num_kv_heads cos, sin = cos.astype(x.dtype), sin.astype(x.dtype) q = apply_rope(self.q_norm(self.q_proj(x).reshape(b, length, nh, hd)), cos, sin).transpose(0, 2, 1, 3) k = self.k_proj(x).reshape(b, length, nkv, hd) v = k if self.v_proj is None else self.v_proj(x).reshape(b, length, nkv, hd) k = apply_rope(self.k_norm(k), cos, sin) v = self.v_norm(v) k, v = k.transpose(0, 2, 1, 3), v.transpose(0, 2, 1, 3) if write: # encoder: append to the prefix cache k, v = cache.append(self.layer_idx, k, v) else: # diffusion decoder: cached prefix + canvas, read-only pk, pv, _ = cache.prefix(self.layer_idx) if pk is not None: if pk.shape[0] != b: # batched draws share the one encoded prefix pk = mx.broadcast_to(pk, (b,) + pk.shape[1:]) pv = mx.broadcast_to(pv, (b,) + pv.shape[1:]) k = mx.concatenate([pk, k], axis=2) v = mx.concatenate([pv, v], axis=2) out = mx.fast.scaled_dot_product_attention( q, k, v, scale=self.scaling, mask=mask ) return self.o_proj(out.transpose(0, 2, 1, 3).reshape(b, length, nh * hd)) # --------------------------------------------------------------------------- MoE class Router: def __init__(self, text: TextConfig, sd: dict[str, mx.array], prefix: str): self.top_k = text.top_k_experts self.scalar_root_size = text.hidden_size**-0.5 self.norm = RMSNorm(None, text.rms_norm_eps) self.proj = Linear(*_packed(sd, f"{prefix}.proj.weight")) self.scale = sd[f"{prefix}.scale"] self.per_expert_scale = sd[f"{prefix}.per_expert_scale"] def __call__(self, x: mx.array) -> tuple[mx.array, mx.array]: """x [T, hidden] -> (top-k expert ids, top-k weights), both [T, top_k].""" h = self.norm(x) * self.scale * self.scalar_root_size probs = mx.softmax(self.proj(h).astype(mx.float32), axis=-1) idx = mx.argpartition(-probs, kth=self.top_k, axis=-1)[..., : self.top_k] weights = mx.take_along_axis(probs, idx, axis=-1) order = mx.argsort(-weights, axis=-1) # descending, like torch.topk idx = mx.take_along_axis(idx, order, axis=-1) weights = mx.take_along_axis(weights, order, axis=-1) weights = weights / weights.sum(axis=-1, keepdims=True) return idx, weights * self.per_expert_scale[idx] def _expert_weights(sd: dict[str, mx.array], prefix: str, num_experts: int, kinds: list[str]): """Stacked (weight, scales, global_scale) over experts, one entry per kind concatenated on the output axis. Takes the converted artifact's pre-stacked tensors as they are (``experts.gate_up_proj`` / ``experts.down_proj``, with ``global_scale``) and falls back to stacking the checkpoint's per-expert layout (``experts.{e}.gate_proj`` etc.) for float weights, as the tiny parity tests need. """ base = f"{prefix}.experts" if f"{base}.{kinds[0]}.weight" in sd: return _packed(sd, f"{base}.{kinds[0]}.weight") if f"{base}.{kinds[0]}" in sd: # reference layout: one Parameter, no suffix (packed or float) name = f"{base}.{kinds[0]}" if sd[name].dtype in (mx.uint32, mx.uint8): return sd[name], sd.get(name + ".scales"), sd.get(name + ".global_scale") return sd[name], None, None weights, scales = [], [] for e in range(num_experts): parts = [_packed(sd, f"{base}.{e}.{kind}.weight") for kind in kinds] weights.append(mx.concatenate([p[0] for p in parts], axis=0)) scales.append(None if parts[0][1] is None else mx.concatenate([p[1] for p in parts], axis=0)) return mx.stack(weights), None if scales[0] is None else mx.stack(scales), None class Experts: """128-expert MLP; gate and up stacked on the output axis, as in the reference's ``gate_up_proj``.""" def __init__(self, text: TextConfig, sd: dict[str, mx.array], prefix: str): n = text.num_experts kinds = ( ["gate_up_proj"] if f"{prefix}.experts.gate_up_proj.weight" in sd or f"{prefix}.experts.gate_up_proj" in sd else ["gate_proj", "up_proj"] ) self.gate_up, self.gate_up_scales, self.gate_up_gs = _expert_weights(sd, prefix, n, kinds) self.down, self.down_scales, self.down_gs = _expert_weights(sd, prefix, n, ["down_proj"]) def __call__(self, x: mx.array, idx: mx.array, weights: mx.array) -> mx.array: """x [T, hidden] with idx/weights [T, top_k] -> [T, hidden]. Expert assignments are sorted by expert before the gathers: unsorted nvfp4 gather_qmm is ~9x slower here (measured on this Mac, 9 ms vs 1 ms per call), because the kernel cannot stream the weights. """ t, k = idx.shape flat = idx.reshape(-1) order = mx.argsort(flat) idx_s = mx.take(flat, order) xs = mx.take(x, order // k, axis=0) gate_up = self._mm(xs, self.gate_up, self.gate_up_scales, self.gate_up_gs, idx_s) gate, up = mx.split(gate_up, 2, axis=-1) h = gelu_tanh(gate) * up out = self._mm(h.reshape(t * k, -1), self.down, self.down_scales, self.down_gs, idx_s) out = mx.take(out, mx.argsort(order), axis=0).reshape(t, k, -1) # router probabilities are float32; promote the 9x bigger activation tensor instead of the weights return (out * weights[..., None].astype(out.dtype)).sum(axis=-2) @staticmethod def _mm(x: mx.array, w: mx.array, scales: mx.array | None, gs: mx.array | None, idx: mx.array) -> mx.array: """x [N, in] with sorted idx [N] expert ids -> [N, out].""" x = mx.expand_dims(x, -2) if scales is None: out = mx.gather_mm(x, w.swapaxes(-1, -2), rhs_indices=idx, sorted_indices=True) else: out = mx.gather_qmm( x, w, scales, rhs_indices=idx, transpose=True, sorted_indices=True, global_scale=gs, **NVFP4 ) return out[:, 0, :] # --------------------------------------------------------------------------- transformer blocks class Layer: def __init__(self, text: TextConfig, layer_idx: int, sd: dict[str, mx.array], prefix: str): self.self_attn = Attention(text, layer_idx, sd, f"{prefix}.self_attn") self.gate_proj = Linear(*_packed(sd, f"{prefix}.mlp.gate_proj.weight")) self.up_proj = Linear(*_packed(sd, f"{prefix}.mlp.up_proj.weight")) self.down_proj = Linear(*_packed(sd, f"{prefix}.mlp.down_proj.weight")) eps = text.rms_norm_eps self.input_layernorm = RMSNorm(sd[f"{prefix}.input_layernorm.weight"], eps) self.post_attention_layernorm = RMSNorm(sd[f"{prefix}.post_attention_layernorm.weight"], eps) self.pre_feedforward_layernorm = RMSNorm(sd[f"{prefix}.pre_feedforward_layernorm.weight"], eps) self.pre_feedforward_layernorm_2 = RMSNorm(sd[f"{prefix}.pre_feedforward_layernorm_2.weight"], eps) self.post_feedforward_layernorm = RMSNorm(sd[f"{prefix}.post_feedforward_layernorm.weight"], eps) self.post_feedforward_layernorm_1 = RMSNorm(sd[f"{prefix}.post_feedforward_layernorm_1.weight"], eps) self.post_feedforward_layernorm_2 = RMSNorm(sd[f"{prefix}.post_feedforward_layernorm_2.weight"], eps) self.layer_scalar = sd[f"{prefix}.layer_scalar"] self.router = Router(text, sd, f"{prefix}.router") self.experts = Experts(text, sd, prefix) def __call__(self, x, cos, sin, mask, cache: KVCache, write: bool) -> mx.array: attn = self.self_attn(self.input_layernorm(x), cos, sin, mask, cache, write) residual = x + self.post_attention_layernorm(attn) h = self.pre_feedforward_layernorm(residual) dense = self.post_feedforward_layernorm_1(self.down_proj(gelu_tanh(self.gate_proj(h)) * self.up_proj(h))) flat = residual.reshape(-1, residual.shape[-1]) # Routing reads the raw residual; the experts read its pre-normalised version. idx, weights = self.router(flat) moe = self.experts(self.pre_feedforward_layernorm_2(flat), idx, weights) moe = self.post_feedforward_layernorm_2(moe.reshape(residual.shape)) return (residual + self.post_feedforward_layernorm(dense + moe)) * self.layer_scalar class SelfConditioning: def __init__(self, text: TextConfig, sd: dict[str, mx.array], prefix: str): eps = text.rms_norm_eps self.pre_norm = RMSNorm(sd[f"{prefix}.pre_norm.weight"], eps) self.post_norm = RMSNorm(None, eps) self.gate_proj = Linear(*_packed(sd, f"{prefix}.gate_proj.weight")) self.up_proj = Linear(*_packed(sd, f"{prefix}.up_proj.weight")) self.down_proj = Linear(*_packed(sd, f"{prefix}.down_proj.weight")) def __call__(self, inputs_embeds: mx.array, signal: mx.array) -> mx.array: h = self.pre_norm(signal) h = self.down_proj(gelu_tanh(self.gate_proj(h)) * self.up_proj(h)) return self.post_norm(inputs_embeds + h) # --------------------------------------------------------------------------- model class DiffusionGemma: """Text-only DiffusionGemma: an encoder that writes a prefix KV cache, and a diffusion decoder.""" def __init__(self, config: DiffusionGemmaConfig, state_dict: dict[str, Any]): text = config.text_config self.config = config self.text = text sd = {k: (v if isinstance(v, mx.array) else mx.array(v)) for k, v in state_dict.items()} self.embed_tokens = sd["model.decoder.embed_tokens.weight"] self.lm_head = sd.get("lm_head.weight", self.embed_tokens) # The reference downcasts sqrt(hidden_size) to the embedding dtype (bf16 -> 53.0 for 2816). self.embed_scale = mx.array(math.sqrt(text.hidden_size)).astype(self.embed_tokens.dtype) self.layers = [Layer(text, i, sd, f"model.decoder.layers.{i}") for i in range(text.num_hidden_layers)] self.norm = RMSNorm(sd["model.decoder.norm.weight"], text.rms_norm_eps) self.self_conditioning = SelfConditioning(text, sd, "model.decoder.self_conditioning") # -- encoder ----------------------------------------------------------------- def _encoder_mask(self, layer_idx: int, positions: mx.array, kv_len: int, kv_pos0: int, block_ids): """Causal (+ sliding window) mask, with bidirectional groups where ``block_ids`` agree.""" text = self.text kv_pos = mx.arange(kv_len) + kv_pos0 allowed = kv_pos[None, :] <= positions[:, None] if text.is_sliding(layer_idx): allowed &= kv_pos[None, :] > (positions - text.sliding_window)[:, None] if block_ids is not None: # keys already in the cache belong to no group of this chunk (-1), so they stay causal pad = kv_len - block_ids.shape[0] keys_ids = mx.concatenate([mx.full((pad,), -1, mx.int32), block_ids]) if pad > 0 else block_ids allowed |= (block_ids[:, None] == keys_ids[None, :]) & (block_ids[:, None] >= 0) # the dtype the attention kernel wants; the mask is cached per layer type, so cast it once here return mx.where(allowed, 0.0, -mx.inf)[None, None].astype(self.embed_tokens.dtype) def encode( self, input_ids: mx.array, positions: mx.array, cache: KVCache | None = None, block_ids: mx.array | None = None, ) -> tuple[mx.array, KVCache]: """Encode ``input_ids`` into the prefix cache; returns (last_hidden_state, cache). ``block_ids`` (one int per token) marks groups that attend bidirectionally inside the group -- the per-question blocks of the reference's ``create_masks_for_generate``; ``-1`` is plain causal text. As in the reference, they apply to the prefill only (they are dropped for later canvases). """ text = self.text cache = cache if cache is not None else KVCache(text) if block_ids is not None and cache.seen[0] != 0 and block_ids.shape[0] != input_ids.shape[1]: raise ValueError("block_ids must cover exactly the tokens being encoded") x = self.embed_tokens[input_ids] * self.embed_scale ropes, masks = {}, {} for i, layer in enumerate(self.layers): layer_type = text.layer_types[i] if layer_type not in ropes: ropes[layer_type] = rope_tables(text, layer_type, positions) _, _, kv_pos0 = cache.prefix(i) kv_len = cache.seen[i] - kv_pos0 + input_ids.shape[1] masks[layer_type] = self._encoder_mask(i, positions, kv_len, kv_pos0, block_ids) x = layer(x, *ropes[layer_type], masks[layer_type] if text.is_causal else None, cache, write=True) return self.norm(x), cache # -- decoder ----------------------------------------------------------------- def denoise( self, canvas_ids: mx.array, cache: KVCache, positions: mx.array, self_conditioning_logits: mx.array | None = None, read_positions: mx.array | None = None, vocab_ids: mx.array | None = None, ) -> mx.array: """One denoiser forward over the canvas; returns softcapped logits [B, canvas, vocab] in float32. ``read_positions``/``vocab_ids`` project only those canvas rows onto those vocabulary ids, which is what a constrained read needs and saves the full-vocabulary unembedding of every canvas position. """ text = self.text x = self.embed_tokens[canvas_ids] * self.embed_scale if self_conditioning_logits is not None: probs = mx.softmax(self_conditioning_logits.astype(mx.float32), axis=-1) soft = (probs.astype(self.embed_tokens.dtype) @ self.embed_tokens) * self.embed_scale else: soft = mx.zeros_like(x) x = self.self_conditioning(x, soft) ropes = {} for i, layer in enumerate(self.layers): layer_type = text.layer_types[i] if layer_type not in ropes: ropes[layer_type] = rope_tables(text, layer_type, positions) x = layer(x, *ropes[layer_type], None, cache, write=False) h = self.norm(x) if read_positions is not None: h = mx.take(h, read_positions, axis=1) head = self.lm_head if vocab_ids is None else mx.take(self.lm_head, vocab_ids, axis=0) logits = (h @ head.T).astype(mx.float32) return mx.tanh(logits / text.final_logit_softcapping) * text.final_logit_softcapping