Download nano/python/qwen35_export.py from FluidInference/d3-coreml: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/FluidInference/d3-coreml/resolve/main/nano/python/qwen35_export.py
- Command line
-
hf download hf://FluidInference/d3-coreml/nano/python/qwen35_export.py
-
curl -L -o qwen35_export.py https://huggingface.co/FluidInference/d3-coreml/resolve/main/nano/python/qwen35_export.py
16 kB
| """Trace-friendly, prefill-only Qwen3.5 text decoder (copied from models/computer-use/cua-s1-4b/coreml, mobius PR #104). | |
| Cua-S1-4B reads one forward pass: the logits of the answer letters A..Z at the | |
| last prompt position. Nothing is generated, so this graph has no KV cache, no | |
| recurrent-state I/O and no full vocabulary head: | |
| - inputs are `inputs_embeds` (host-side embedding gather, so a multimodal | |
| caller can splice vision features in) plus host-computed M-RoPE `cos`/`sin`; | |
| - prompts are right-padded to a fixed bucket; the model is causal and the gated | |
| delta rule is a forward scan, so padding after the last real token cannot | |
| change anything before it; | |
| - the last chunk gathers the hidden state at `last_index` (one-hot matmul), | |
| applies the final norm and multiplies by the 26 tied-embedding rows of the | |
| letter tokens. | |
| The gated delta rule uses the chunked (WY/UT) form. The unit-lower-triangular | |
| solve is a recursive 2x2 block inverse (log2(chunk) batched matmul levels). The | |
| nilpotent power series (I + N)(I + N^2)... is exact algebraically but its terms | |
| grow combinatorially before cancelling and overflow on real Qwen3.5 layers. | |
| Within-chunk decay differences are computed as masked sums of the per-step | |
| decays instead of differences of cumulative sums, which keeps them accurate in | |
| fp16 when the cumulative decay is large. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import torch | |
| import torch.nn.functional as F | |
| from torch import nn | |
| class TextConfig: | |
| def __init__(self, cfg: dict): | |
| self.hidden_size = cfg["hidden_size"] | |
| self.intermediate_size = cfg["intermediate_size"] | |
| self.num_layers = cfg["num_hidden_layers"] | |
| self.layer_types = cfg["layer_types"] | |
| self.eps = cfg["rms_norm_eps"] | |
| self.num_heads = cfg["num_attention_heads"] | |
| self.num_kv_heads = cfg["num_key_value_heads"] | |
| self.head_dim = cfg["head_dim"] | |
| self.lin_k_heads = cfg["linear_num_key_heads"] | |
| self.lin_v_heads = cfg["linear_num_value_heads"] | |
| self.lin_k_dim = cfg["linear_key_head_dim"] | |
| self.lin_v_dim = cfg["linear_value_head_dim"] | |
| self.conv_kernel = cfg["linear_conv_kernel_dim"] | |
| rope = cfg["rope_parameters"] | |
| self.rope_theta = rope["rope_theta"] | |
| self.rotary_dim = int(self.head_dim * rope.get("partial_rotary_factor", 1.0)) | |
| self.mrope_section = rope.get("mrope_section", [11, 11, 10]) | |
| def rope_cos_sin(cfg: TextConfig, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Interleaved M-RoPE tables. `position_ids` is [3, L] (t, h, w); returns [L, rotary_dim] fp32 each. | |
| Mirrors `Qwen3_5TextRotaryEmbedding` (computed in float64 then cast, host side).""" | |
| dim = cfg.rotary_dim | |
| inv_freq = 1.0 / (cfg.rope_theta ** (torch.arange(0, dim, 2, dtype=torch.float64) / dim)) | |
| freqs = position_ids.to(torch.float64)[:, :, None] * inv_freq[None, None, :] # [3, L, dim/2] | |
| thw = freqs[0].clone() | |
| for axis, offset in ((1, 1), (2, 2)): | |
| length = cfg.mrope_section[axis] * 3 | |
| thw[:, offset:length:3] = freqs[axis][:, offset:length:3] | |
| emb = torch.cat([thw, thw], dim=-1) | |
| return emb.cos().float(), emb.sin().float() | |
| class RMSNorm(nn.Module): | |
| """Qwen3.5 zero-centred RMSNorm: x * rsqrt(mean(x^2)+eps) * (1 + w).""" | |
| def __init__(self, dim: int, eps: float): | |
| super().__init__() | |
| self.eps = eps | |
| self.weight = nn.Parameter(torch.zeros(dim)) | |
| def forward(self, x): | |
| x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) | |
| return x * (1.0 + self.weight) | |
| class MLP(nn.Module): | |
| def __init__(self, cfg: TextConfig): | |
| super().__init__() | |
| self.gate_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False) | |
| self.up_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False) | |
| self.down_proj = nn.Linear(cfg.intermediate_size, cfg.hidden_size, bias=False) | |
| def forward(self, x): | |
| return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) | |
| def rotate_half(x): | |
| half = x.shape[-1] // 2 | |
| return torch.cat([-x[..., half:], x[..., :half]], dim=-1) | |
| class FullAttention(nn.Module): | |
| def __init__(self, cfg: TextConfig): | |
| super().__init__() | |
| self.cfg = cfg | |
| h, kv, d = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim | |
| self.q_proj = nn.Linear(cfg.hidden_size, h * d * 2, bias=False) | |
| self.k_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False) | |
| self.v_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False) | |
| self.o_proj = nn.Linear(h * d, cfg.hidden_size, bias=False) | |
| self.q_norm = RMSNorm(d, cfg.eps) | |
| self.k_norm = RMSNorm(d, cfg.eps) | |
| def forward(self, x, cos, sin, mask): | |
| cfg = self.cfg | |
| L = x.shape[1] | |
| h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim | |
| qg = self.q_proj(x).view(1, L, h, 2 * d) | |
| q, gate = qg[..., :d], qg[..., d:] | |
| gate = gate.reshape(1, L, h * d) | |
| q = self.q_norm(q).transpose(1, 2) # [1, h, L, d] | |
| k = self.k_norm(self.k_proj(x).view(1, L, kv, d)).transpose(1, 2) | |
| v = self.v_proj(x).view(1, L, kv, d).transpose(1, 2) | |
| c, s = cos[None, None], sin[None, None] # [1, 1, L, r] | |
| q = torch.cat([q[..., :r] * c + rotate_half(q[..., :r]) * s, q[..., r:]], dim=-1) | |
| k = torch.cat([k[..., :r] * c + rotate_half(k[..., :r]) * s, k[..., r:]], dim=-1) | |
| rep = h // kv | |
| k = k[:, :, None].expand(1, kv, rep, L, d).reshape(1, h, L, d) | |
| v = v[:, :, None].expand(1, kv, rep, L, d).reshape(1, h, L, d) | |
| scores = torch.matmul(q, k.transpose(-1, -2)) * (d**-0.5) + mask | |
| out = torch.matmul(torch.softmax(scores, dim=-1), v) # [1, h, L, d] | |
| out = out.transpose(1, 2).reshape(1, L, h * d) | |
| return self.o_proj(out * torch.sigmoid(gate)) | |
| class GatedRMSNorm(nn.Module): | |
| def __init__(self, dim: int, eps: float): | |
| super().__init__() | |
| self.eps = eps | |
| self.weight = nn.Parameter(torch.ones(dim)) | |
| def forward(self, x, gate): | |
| x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) | |
| return self.weight * x * F.silu(gate) | |
| class GatedDeltaNet(nn.Module): | |
| def __init__(self, cfg: TextConfig, seq_len: int, chunk_size: int): | |
| super().__init__() | |
| self.cfg = cfg | |
| self.chunk = chunk_size | |
| assert seq_len % chunk_size == 0, "bucket length must be a multiple of the delta-rule chunk size" | |
| self.num_chunks = seq_len // chunk_size | |
| kd = cfg.lin_k_heads * cfg.lin_k_dim | |
| vd = cfg.lin_v_heads * cfg.lin_v_dim | |
| self.key_dim, self.value_dim = kd, vd | |
| self.conv_dim = 2 * kd + vd | |
| self.in_proj_qkv = nn.Linear(cfg.hidden_size, self.conv_dim, bias=False) | |
| self.in_proj_z = nn.Linear(cfg.hidden_size, vd, bias=False) | |
| self.in_proj_b = nn.Linear(cfg.hidden_size, cfg.lin_v_heads, bias=False) | |
| self.in_proj_a = nn.Linear(cfg.hidden_size, cfg.lin_v_heads, bias=False) | |
| self.conv1d = nn.Conv1d(self.conv_dim, self.conv_dim, cfg.conv_kernel, groups=self.conv_dim, bias=False) | |
| self.dt_bias = nn.Parameter(torch.ones(cfg.lin_v_heads)) | |
| self.A_log = nn.Parameter(torch.zeros(cfg.lin_v_heads)) | |
| self.norm = GatedRMSNorm(cfg.lin_v_dim, cfg.eps) | |
| self.out_proj = nn.Linear(vd, cfg.hidden_size, bias=False) | |
| C = chunk_size | |
| idx = torch.arange(C) | |
| # incl[i, j, k] = 1 if j < k <= i : sum_k g_k = cum_i - cum_j (lower triangle incl. diagonal is 0 on i == j) | |
| incl = ((idx[None, None, :] > idx[None, :, None]) & (idx[None, None, :] <= idx[:, None, None])).float() | |
| self.register_buffer("pair_sum_t", incl.reshape(C * C, C), persistent=False) # [C*C, C] | |
| self.register_buffer("prefix_sum_t", (idx[None, :] <= idx[:, None]).float(), persistent=False) # [C, C] | |
| self.register_buffer("suffix_sum_t", (idx[None, :] > idx[:, None]).float(), persistent=False) | |
| self.register_buffer("lower_incl", (idx[None, :] <= idx[:, None]).float(), persistent=False) | |
| self.register_buffer("strict_lower", (idx[None, :] < idx[:, None]).float(), persistent=False) | |
| self.register_buffer("eye", torch.eye(C), persistent=False) | |
| assert C & (C - 1) == 0, "delta-rule chunk size must be a power of two" | |
| for n in (C // (2 * s) for s in (2**i for i in range(int(math.log2(C))))): | |
| self.register_buffer(f"block_eye_{n}", torch.eye(n), persistent=False) | |
| def unit_lower_inverse(self, t: torch.Tensor) -> torch.Tensor: | |
| """Inverse of unit-lower-triangular [..., C, C] by recursive 2x2 blocking: | |
| [[A, 0], [X, B]]^-1 = [[A^-1, 0], [-B^-1 X A^-1, B^-1]]. | |
| Stable like forward substitution (it only forms the true inverse's sub-blocks), | |
| but log2(C) batched levels instead of C sequential row updates.""" | |
| C = self.chunk | |
| lead = t.shape[:-2] | |
| t = t.reshape(-1, C, C) # Core ML tensors are rank <= 5 | |
| b = t.shape[0] | |
| inv = torch.ones(b, C, 1, 1, dtype=t.dtype, device=t.device) # 1x1 diagonal blocks of a unit triangle | |
| s = 1 | |
| while s < C: | |
| n2 = C // (2 * s) | |
| # diagonal 2s x 2s blocks of t, then their lower-left s x s part | |
| blocks = (t.reshape(b, n2, 2 * s, n2, 2 * s) * getattr(self, f"block_eye_{n2}")[:, None, :, None]).sum(-2) | |
| x = blocks[..., s:, :s] # [b, n2, s, s] | |
| pair = inv.reshape(b, n2, 2, s, s) | |
| a_inv, b_inv = pair[..., 0, :, :], pair[..., 1, :, :] | |
| lower = -torch.matmul(b_inv, torch.matmul(x, a_inv)) | |
| top = torch.cat([a_inv, torch.zeros_like(a_inv)], dim=-1) | |
| bottom = torch.cat([lower, b_inv], dim=-1) | |
| inv = torch.cat([top, bottom], dim=-2) # [b, n2, 2s, 2s] | |
| s *= 2 | |
| return inv.reshape(*lead, C, C) | |
| def forward(self, x): | |
| cfg = self.cfg | |
| L = x.shape[1] | |
| C, N = self.chunk, self.num_chunks | |
| H, Dk, Dv = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim | |
| rep = cfg.lin_v_heads // cfg.lin_k_heads | |
| qkv = self.in_proj_qkv(x).transpose(1, 2) # [1, conv_dim, L] | |
| qkv = F.silu(self.conv1d(F.pad(qkv, (cfg.conv_kernel - 1, 0)))).transpose(1, 2) | |
| q = qkv[..., : self.key_dim].reshape(1, L, cfg.lin_k_heads, Dk) | |
| k = qkv[..., self.key_dim : 2 * self.key_dim].reshape(1, L, cfg.lin_k_heads, Dk) | |
| v = qkv[..., 2 * self.key_dim :].reshape(1, L, H, Dv) | |
| z = self.in_proj_z(x).reshape(1, L, H, Dv) | |
| beta = torch.sigmoid(self.in_proj_b(x)) # [1, L, H] | |
| g = -torch.exp(self.A_log) * F.softplus(self.in_proj_a(x) + self.dt_bias) # [1, L, H], <= 0 | |
| # l2norm (FLA convention) then repeat k heads up to v heads | |
| q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6) | |
| k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6) | |
| q = q[:, :, :, None].expand(1, L, cfg.lin_k_heads, rep, Dk).reshape(1, L, H, Dk) | |
| k = k[:, :, :, None].expand(1, L, cfg.lin_k_heads, rep, Dk).reshape(1, L, H, Dk) | |
| q = q * (Dk**-0.5) | |
| # -> [H, N, C, D] | |
| q = q[0].permute(1, 0, 2).reshape(H, N, C, Dk) | |
| k = k[0].permute(1, 0, 2).reshape(H, N, C, Dk) | |
| v = v[0].permute(1, 0, 2).reshape(H, N, C, Dv) | |
| beta = beta[0].T.reshape(H, N, C, 1) | |
| g = g[0].T.reshape(H, N, C) | |
| # constant-first matmuls: `x @ const` would lower to Core ML `linear` and be picked up by weight compression | |
| g_col = g.unsqueeze(-1) # [H, N, C, 1] | |
| cum = torch.matmul(self.prefix_sum_t, g_col).squeeze(-1) # cum_i = sum_{k<=i} g_k | |
| to_end = torch.matmul(self.suffix_sum_t, g_col).squeeze(-1) # sum_{k>i} g_k = cum_last - cum_i | |
| pair = torch.matmul(self.pair_sum_t, g_col).reshape(H, N, C, C) # cum_i - cum_j (0 above diagonal) | |
| pair_decay = torch.exp(pair) * self.lower_incl | |
| k_beta = k * beta | |
| v_beta = v * beta | |
| kkt = torch.matmul(k_beta, k.transpose(-1, -2)) * pair_decay | |
| attn_intra = torch.matmul(q, k.transpose(-1, -2)) * pair_decay | |
| inv = self.unit_lower_inverse(self.eye + kkt * self.strict_lower) | |
| new_v = torch.matmul(inv, v_beta) # [H, N, C, Dv] | |
| k_cumdecay = torch.matmul(inv, k_beta * torch.exp(cum)[..., None]) # [H, N, C, Dk] | |
| q_dec = q * torch.exp(cum)[..., None] | |
| k_dec = k * torch.exp(to_end)[..., None] | |
| chunk_decay = torch.exp(cum[..., -1:])[..., None] # [H, N, 1, 1] | |
| state = torch.zeros(H, Dk, Dv, dtype=x.dtype, device=x.device) | |
| outs = [] | |
| for i in range(N): | |
| v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state) | |
| outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new)) | |
| if i + 1 < N: | |
| state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new) | |
| core = torch.stack(outs, dim=1).reshape(H, L, Dv).permute(1, 0, 2) # [L, H, Dv] | |
| core = self.norm(core, z[0]) | |
| return self.out_proj(core.reshape(1, L, H * Dv)) | |
| class DecoderLayer(nn.Module): | |
| def __init__(self, cfg: TextConfig, layer_idx: int, seq_len: int, chunk_size: int): | |
| super().__init__() | |
| self.is_linear = cfg.layer_types[layer_idx] == "linear_attention" | |
| if self.is_linear: | |
| self.linear_attn = GatedDeltaNet(cfg, seq_len, chunk_size) | |
| else: | |
| self.self_attn = FullAttention(cfg) | |
| self.mlp = MLP(cfg) | |
| self.input_layernorm = RMSNorm(cfg.hidden_size, cfg.eps) | |
| self.post_attention_layernorm = RMSNorm(cfg.hidden_size, cfg.eps) | |
| def forward(self, x, cos, sin, mask): | |
| h = self.input_layernorm(x) | |
| h = self.linear_attn(h) if self.is_linear else self.self_attn(h, cos, sin, mask) | |
| x = x + h | |
| return x + self.mlp(self.post_attention_layernorm(x)) | |
| class DecoderChunk(nn.Module): | |
| """Layers [start, end). The last chunk (with_head) returns letter logits [1, n_letters].""" | |
| def __init__( | |
| self, | |
| cfg: TextConfig, | |
| start: int, | |
| end: int, | |
| seq_len: int, | |
| chunk_size: int = 64, | |
| with_head: bool = False, | |
| n_letters: int = 26, | |
| ): | |
| super().__init__() | |
| self.start, self.end, self.with_head = start, end, with_head | |
| self.layers = nn.ModuleList([DecoderLayer(cfg, i, seq_len, chunk_size) for i in range(start, end)]) | |
| mask = torch.full((seq_len, seq_len), -1e4).triu(1) | |
| self.register_buffer("mask", mask[None, None], persistent=False) | |
| if with_head: | |
| self.norm = RMSNorm(cfg.hidden_size, cfg.eps) | |
| self.letter_head = nn.Linear(cfg.hidden_size, n_letters, bias=False) | |
| def forward(self, hidden, cos, sin, last_onehot=None): | |
| for layer in self.layers: | |
| hidden = layer(hidden, cos, sin, self.mask) | |
| if not self.with_head: | |
| return hidden | |
| last = torch.matmul(last_onehot, hidden[0]) # [1, hidden] | |
| return self.letter_head(self.norm(last)) | |
| def load_merged(self, state: dict[str, torch.Tensor], letter_rows: torch.Tensor | None = None): | |
| """`state` uses Qwen3_5TextModel names relative to the language model (`layers.N...`, `norm.weight`).""" | |
| own = {} | |
| for local, global_idx in enumerate(range(self.start, self.end)): | |
| prefix = f"layers.{global_idx}." | |
| for key, value in state.items(): | |
| if key.startswith(prefix): | |
| own[f"layers.{local}.{key[len(prefix):]}"] = value | |
| if self.with_head: | |
| own["norm.weight"] = state["norm.weight"] | |
| own["letter_head.weight"] = letter_rows | |
| missing, unexpected = self.load_state_dict({k: v.float() for k, v in own.items()}, strict=False) | |
| missing = [m for m in missing if not m.endswith(("mask",))] | |
| if missing or unexpected: | |
| raise RuntimeError(f"weight mismatch: missing={missing[:8]} unexpected={unexpected[:8]}") | |