Publish NanoJev Core ML conversion source without trained weights
Browse files- source/STATUS.md +3 -2
- source/convert-coreml.py +6 -0
- source/export_model.py +4 -5
source/STATUS.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# NanoJev status
|
| 2 |
|
| 3 |
- Pinned current checkpoint and audited native source; full checkpoint SHA256 matches upstream and contains 596,250,498 trained parameters across 322 tensors. Historical tracker revision unresolved.
|
| 4 |
-
- Pinned full checkpoint downloaded. Native Qwen3 encoder plus
|
| 5 |
-
- Core ML
|
|
|
|
| 6 |
- Trained-weight redistribution license unresolved. Public HF model artifact is not yet authorized by the source metadata.
|
|
|
|
| 1 |
# NanoJev status
|
| 2 |
|
| 3 |
- Pinned current checkpoint and audited native source; full checkpoint SHA256 matches upstream and contains 596,250,498 trained parameters across 322 tensors. Historical tracker revision unresolved.
|
| 4 |
+
- Pinned full checkpoint downloaded. Native Qwen3 encoder plus decision-head exporter matched the trained model on Choice, Boolean, and Score requests: same argmax for all three, maximum logit error `1.67e-6`.
|
| 5 |
+
- FP16 Core ML L128/K4 encoder and full decision-head packages exported in 22.7 seconds. Encoder package is about 1.1 GiB; head package is about 420 KiB.
|
| 6 |
+
- Core ML runtime parity for Choice, Boolean, and Score remains pending.
|
| 7 |
- Trained-weight redistribution license unresolved. Public HF model artifact is not yet authorized by the source metadata.
|
source/convert-coreml.py
CHANGED
|
@@ -46,6 +46,7 @@ def main() -> None:
|
|
| 46 |
parser.add_argument("--candidates", type=int, default=4)
|
| 47 |
parser.add_argument("--output-dir", type=Path, default=ROOT / "build")
|
| 48 |
parser.add_argument("--parity-only", action="store_true")
|
|
|
|
| 49 |
args = parser.parse_args()
|
| 50 |
if args.candidates < 2:
|
| 51 |
raise ValueError("candidate bucket must be at least 2 for Boolean output")
|
|
@@ -62,6 +63,11 @@ def main() -> None:
|
|
| 62 |
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 63 |
started = time.perf_counter()
|
| 64 |
encoder_trace = torch.jit.trace(encoder, tuple(torch.from_numpy(value) for value in inputs.values()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
encoder_coreml = ct.convert(
|
| 66 |
encoder_trace,
|
| 67 |
convert_to="mlprogram",
|
|
|
|
| 46 |
parser.add_argument("--candidates", type=int, default=4)
|
| 47 |
parser.add_argument("--output-dir", type=Path, default=ROOT / "build")
|
| 48 |
parser.add_argument("--parity-only", action="store_true")
|
| 49 |
+
parser.add_argument("--trace-only", action="store_true")
|
| 50 |
args = parser.parse_args()
|
| 51 |
if args.candidates < 2:
|
| 52 |
raise ValueError("candidate bucket must be at least 2 for Boolean output")
|
|
|
|
| 63 |
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 64 |
started = time.perf_counter()
|
| 65 |
encoder_trace = torch.jit.trace(encoder, tuple(torch.from_numpy(value) for value in inputs.values()))
|
| 66 |
+
if args.trace_only:
|
| 67 |
+
for index, node in enumerate(encoder_trace.inlined_graph.nodes()):
|
| 68 |
+
if node.kind() == "aten::Int":
|
| 69 |
+
print(f"trace node {index}: {node} scope={node.scopeName()}", flush=True)
|
| 70 |
+
return
|
| 71 |
encoder_coreml = ct.convert(
|
| 72 |
encoder_trace,
|
| 73 |
convert_to="mlprogram",
|
source/export_model.py
CHANGED
|
@@ -16,8 +16,7 @@ def rotate_half(value: torch.Tensor) -> torch.Tensor:
|
|
| 16 |
return torch.cat((-second, first), dim=-1)
|
| 17 |
|
| 18 |
|
| 19 |
-
def rms_norm(hidden: torch.Tensor, norm: nn.Module) -> torch.Tensor:
|
| 20 |
-
width = norm.weight.numel()
|
| 21 |
doubled = torch.cat((hidden, -hidden), dim=-1)
|
| 22 |
normalized = F.layer_norm(doubled, (width * 2,), eps=norm.variance_epsilon)
|
| 23 |
return normalized[..., :width] * norm.weight
|
|
@@ -51,7 +50,7 @@ class NanoEncoder(nn.Module):
|
|
| 51 |
|
| 52 |
def _layer(self, layer: nn.Module, hidden: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 53 |
attention = layer.self_attn
|
| 54 |
-
normalized = rms_norm(hidden, layer.input_layernorm)
|
| 55 |
q = attention.q_proj(normalized).view(self.candidates, self.length, self.heads, self.head_dim)
|
| 56 |
k = attention.k_proj(normalized).view(self.candidates, self.length, self.kv_heads, self.head_dim)
|
| 57 |
v = attention.v_proj(normalized).view(self.candidates, self.length, self.kv_heads, self.head_dim)
|
|
@@ -68,7 +67,7 @@ class NanoEncoder(nn.Module):
|
|
| 68 |
self.candidates, self.length, self.heads * self.head_dim
|
| 69 |
)
|
| 70 |
hidden = hidden + attention.o_proj(attended)
|
| 71 |
-
return hidden + layer.mlp(rms_norm(hidden, layer.post_attention_layernorm))
|
| 72 |
|
| 73 |
def forward(
|
| 74 |
self, input_ids: torch.Tensor, attention_mask: torch.Tensor, eos_map: torch.Tensor
|
|
@@ -78,7 +77,7 @@ class NanoEncoder(nn.Module):
|
|
| 78 |
hidden = self.embed_tokens(input_ids.long())
|
| 79 |
for layer in self.layers:
|
| 80 |
hidden = self._layer(layer, hidden, mask)
|
| 81 |
-
hidden = rms_norm(hidden, self.norm).float()
|
| 82 |
return torch.matmul(eos_map, hidden).transpose(0, 1)
|
| 83 |
|
| 84 |
|
|
|
|
| 16 |
return torch.cat((-second, first), dim=-1)
|
| 17 |
|
| 18 |
|
| 19 |
+
def rms_norm(hidden: torch.Tensor, norm: nn.Module, width: int) -> torch.Tensor:
|
|
|
|
| 20 |
doubled = torch.cat((hidden, -hidden), dim=-1)
|
| 21 |
normalized = F.layer_norm(doubled, (width * 2,), eps=norm.variance_epsilon)
|
| 22 |
return normalized[..., :width] * norm.weight
|
|
|
|
| 50 |
|
| 51 |
def _layer(self, layer: nn.Module, hidden: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 52 |
attention = layer.self_attn
|
| 53 |
+
normalized = rms_norm(hidden, layer.input_layernorm, self.hidden)
|
| 54 |
q = attention.q_proj(normalized).view(self.candidates, self.length, self.heads, self.head_dim)
|
| 55 |
k = attention.k_proj(normalized).view(self.candidates, self.length, self.kv_heads, self.head_dim)
|
| 56 |
v = attention.v_proj(normalized).view(self.candidates, self.length, self.kv_heads, self.head_dim)
|
|
|
|
| 67 |
self.candidates, self.length, self.heads * self.head_dim
|
| 68 |
)
|
| 69 |
hidden = hidden + attention.o_proj(attended)
|
| 70 |
+
return hidden + layer.mlp(rms_norm(hidden, layer.post_attention_layernorm, self.hidden))
|
| 71 |
|
| 72 |
def forward(
|
| 73 |
self, input_ids: torch.Tensor, attention_mask: torch.Tensor, eos_map: torch.Tensor
|
|
|
|
| 77 |
hidden = self.embed_tokens(input_ids.long())
|
| 78 |
for layer in self.layers:
|
| 79 |
hidden = self._layer(layer, hidden, mask)
|
| 80 |
+
hidden = rms_norm(hidden, self.norm, self.hidden).float()
|
| 81 |
return torch.matmul(eos_map, hidden).transpose(0, 1)
|
| 82 |
|
| 83 |
|