Skip to content

Vela 3.0 (vela3-fast) on Core ML: decisions, PII and unsupported-claim spans - #40

Merged
Alex-Wengg merged 2 commits into
mainfrom
feat/vela-3.0
Oct 28, 2026
Merged

Alex-Wengg merged 2 commits into
mainfrom
feat/vela-3.0

Conversation

@Alex-Wengg

@Alex-Wengg Alex-Wengg commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

What

Vela3Manager runs vllm-sr/vela3-fast (Vela 3.0, UltraBERT-380M, Apache-2.0) from the new
FluidInference/vela-3.0-coreml snapshot:

  • Decisions: choice or yes / no questions about a state, on the decision backbone (Vela3Decision.mlpackage, L128–L1024).
  • Spans: the release's span_sets adapter (LoRA merged, Vela3SpanSets.mlpackage, L512 / L1024): 17 PII types, and the words of an answer its context does not support.

One encoder pass per question on the GPU. The Neural Engine is slower for this encoder at every length and drifts further from fp32 (15 / 1,200 decisions flipped), so it is not used.

How

  • Vela3Tokenizer: byte-level BPE reproducing Hugging Face tokenizers offsets.
    • No offset trimming, because the post-processor is TemplateProcessing.
    • Pieces of one multi-byte character share its offsets.
    • lstrip / rstrip special tokens absorb the whitespace next to them.
    • Merges use a (rank, position) min-heap, so long pieces don't run in quadratic time.
  • Decision rendering and readout follow vela3_encoder.DecisionEncoder.
  • Span assembly, word units and the decoder reuse the Vela 2.0 Swift code. The release copied that Python verbatim; I diffed every helper and they are identical.
  • Long span inputs follow the release's assemble / _windows within a 1,024-token pass:
    • the other part (an unsupported-claim context) is cut first;
    • then the target is read in overlapping windows, each window re-fitted;
    • per-word logits are averaged across windows.
  • Thresholds come from the release's vela3_config.json: set-adapter temperature, PII thresholds by input length, and the unsupported-claim threshold.
  • Vela2Heads.init(weights:span:) was added so both models share the readout. It validates tensor shapes, and Vela 2 values are unchanged.
  • Vela3ModelStore is pinned to revision deaf348c with a SHA-256 for every file.

Checks

swift run -c release Vela3Check parity <model dir> <fixtures.json> against the Python runtime of the HF repo (vela3_coreml.py, same encoders, the release's own code). Fixtures come from conversion/fixtures.py in the HF repo. They cover emoji ZWJ sequences and flags, CJK, Arabic, Cyrillic, CRLF, URLs, <|mask|> and a 3,000-letter word; 2- and 9-window PII; and long contexts and answers.

tokens: 23 texts, 0 mismatches
decisions: 120 questions, 0 sequence mismatches, 0 answers differ, max |dp| 0.00000
spans: 16 calls (29 windows, 599 spans), 0 sequence mismatches, 0 span sets differ, max span |dp| 0.00000
median call: decision 9.7 ms, span 18.1 ms (GPU, end to end, M5 Pro)

The check now fails on probability drift above 1e-3, not only on changed answers. The Python runtime itself matches fp32 PyTorch: 0 / 1,200 typed decisions differ, and PII span sets are identical on 25 passages (HF reports/verify.txt).

Built in release mode: Vela3Check, Vela2Check, GuardrailDemo, and FrontDoorDemo (with the Xcode toolchain; the CommandLineTools toolchain lacks the SwiftUI macro plugin, and that predates this PR).

Core ML cold-start abort (fixed here)

When Core ML allocated the output, the first GPU prediction on a cold compile cache aborted inside MPSGraphTensorData (shape.count = 0 != strides.count = 3). This happened in about 1 of 3 cold runs, inside MLE5Engine, with either the sync or the async prediction API (macOS 27.0).

With a preallocated output backing and synchronous prediction it happened 0 of 20 times: 12 cold runs, plus 8 under a launcher that had crashed 3 of 3. Vela2Manager uses the same async, Core ML-allocated output pattern; it may be worth the same change in a follow-up.

Review

A /code-review pass found 15 issues. Fixed in 7792562:

  • long-input handling for unsupported claims and PII;
  • <|mask|> lstrip;
  • quadratic BPE;
  • silent span-temperature default;
  • safetensors and hidden-shape bounds;
  • pii_buckets validation;
  • duplicate option names;
  • Vela2 error type in Vela3 load;
  • parity exit ignoring probability drift;
  • per-call schema tokenization;
  • redundant compile check.

Not changed: the model-store, encode and byte-table duplication, which follows the existing per-model pattern across FluidUse (a shared helper would be a repo-wide refactor), and the hard-coded function lengths (the snapshot is pinned).

🤖 Generated with Claude Code

Alex-Wengg and others added 2 commits October 28, 2026 18:52
…m spans

Vela3Manager runs vllm-sr/vela3-fast (UltraBERT-380M) from FluidInference/vela-3.0-coreml:
decisions over named options (choice / yes-no about a state) on the decision
backbone, and the span_sets adapter's two label sets (17 PII types, words of an
answer its context does not support). One encoder pass per question, GPU.

- Vela3Tokenizer: byte-level BPE with Hugging Face offsets (no trimming: the
  post-processor is TemplateProcessing; pieces of a multi-byte character share
  its offsets).
- Rendering, readout and thresholds follow the release's vela3_encoder.py /
  vela3_schema.py; span assembly, word units and the decoder reuse the Vela 2.0
  Swift code (the release copied that Python verbatim).
- Vela2Heads gains init(weights:) (Vela 3 ships safetensors heads; the decision
  head set has no span temperature).
- Vela3ModelStore pins revision deaf348c with per-file SHA-256.
- Vela3Check parity vs the Python runtime on the same encoders: 0 token
  mismatches on 17 Unicode-heavy texts, 0 / 120 decisions and 0 / 12 span sets
  differ.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…alidation)

- Long span inputs follow the release's assemble/_windows with a 1,024-token
  pass: the other part (an unsupported-claim context) is cut first (to 64
  tokens, then 1), then the target is read in overlapping windows, each
  window re-fitted (a short last window gives the context its room back);
  per-word logits are averaged over windows (-30 where uncovered).
- Tokenizer: added tokens honour lstrip / rstrip (<|mask|> absorbs the
  whitespace before it, as HF); BPE merges via a (rank, position) min-heap
  instead of a quadratic rescan; long pieces are not cached.
- Core ML: hidden states go into an output backing with a synchronous
  prediction. With Core ML allocating the output, the first GPU prediction
  on a cold compile cache aborted in MPSGraphTensorData
  (shape.count = 0 != strides.count = 3) in ~1 of 3 cold runs; with the
  backing 0 of 20 (12 cold, 8 under a launcher that crashed 3 of 3).
- Validation: heads tensor shapes (span heads must carry w_t / w_e /
  log_tau_span), safetensors bounds, hidden-state shape, pii_buckets,
  duplicate option names; heads errors surface as Vela3Error.
- The label schema is tokenized once at load.
- Vela3Check: compares every window's sequence and span probabilities, fails
  on probability drift (> 1e-3), adds a `warm` cold-start command; fixtures
  add <|mask|>, a 3,000-letter word, 2- and 9-window PII and long contexts.

Parity vs the Python runtime: 0 / 23 token, 0 / 120 decision and 0 / 16
span-set mismatches (29 windows, 599 spans), max |dp| 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Alex-Wengg
Alex-Wengg merged commit 5462614 into main Oct 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant