Shieldstral-1.0-3B -> Core AI: policy-conditioned safety classifier, one forward = one verdict; 9/9 vs fp32 at int4lin
777ac29 verified | license: apache-2.0 | |
| base_model: mistralai/Shieldstral-1.0-3B | |
| tags: | |
| - coreai | |
| - aimodel | |
| - apple-silicon | |
| - on-device | |
| - safety | |
| - moderation | |
| - classifier | |
| - mistral | |
| pipeline_tag: text-classification | |
| # Shieldstral-1.0-3B β Apple Core AI (`.aimodel`) | |
| **Mistral's 3B safety model converted to Apple's Core AI** (the Core ML successor announced at | |
| WWDC26), for macOS 27 and iOS 27. Twelve languages, Apache-2.0. | |
| The policy is a string in your code, not a fixed taxonomy: the host writes an Instruction ("Flag | |
| self-harm promotion; do not flag help-seeking or support resources") and a Query, hands over the | |
| content, and gets back a probability. | |
| **It ships as a classifier, not a decoder.** Shieldstral answers by putting mass on `yes` or `no` | |
| at the last prompt token, so the whole tail is baked into the graph: | |
| ``` | |
| (input_ids [1,S] int32, attention_mask [1,S] int32) -> probs [1,2] = softmax([no, yes]) | |
| ``` | |
| One `.aimodel` forward is one verdict. No KV cache, no decode loop, no sampling, and no | |
| 131 072-way head β two rows of the tied embedding are the head, which is 805 MB of fp16 a | |
| classifier never reads. | |
| > Requires macOS 27 / iOS 27 (Core AI ships with the OS). Conversion code, gates and knowledge base: | |
| > **[coreai-model-zoo](https://github.com/john-rocky/coreai-model-zoo)**. | |
| ## Bundles | |
| | path | size | verdict latency (M4 Max) | numerics | | |
| |---|---:|---:|---| | |
| | `gpu-classify/β¦_int4lin_s512` | 2.53 GB | **232.5 ms** | **9/9** verdicts vs fp32, worst \|ΞP\| 0.030 | | |
| | `gpu-classify/β¦_int4lin_s256` | 2.53 GB | **123.6 ms** | 9/9, numerics identical to S=512 | | |
| | `ios-h18p/β¦_int4lin_s512` | 2.336 GiB AOT | 624.7 ms *(iPhone 17 Pro)* | **9/9** on device | | |
| | `ios-h18p/β¦_int4lin_s256` | 2.336 GiB AOT | 371.9 ms *(iPhone 17 Pro)* | **9/9** on device | | |
| macOS 27.0 (26A5378n), Xcode 27.0 (27A5218g), `coreai-torch 0.4.1`, median of 10 warm forwards, | |
| engine ready in ~2 s. | |
| The iPhone rows are an iPhone 17 Pro running PipelinedBench's `PB_SHIELD` gate, median of 5 warm | |
| forwards, engine ready in 10.9 s (S=512) / 5.3 s (S=256). Its probabilities match the Mac's **to | |
| four decimals**, so what int4 costs this model is a property of the weights, not of where they | |
| run. | |
| **Two measurements worth knowing before you pick a bundle.** | |
| **Quantization buys size, not speed.** At the same grid, fp16 runs 230.5 ms and int8lin 253.8 ms | |
| against int4lin's 232.5 β one forward over a padded grid is compute-bound, so shrinking weights | |
| moves 6.88 GB to 2.53 GB and leaves the clock alone. That is the inverse of the decode loop, | |
| where int4 is the main speed lever. Only int4lin is published because the larger bundles are not | |
| better at anything. | |
| **The cost of a verdict is the grid, not the text.** Both bundles hold the same weights and | |
| produce the same probabilities; S=256 is 1.9Γ faster because it computes half as much padding. | |
| Pick the grid from the longest document you will actually moderate β the scaffolding alone is | |
| ~60 tokens, so S=256 leaves ~196 for the document and S=512 leaves ~450. | |
| ## Verdicts | |
| Nine cases, four policies, EN + JA β four of them near-misses that share a topic with a flagged | |
| case, because a model that only separates the easy pairs is a keyword filter with extra steps. | |
| | flagged | fp32 | int4 | | not flagged | fp32 | int4 | | |
| |---|---:|---:|---|---|---:|---:| | |
| | violence (EN) | 0.9972 | 0.9988 | | sourdough recipe | 0.0000 | 0.0000 | | |
| | violence (**JA**) | 0.9011 | 0.9315 | | park recommendation (**JA**) | 0.0001 | 0.0001 | | |
| | weapon-making | 0.9919 | 0.9967 | | chemical **safety** question | 0.0001 | 0.0001 | | |
| | doxxing request | 1.0000 | 1.0000 | | refusal to dox | 0.0003 | 0.0004 | | |
| | | | | | help-seeking | 0.0001 | 0.0002 | | |
| Every verdict survives int4. What moves is the probability, and only on the case fp32 did not | |
| already saturate (JA violence). The fp16 bundle's own noise floor is 0.00056, which is what makes | |
| int4's 0.030 readable as real. **Tune any threshold against the bundle you ship, not against | |
| fp32.** | |
| ## Host contract | |
| Everything outside the forward is yours, and all of it is in `reference.json` next to the bundle: | |
| ``` | |
| PREFIX = "<s>[SYSTEM_PROMPT]" + SYSTEM + "[/SYSTEM_PROMPT][INST]" | |
| BODY = "<Instruct>: {instruction}\n\n<Query>: {query}\n\n<Document>: {document}" | |
| SUFFIX = "[/INST]" | |
| ``` | |
| - encode with **`add_special_tokens=False`** β `<s>` is in the template text and this tokenizer's | |
| post-processor does not add one, so letting it add specials gives you two; | |
| - **right-pad** to the grid with `pad_token_id` 11, mask `1 Γ real + 0 Γ pad`. Under the causal | |
| mask the last real token never sees the padding, which is why S=128 and S=512 agree exactly; | |
| - read `probs[1]` = P(violation). | |
| `SYSTEM` is fixed (it ships in `reference.json`); `Instruct`, `Query` and `Document` are yours. | |
| **Not included:** the checkpoint's Pixtral vision tower (`image_size` 1540). Text only. | |
| ## Using it from Swift | |
| ```swift | |
| let guard = try await SafetyClassifier(model: .shieldstral3B) // .shieldstral3BShort for S=256 | |
| let verdict = try await guard.check(message, policy: .selfHarm) | |
| ``` | |
| [coreai-kit](https://github.com/john-rocky/coreai-kit)'s `SafetyClassifier` owns the scaffolding, | |
| the padding and the threshold. `reference.json` in each bundle ships the nine gated cases with | |
| their fp32 probabilities, so any host β Swift, Python, yours β can check its own prompt | |
| construction rather than trusting it. | |
| ## Converting it yourself | |
| The conversion venv here is transformers 4.57.6, which cannot load this checkpoint at all β the | |
| tokenizer declares `TokenizersBackend`, `AutoModelForCausalLM` rejects `Mistral3Config`. The | |
| oracle therefore runs on transformers **git main**, which knows `ministral3` natively, and the | |
| export is built on the claim that `ministral3` is Mistral + YARN (4.57.6's `MistralModel` handed | |
| this config's `rope_parameters` as `rope_scaling`). | |
| That claim is **measured, not assumed** β cos 1.000000 on last-position logits and |ΞP| = 0.00000 | |
| across all nine cases β because a mis-scaled rope still emits fluent logits and plausible | |
| probabilities. See | |
| [`_smoke/test_shieldstral_torch_ladder.py`](https://github.com/john-rocky/coreai-model-zoo/blob/main/_smoke/test_shieldstral_torch_ladder.py), | |
| [`conversion/export_shieldstral.py`](https://github.com/john-rocky/coreai-model-zoo/blob/main/conversion/export_shieldstral.py) | |
| and | |
| [`knowledge/shieldstral-port.md`](https://github.com/john-rocky/coreai-model-zoo/blob/main/knowledge/shieldstral-port.md). | |
| ## License | |
| Apache-2.0, carried from | |
| [`mistralai/Shieldstral-1.0-3B`](https://huggingface.co/mistralai/Shieldstral-1.0-3B) (revision | |
| `003ec7e2b0bab5f0e6307edbaf186fa5822b76f5`). Not affiliated with Apple or Mistral AI. | |