feat(models): Florence-2 end-to-end integration and real-checkpoint validation - #1071
Merged
Merged
Conversation
…nerate path Capstone of the Florence-2 epic (#850): the engine (#852), DaViT tower (#853), fusion (#854), and processor (#855) all landed as libraries with no runtime reachability. This change registers the family end to end so a real checkpoint loads and answers task prompts through `mlxcel generate`. Florence-2 is an encoder-decoder (seq2seq) VLM: the BART decoder cross-attends to cached encoder output over the fused image+prompt sequence, using its own dual-cache (`Florence2SeqCache`) rather than the decoder-only `KVCache` list. It therefore cannot ride the shared autoregressive loop. The integration follows the DiffusionGemma precedent: a `LoadedModel::Florence2VLM` variant whose CLI route exits into a dedicated task pipeline before the standard loop, plus a startup refusal on the server until a seq2seq worker path exists (letting a decoder-only worker pick it up would serve garbage). What changed: - `src/models/florence2/runtime.rs` (new): `Florence2VlmModel` bundling the fused model with its processor, `run_task` (prompt expand -> preprocess -> greedy decode -> coordinate parse, also returning the token count for stats), `parse_task_prompt` for the CLI `-p` syntax (`<OD>`, bare `od`, `<CAPTION_TO_PHRASE_GROUNDING> text`, `<REGION_TO_CATEGORY><loc_*>...`), and an honest-minimal `LanguageModel` impl (teacher-forced BART forward, `supports_batching = false`) for trait completeness. - `src/models/detection.rs`: `"florence2"` arm -> `ModelType::Florence2VLM`; registered in `src/model_metadata.rs` (`kind: Vlm, directory: Vlm`, adapter loading refused with a named message); `ModelType` variant, `ALL_MODEL_TYPES`, `metadata()` ("Other VLM" family, bf16/f16-only note), and the exhaustiveness test list updated in `src/models/mod.rs`. - `src/loading/vlm_florence2.rs` (new) plus the `try_load_vlm_model_from_dir` arm; `LoadedModel::Florence2VLM` variant and `delegate_language_model!` arm. - `src/commands/generate_florence2.rs` (new): the CLI driver. Rejects `--audio`/`--video`, requires exactly one `--image`, parses the task prompt, and routes image decoding through `decode_image_payloads_with_limits` (decompression-bomb defense handed over from #855). Renders parsed boxes/quads/polygons one instance per line in original-image pixels, with a raw-text fallback that distinguishes "model found nothing" from "parser rejected the answer". Early exit wired into `run_generate_once` after the diffusion exits. - `src/server/startup.rs`: `start_server` bails for `ModelType::Florence2VLM` with a message pointing at the CLI, before any worker spawns. - `src/distributed/tensor_parallel/inference.rs`: placeholder `fallback_architecture` arm keeps the dispatch table total; TP never serves this family. - `docs/supported-models.md`: Florence-2 entry documenting the seq2seq pipeline, the fifteen task markers, and the bf16/f16-only constraint (quantized mlx-community conversions are rejected at load; no quantized code path exists for the BART stack or the DaViT tower). Validation: unit tests beside the code (`florence2_runtime_tests.rs` prompt-parse matrix, render tests in `generate_florence2.rs`, a detection test with the real config shape), plus real-checkpoint CLI runs on `models/Florence-2-base-ft-bf16` for caption, OCR, and detection reproducing the byte-identical parity answers from #855 (recorded in the PR). Refs #850.
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Capstone of the Florence-2 epic #850: wires the engine (#852), DaViT tower (#853), fusion (#854), and processor (#855) into model detection, registration, loading, and the CLI generate path, then validates caption, OCR, and detection on the real
models/Florence-2-base-ft-bf16checkpoint. No new model math; this makes the family reachable.Florence-2 is an encoder-decoder (seq2seq) VLM: the BART decoder generates by cross-attending to cached encoder output over the fused image+prompt sequence, with a dual self/cross-attention cache (
Florence2SeqCache) that does not fit the decoder-onlyKVCachecontract every otherLoadedModelfamily runs on. The integration therefore follows the DiffusionGemma phase-1 precedent:LoadedModel::Florence2VLMexists as a first-class variant, the CLI routes it to a dedicated task pipeline before the autoregressive loop, andmlxcel-serverrefuses the checkpoint at startup with a named error until a seq2seq worker path exists.What changed
src/models/florence2/runtime.rs(new):Florence2VlmModel(fused model + processor as one loadable unit),run_task(prompt expand, 768x768 preprocess, greedy seq2seq decode, coordinate parse against the original image size, returns the generated-token count for stats),parse_task_promptfor the CLI-psyntax, and an honest-minimalLanguageModelimpl (teacher-forced BART forward with the shift-right built on-device,make_cachesempty,supports_batching/supports_padded_prefillfalse) for trait completeness only.src/models/florence2/florence2_runtime_tests.rs(new): prompt-parse matrix (marker/bare/case-insensitive forms,<loc_*>region input without separating space, unknown/unclosed/empty errors listing valid markers, input-validation deferral toFlorence2Task::expand).src/models/detection.rs:"florence2"arm returningModelType::Florence2VLM;src/models/detection_tests.rscovers it with the real config shape (emptyvision_config.model_type).src/models/mod.rs:ModelType::Florence2VLMvariant,ALL_MODEL_TYPESentry,metadata()arm ("Florence-2 (DaViT + BART seq2seq, task prompts, bf16/f16 only)", family "Other VLM"), exhaustiveness-test list entry, and re-exports forFlorence2VlmModel/Florence2RunOutput.src/model_metadata.rs:Florence2VLM => { kind: Vlm, directory: Vlm, weight: None, adapter: Some(...) }registration; adapter loading is refused with a named message.src/loading/vlm_florence2.rs(new) plus thetry_load_vlm_model_from_dirarm insrc/loading/mod.rsand module registration insrc/loading/vlm.rs.src/loaded_model.rs:Florence2VLMvariant anddelegate_language_model!arm.src/commands/generate_florence2.rs(new): CLI driver. Requires exactly one--image, rejects--audio/--video, parses the task prompt, decodes the image throughmlxcel::decode_image_payloads_with_limitswith the sharedImageInputLimits(decompression-bomb defense handed over by feat(models): Florence-2 processor, task prompts, and location tokens (sub of #850) #855), runs the task, renders parsed boxes / quad boxes / polygons one instance per line in original-image pixels (a raw-text fallback distinguishes "model found nothing" from "parser rejected the answer";--profilealso prints the raw answer), and prints the standard throughput line. Renderer unit tests sit beside it.src/commands/generate.rs: early-exit branch forLoadedModel::Florence2VLMinrun_generate_once, after the DiffusionGemma / LLaDA-2 exits and before the VLM-embedding path, so the decoder-only path of every other model is untouched.src/server/startup.rs:start_serverbails for Florence-2 before any worker spawns, pointing at the CLI. Without this the decoder-only worker would load the model and serve garbage through the trait-completeness forward.src/distributed/tensor_parallel/inference.rs: placeholderfallback_architecturearm ("florence2") keeps the exhaustive dispatch table total; TP planning rejects the string as unsupported, same as Whisper/Kokoro.docs/supported-models.md: Florence-2 entry documenting the architecture, the fifteen task markers, the CLI form, the server limitation, and the bf16/f16-only constraint.Scoping decisions and corrections to the issue text
mlx-community/Florence-2-base-ft-bf16"or-4bit" as the validation target. The-4bit(and 8/6/3-bit) conversions cannot work: neither the BART stack nor the DaViT tower has a quantized code path, and a packed uint32 weight reaching MLX aborts the process, soFlorence2Model::loadrejects quantized checkpoints with a named error (a deliberate feat(models): Florence-2 vision-language fusion + full weight loading (sub of #850) #854 decision). Validation ran on the bf16 export, anddocs/supported-models.mdstates the bf16/f16-only limitation plainly. Quantized support is follow-up work on the epic.mlxcel listreports florence2", butmlxcel listis the downloaded-model store listing (name/size/modified, no architecture column); the architecture catalog ismlxcel arch(aliassupported). Both outputs are recorded below:archnow lists Florence-2 under Other VLM, andlist --models-dir modelsshows the checkpoint.generate/generate_vlm". The decode path deliberately does not run throughcompute_vlm_embeddings(generate_vlm): that surface produces merged input embeddings for a decoder-only prefill and cannot express an encoder pass plus cross-attention decode. The wiring point that preserves the decoder-only path for every other model is the pre-loop early exit inrun_generate_once, the same integration shape DiffusionGemma and LLaDA-2 use. For the same reason there is noVlmRuntimeRef::Florence2:is_vlm()stays false so no image ever routes into the embedding-merge path._tests.rsunit tests (DaViT shapes, cross-attention, task-prompt parse)". DaViT-shape and cross-attention tests landed with feat(models): Florence-2 DaViT vision backbone (sub of #850) #853/feat(models): Florence-2 BART-style seq2seq encoder-decoder engine + text core (sub of #850) #852 (florence2_davit_tests.rs,florence2_tests.rs, plus the real-checkpoint parity suites undertests/); this PR adds the task-prompt-parse and runtime-surface tests that belong to the integration layer.LanguageModeldecode, and a Florence-2 seq2seq worker loop is follow-up work on the epic (mirrors DiffusionGemma, whose server support arrived in a later phase).Validation
Real-checkpoint CLI runs (
models/Florence-2-base-ft-bf16, Apple Silicon Metal). All four answers agree with the byte-identical mlx-vlm parity references recorded by #855 intests/florence2_processor_parity.rs(REF_ANSWERS/REF_OD_BOX/REF_OCR_QUAD, fixturetests/fixtures/test_image.png).Architecture catalog and store listing:
Guard rails exercised:
Tests (test-fast profile, metal,accelerate):
cargo test --lib --bins florence2(florence2 unit suites incl. the new runtime prompt-parse and CLI renderer tests, plusdetection_tests::florence2_model_type_is_detected): all passcargo test --lib every_variant/all_model_types/family_order/model_metadata: all pass (registry exhaustiveness guards)cargo clippy --profile test-fast --features metal,accelerate --lib --tests -- -D warnings: cleancargo fmt --check: cleanPre-existing failure, not caused by this PR:
loading::vlm::gemma_unified::tests::unified_sanitize_quantized_split_dequant_equivalencefails identically at currentmain(9550388) on this machine (checked out and re-run both ways); it involves only Gemma 4 Unified quantized-weight sanitize, which this change never touches, and may be related to the Rust 1.97.1 toolchain bump from #1066.Closes #856.