d3-mini-CoreAI / convert.py
maxffarrell's picture
Publish experimental Decision 3.0 Core AI conversion with reproducible validation
086a142 verified
Raw History Blame Contribute Delete
6.34 kB
"""Export pinned Decision 3.0 text inference; vision export is separate."""
import argparse
import json
from pathlib import Path
import torch
import torch.nn.functional as F
from coreai_torch import TorchConverter, get_decomp_table
from safetensors.torch import load_file
from transformers import Qwen3_5Model
from transformers.models.qwen3_5.modeling_qwen3_5 import apply_rotary_pos_emb_vision
from transformers.vision_utils import (
get_vision_interpolation_indices_and_weights,
get_vision_position_ids,
)
class DecisionText(torch.nn.Module):
def __init__(self, backbone, readout):
super().__init__()
self.language_model = backbone.language_model
self.register_buffer("readout", readout.float())
def forward(
self, input_ids, attention_mask, position_ids, visual_embeddings, visual_mask
):
embeddings = self.language_model.embed_tokens(input_ids)
embeddings = torch.where(
visual_mask, visual_embeddings.to(embeddings.dtype), embeddings
)
hidden = self.language_model(
inputs_embeds=embeddings,
attention_mask={
"full_attention": attention_mask[:, None, None, :].bool(),
"linear_attention": attention_mask,
},
position_ids=position_ids,
use_cache=False,
).last_hidden_state
return hidden[:, -1].float() @ self.readout.T
class DecisionVision(torch.nn.Module):
"""The unchanged vision weights at a fixed one-image 224px grid."""
def __init__(self, visual, side=224):
super().__init__()
self.visual = visual
grid = torch.tensor(
[[1, side // visual.config.patch_size, side // visual.config.patch_size]]
)
indices, weights = get_vision_interpolation_indices_and_weights(
grid,
num_grid_per_side=visual.num_grid_per_side,
mode=visual.interpolation_mode,
align_corners=visual.interpolation_align_corners,
spatial_merge_size=visual.config.spatial_merge_size,
kwargs={},
)
self.register_buffer("indices", indices)
self.register_buffer("weights", weights)
self.register_buffer(
"positions",
get_vision_position_ids(grid, visual.spatial_merge_size, kwargs={}),
)
def forward(self, pixel_values):
v = self.visual
hidden = F.linear(
pixel_values.to(v.patch_embed.proj.weight.dtype),
v.patch_embed.proj.weight.flatten(1),
v.patch_embed.proj.bias,
)
hidden = hidden + (v.pos_embed(self.indices) * self.weights[:, :, None]).sum(
1
).to(hidden.dtype)
cos, sin = v.rotary_pos_emb(hidden, self.positions)
for block in v.blocks:
x = block.norm1(hidden)
attn = block.attn
q, k, value = (
attn.qkv(x)
.reshape(x.shape[0], 3, attn.num_heads, -1)
.permute(1, 0, 2, 3)
.unbind(0)
)
q, k = apply_rotary_pos_emb_vision(q, k, cos, sin)
y = F.scaled_dot_product_attention(
q.transpose(0, 1).unsqueeze(0),
k.transpose(0, 1).unsqueeze(0),
value.transpose(0, 1).unsqueeze(0),
scale=attn.scaling,
)
hidden = hidden + attn.proj(
y.squeeze(0).transpose(0, 1).reshape(x.shape[0], -1)
)
hidden = hidden + block.mlp(block.norm2(hidden))
return v.merger(hidden).float()
def save(model, sample, path):
with torch.no_grad():
exported = torch.export.export(model, sample, strict=False)
print("Decomposing", flush=True)
exported = exported.run_decompositions(get_decomp_table())
print("Converting", flush=True)
program = TorchConverter().add_exported_program(exported).to_coreai()
program.save_asset(path)
return exported.graph_signature.user_outputs[0]
def main():
p = argparse.ArgumentParser()
p.add_argument("--source", default="upstream/d3-edge")
p.add_argument("--output", default="bundles/d3-edge-CoreAI")
p.add_argument("--length", type=int, default=512)
p.add_argument("--precision", choices=["bfloat16", "float32"], default="bfloat16")
a = p.parse_args()
torch.set_num_threads(4)
source = Path(a.source)
output = Path(a.output)
output.mkdir(parents=True, exist_ok=True)
print("Loading backbone", flush=True)
backbone = Qwen3_5Model.from_pretrained(
source, dtype=getattr(torch, a.precision), attn_implementation="sdpa"
).eval()
model = DecisionText(
backbone, load_file(str(source / "readout.safetensors"))["weight"]
).eval()
hidden = backbone.config.text_config.hidden_size
sample = (
torch.ones(1, a.length, dtype=torch.int32),
torch.ones(1, a.length, dtype=torch.int32),
torch.arange(a.length, dtype=torch.int32).view(1, 1, -1).expand(4, 1, -1),
torch.zeros(1, a.length, hidden),
torch.zeros(1, a.length, 1, dtype=torch.bool),
)
print("Exporting", flush=True)
out = save(model, sample, output / "decision.aimodel")
vision = DecisionVision(backbone.visual).eval()
patches = (224 // backbone.config.vision_config.patch_size) ** 2
width = (
3
* backbone.config.vision_config.temporal_patch_size
* backbone.config.vision_config.patch_size**2
)
vision_out = save(vision, (torch.zeros(patches, width),), output / "vision.aimodel")
(output / "manifest.json").write_text(
json.dumps(
{
"source": source.name,
"sequence_length": a.length,
"hidden_size": hidden,
"inputs": [
"input_ids",
"attention_mask",
"position_ids",
"visual_embeddings",
"visual_mask",
],
"output": out,
"vision_output": vision_out,
"image_side": 224,
"precision": a.precision,
},
indent=2,
)
+ "\n"
)
print("Saved", flush=True)
if __name__ == "__main__":
main()