Commit e1af99d
feat: Rust ONNX inference for LiquidAI LFM2.5-VL (vision-language)
`lfm` is a Rust crate that runs the LiquidAI LFM2.5-VL ONNX vision-
language model end-to-end: image preprocessing → vision encoding →
text embedding → decoder prefill+decode loop → detokenize, with
optional schema-constrained generation via llguidance.
## Public surface
- `Engine::from_dir(model_dir, opts)` — strict constructor that
byte-validates the supplied tokenizer.json, chat_template.jinja,
preprocessor_config.json, and config.json's
max_position_embeddings against the bundled assets before
loading any ONNX session. Catches model-revision drift early.
- `Engine::from_onnx_dir(onnx_dir, opts)` — load just the ONNX
files; tokenizer + JSON configs come from the bundled
`models/` assets via `include_bytes!`.
- `Engine::from_paths(paths, opts)` — escape hatch for unusual
layouts; opts out of the strict drift checks.
- `Engine::generate(messages, images, req)` — free-form chat.
- `Engine::run(task, images, req)` — schema-constrained
generation driven by a `vlm_tasks::Task` (separate crate).
- `Preprocessor`, `ImageBudget`, `RequestOptions`, `Options`,
`ChatMessage`, `ImageInput`, `Error` — public configuration
and request types.
## Algorithm fidelity
- **Tile grid + smart_resize**: ports upstream
`Lfm2VlImageProcessorFast.crop_image_to_patches` /
`_is_image_too_large` / aspect-ratio search bit-for-bit. Phase
0 fixture `tests/fixtures/multi_image_ordering_proof.json`
captures upstream `image_features` and we match.
- **Per-tile marker order**: matches upstream's variable-naming
inversion (`num_rows = grid_width`, `num_cols = grid_height`).
Verified against `tests/fixtures/image_expansion_cases.json`.
- **Patch layout**: HWC interleaved per `(dy, dx, ch)` — what
upstream's `convert_image_to_patches` produces after the
permute+reshape, despite preprocessor_config declaring CHW.
- **Resize**: uses `fast_image_resize`'s
`Convolution(FilterType::Bilinear)` to match torchvision
`F.resize(..., interpolation=BILINEAR, antialias=True)` =
PIL's `Image.resize(..., Image.BILINEAR)`.
- **EXIF orientation**: applied during decode so header-only
dimensions in the admission preflight match the eventual
patchified grid.
- **Per-image vision encoding**: never batches across images
(Phase 0 G6 contract — batching corrupts multi-tile outputs).
Per-image pixel buffers are decoded + freed inside the
vision-encode loop, so peak memory is O(1 image) not O(N).
## Numeric safety
- Sampler caps logits beyond tokenizer vocab size to -Inf so
decoder-only padding IDs (64400-65535) can't win sampling.
- Post-penalty range-restricted guard on `[0, vocab_size)`:
any-NaN → SamplerNonFinite (catches model-emitted NaN);
all-(-Inf) → SamplerNonFinite (catches penalty overflow).
- `RequestOptions::validate()` rejects NaN/Inf/subnormal-positive
temperature, NaN/Inf/out-of-range min_p, NaN/Inf/<1.0/>100.0
repetition_penalty, max_new_tokens > 32_768.
- `ImageBudget::validate()` enforces tile counts, image-token
bounds, and `max_tiles ≤ MAX_TOKENIZER_TILE_DIM` (= 10).
## Admission control
Cheap-first, expensive-last admission gates in `generate()`:
- Request-shape cap (max messages, total content parts)
- Body-size cap (text bytes ≤ 16× MODEL_CONTEXT_TOKENS)
- Special-token denylist (tokenizer added vocab + structural
strings + named LFM control tokens, even across split parts)
- Image-count match + lower-bound floor (rejects impossible
batches before any image_dimensions header read)
- Decoded-buffer alloc cap (worst-case W*H*4 bytes vs
decode_limits().max_alloc) at header time
- Per-grid token sum (including IMAGE_START/END wrappers and
row/col markers) before render+tokenize
- Authoritative context-length check after tokenize
## Features + CI
- Cargo features: `inference`, `bundled` (= inference + decoders),
`decoders`, `serde`, `cuda`, `tensorrt`, `directml`, `rocm`,
`coreml`, `integration`. Default = bundled + inference + decoders.
- Examples: `smoke`, `scene_analysis` (require bundled+inference+
decoders), `preprocess_only` (cfg-split for no-default builds).
- CI runs clippy with `-D warnings` across no-default,
decoders-only, and all-features configurations.
- 136 lib tests cover sampler math, RNG state, tile-grid edge
cases, smart_resize parity, chat-template rendering, image-
block layout, admission gates, and drift-check failure modes.
## Bundled assets (`models/` via `include_bytes!`)
- tokenizer.json (~4.5 MB), tokenizer_config.json,
preprocessor_config.json, processor_config.json, config.json,
generation_config.json, chat_template.jinja.
- Total payload ~4.5 MB (under crates.io's 10 MB include limit).
- ONNX models (vision_encoder ~86 MB, decoder ~350 MB) NOT
bundled — users supply via `from_dir` / `from_onnx_dir`.
## Trust model
Documented in `docs/codex-review-rejections.md`: both supplied
model files and caller inputs are trusted (in-process library, no
attacker). In-scope review concerns: algorithmic correctness vs
upstream Python, model I/O contracts, bugs reachable on cooperative
callers, concurrency hazards. Out of scope: defense-in-depth
hardening against tampered model assets, DoS via large/malformed
caller inputs, sampler-config exploits.
## Provenance
Distilled from ~95 incremental commits on the `0.1.0` branch
covering the design spec, plan, implementation phases, and 42
rounds of Codex adversarial review. Commit `53d8394` was the
final fix (issue #2 C-001 single-NaN logit poisoning); commit
`b9bgyuvsm` was the final review pass returning approve / no
findings. See git history before squash for round-by-round
context if needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent ee6eaa7 commit e1af99d
53 files changed
Lines changed: 342514 additions & 359 deletions
File tree
- .github/workflows
- benches
- docs
- superpowers
- plans
- specs
- examples
- models
- scripts
- src
- preproc
- runtime
- tests
- fixtures
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
4 | 5 | | |
5 | | - | |
| 6 | + | |
6 | 7 | | |
| 8 | + | |
7 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
21 | 20 | | |
22 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
23 | 41 | | |
24 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
25 | 46 | | |
26 | | - | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
27 | 96 | | |
28 | 97 | | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
37 | 106 | | |
38 | 107 | | |
39 | | - | |
| 108 | + | |
40 | 109 | | |
41 | 110 | | |
42 | 111 | | |
43 | | - | |
| 112 | + | |
44 | 113 | | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
| 114 | + | |
0 commit comments