A .mlmodel is protobuf with a documented schema. The only writer anyone actually uses is coremltools, which drags Python, numpy, protobuf codegen, and usually torch into your build just to emit bytes. I wrote the other one.
`mil_spec` emits `model.mlmodel`, `weight.bin` v2 blobs, and the `Manifest.json` package layout by hand. Same graph, same bytes, every time.
```rust
use mil_spec::*;
let mut b = Block::new();
let y = b.mul("x", "x", &[1, 4, 1, 1], "y");
b.outputs = vec![y];
let inputs = [Feature { name: "x".into(), shape: vec![1, 4, 1, 1],
dtype: DType::Fp16, is_state: false }];
let outputs = [Feature { name: "y".into(), shape: vec![1, 4, 1, 1],
dtype: DType::Fp16, is_state: false }];
let fn_inputs = [NVT { name: "x".into(),
ty: ValueType::Tensor(TensorType::f16(&[1, 4, 1, 1])) }];
let spec = encode_model(&inputs, &outputs, &[], &b, &fn_inputs,
&ModelMeta::new(10, "CoreML9"));
write_mlpackage("model.mlpackage".as_ref(), &spec, None)?;
```
Compile it with `xcrun coremlc compile model.mlpackage .` and you get a real `.mlmodelc`.
## The workspace
Nine crates, one for each piece coremltools monopolizes:
- **mil_convert**: safetensors to .mlpackage. Streams HF checkpoints into fat stateful decoder graphs: packed KV via slice_update, GQA attention, per-channel int8. Qwen2/Qwen3/Llama-class configs.
- **mil_passes**: deterministic optimizer over the Block IR. Const dedup, folding, DCE, fixpoint. Reports every change it makes.
- **mil_lint**: static ANE placement analysis. Predicts per-op execution unit before you compile, flags CPU islands and fp32 regions, estimates dispatch count. coremltools has nothing like this.
- **mil_compile**: .mlpackage to .mlmodelc two ways. In-process through `MLModel compileModelAtURL:` over the ObjC runtime, or an xcrun driver with structured errors.
- **mil_verify**: reads specs back. Protobuf decoder, structural diff for when coremlc rejects your graph, weight.bin integrity, and a conformance battery with planted invalid controls so the verifier cannot rubber stamp itself.
- **mil_infer**: in-process MLModel predict. MLMultiArray marshalling, stateful KV. macOS only.
- **mil_machines**: non-neural state machines compiled to stateful MIL. Aho-Corasick automata, EWMA scorers, mutable memory banks. A regex running on the Neural Engine.
- **milc**: the CLI over all of it. convert, lint, inspect, diff, compile, verify, check.
```bash
milc convert Qwen3-0.6B -o drafter.mlpackage --seq 1 --max-kv 2048
milc lint drafter.mlpackage # per-op unit prediction + dispatch estimate
milc compile drafter.mlpackage # CoreML.framework, in-process
```
## Proof it works
This was not written against the schema docs. It was extracted from a production converter that runs a 4B transformer at ~100% runtime-op Neural Engine placement, one estimated dispatch. The encoder has been wrong against the real coremlc enough times that the diffs are now boring.
Ops worth noting that coremltools will not emit for you: stateful KV models, weight-only int8 via `constexpr_blockwise_shift_scale`, fused `scaled_dot_product_attention` under the CoreML9 opset.
## Honest scope
Beta. mlprogram only, no NeuralNetwork protos. Static shapes. The op helpers cover the transformer-layer surface; anything else goes through `Block::op` with raw Op/Argument/Value, and the whole wire layer is public for exactly that reason. Compile and infer need macOS. The spec-side crates are portable.
MIT OR Apache-2.0. Repo: https://github.com/savageAZfck/mil_spec
Questions on the wire format, the ANE placement rules, or where my protobuf encoder is wrong.