Skip to content

epic: add Florence-2 (florence2) VLM support #850

Open

Description

@inureyes

Summary

Florence-2 (microsoft/Florence-2-large / -base) is a unified multi-task vision seq2seq model. IMPORTANT: it is architecturally new for mlxcel. A DaViT vision backbone (model_type: davit) produces image tokens that are concatenated with the task-prompt tokens and fed to a BART-style encoder-decoder transformer (a Florence2Encoder plus a Florence2Decoder with cross-attention from decoder to encoder outputs), NOT a decoder-only stack. One text-generation interface covers captioning, OCR, detection, dense region captioning, grounding, and segmentation; task-prompt tokens select the mode, and spatial tasks emit location tokens that post-process into boxes / regions. This is the highest-risk item in the batch and should be scoped as its own epic.

Concretizes the Florence-2 entry recorded as out-of-scope-pending-seq2seq groundwork in #524.

This is the tracking epic for Florence-2 support. This repository has no type:epic label, so the issue keeps type:enhancement while serving as the epic. GitHub native sub-issue linking is not used here; the sub-issues below are tracked by the checklist in this issue.

Sub-issues (execution order)

Phase 1 (parallel foundations)

Phase 2 (fusion)

Phase 3 (modes + capstone)

Upstream reference

Public checkpoint

Confirmed available on HuggingFace:

  • microsoft/Florence-2-large, microsoft/Florence-2-base (originals)
  • mlx-community conversions confirmed: mlx-community/Florence-2-large-ft-4bit / -8bit / -6bit / -bf16 and mlx-community/Florence-2-base-ft-4bit / -8bit / -6bit / -3bit / -bf16 (primary validation target: a -bf16 or -4bit ft variant).

Architecture notes

  • model_type: florence2 with a davit vision sub-config.
  • Vision: DaViT encoder with windowed attention (window_partition / window_reverse), ChannelAttention, ConvEmbed, DepthWiseConv2d, learned absolute positions.
  • Language: BART-style encoder-decoder (6 encoder + 6 decoder layers, learned absolute positions). Image tokens are concatenated with prompt tokens, the encoder runs over the joint sequence, and the decoder cross-attends to encoder outputs while decoding autoregressively.
  • Task-prompt tokens drive the mode (caption / OCR / detect / ground / segment); spatial tasks emit location tokens post-processed into boxes or regions.

Implementation plan

  • PROMINENT CONSTRAINT: mlxcel's generation engine is decoder-only today. Florence-2 requires a NEW encoder-decoder inference path: an encoder pass over the concatenated vision-plus-prompt sequence, then cross-attention decoding against the cached encoder outputs. The only in-tree encoder-decoder precedent is Whisper's dedicated ASR pipeline (src/models/whisper/); use it as the structural template for a seq2seq decode loop and encoder-output caching, but note Florence-2 needs vision-plus-text encoder input and location-token output rather than audio-to-text.
  • DaViT backbone: new, but reuse existing window-attention helpers where possible (the windowed-attention patterns in the qwen2_5_vl / SAM-style encoders under src/vision/encoders/); ConvEmbed / DepthWiseConv2d map to the existing conv layer helpers used by the FastViT / gemma3n conv paths.
  • Task-prompt handling + location-token post-processing (coordinate tokens to boxes / regions) is a dedicated processing stage; the coordinate / OCR post-processing in the OCR VLM modules is a partial reference.
  • RECOMMENDED SCOPING: file this as an epic. Phase 1: seq2seq engine groundwork (encoder-decoder decode loop, cross-attention, encoder-output cache) validated on a minimal path. Phase 2: DaViT backbone. Phase 3: task-prompt modes + location-token post-processing + full multi-task validation. Land Phase 1 as reusable engine infrastructure before the model-specific work.

Touchpoints & acceptance criteria

  • NEW encoder-decoder inference path (encoder pass, cross-attention decode, encoder-output cache) landed as reusable engine infrastructure (Whisper as template).
  • Text config (BART encoder-decoder) + vision config (davit) parse.
  • DaViT vision encoder (windowed attention, window_partition / window_reverse, ChannelAttention, ConvEmbed, DepthWiseConv2d).
  • Image-token concatenation with prompt tokens (Florence-2 concatenates vision + prompt for the encoder rather than scattering into placeholders).
  • from_weights + sanitize: encoder-decoder weight naming, DaViT conv weights channels-last remap, mixed precision.
  • Task-prompt mode handling + location-token to box / region post-processing.
  • Arch-string arm "florence2" in src/models/detection.rs.
  • Registration in src/model_metadata.rs (for_each_model_registration!).
  • generate_vlm (or a dedicated seq2seq VLM path) summary + TP / distributed arch-string if applicable.
  • _tests.rs unit tests (DaViT shapes, cross-attention, task-prompt parse).
  • docs/supported-models.md updated.
  • Real-checkpoint validation: ./target/release/mlxcel generate -m models/Florence-2-base-ft-bf16 --image <img> -p "<CAPTION or OCR or DETECTION task prompt>" produces correct output for at least caption + OCR + detection modes. Follow docs/adding-models.md.

Effort

HIGH, highest-risk item of the batch. The blocker is the seq2seq encoder-decoder engine path, which mlxcel does not have outside Whisper. Recommend an epic with the seq2seq groundwork landed first.

Activity

  1. added
    type:enhancementNew features, capabilities, or significant additions
    status:backlogIn the backlog, not yet ready
    area:modelsModel architectures, weights, loading, metadata
    area:inferenceGeneration, sampling, decoding (incl. speculative, DRY)
    on Jul 21, 2026
  2. changed the title [-]feat(models): add Florence-2 (florence2) VLM support[/-] [+]epic: add Florence-2 (florence2) VLM support[/+] on Jul 22, 2026
  3. inureyes commented on Aug 7, 2026

    @inureyes
    MemberAuthor

    Epic implementation complete

    All five sub-issues are merged and closed. Florence-2 is reachable from the CLI and validated on a real checkpoint.

    Real-checkpoint validation

    Run by the orchestrator against models/Florence-2-base-ft-bf16 through the release binary, on real images rather than the synthetic fixture used inside the sub-issue PRs:

    Task Image Output
    <CAPTION> COCO 000000039769 Two cats are sleeping on a pink blanket.
    <DETAILED_CAPTION> COCO 000000039769 In this image there are two cats sleeping on the pink color cloth. Beside them there are remotes.
    <OD> COCO 000000039769 cat: [17.0, 54.0, 318.4, 471.6], cat: [346.6, 25.7, 639.0, 372.7], couch: [0.3, 0.2, 639.0, 479.3], remote: [40.6, 73.7, 175.7, 116.9], remote: [333.1, 76.6, 370.2, 186.5]
    <CAPTION> car.jpg A green car parked in front of a yellow building.
    <OCR> rendered text image HELLOWORLD
    <OCR_WITH_REGION> rendered text image HELLO: [14.5, 37.5, 565.5, 37.5, 565.5, 177.5, 14.5, 177.5], WORLD: [9.5, 248.5, 632.5, 248.5, 632.5, 387.5, 9.5, 387.5]

    mlxcel arch reports Florence-2 (DaViT + BART seq2seq, task prompts, bf16/f16 only).

    This step was added because #856 validated against tests/fixtures/test_image.png, a 679-byte synthetic PNG. Its answers (unanswerable for caption, a single image-spanning box for detection) are byte-identical to the mlx-vlm reference, which proves the wiring is faithful but does not demonstrate the "correct output" the epic asks for. Wiring fidelity and output usefulness are separate claims and both are now evidenced.

    Verification on final main

    Gate Result
    scripts/ci/check_cross_repo_refs.py pass
    scripts/ci/check_crate_versions.py pass
    scripts/ci/check_kernel_dtype_keys.py pass
    make verify-fmt pass
    make verify-clippy (--workspace --all-targets --features metal,accelerate -- -D warnings) pass
    make verify-test (--workspace --profile test-fast) 7439 passed, 1 failed

    The single failure is loading::vlm::gemma_unified::tests::unified_sanitize_quantized_split_dequant_equivalence, filed as #1074. It is not caused by this epic: none of the five merged PRs touches gemma or quantization code, and src/loading/vlm_gemma_unified.rs was last modified by #400. It is also not the Rust 1.97.1 bump from #1066, which was the first hypothesis; compiling the same source with cargo +1.93.1 reproduces it with byte-identical values.

    Corrections to the epic and sub-issue text

    Every sub-issue found at least one error in its own issue body. Recording them so the next reader trusts the code over the prose:

    • Attention heads are 12 in the real config, not the 8 this epic claims.
    • vision_config.model_type is the empty string on real checkpoints, not "davit". Gating on the literal would fail on every published Florence-2 export.
    • There is no hidden_size. The backbone emits dim_embed[-1] of 1024; projection_dim 768 is the fusion-stage projection.
    • window_partition / window_reverse live only in src/vision/encoders/deepseekocr_sam.rs, not in qwen2_5_vl or kimi_vl as feat(models): Florence-2 DaViT vision backbone (sub of #850) #853 suggested.
    • Mixed precision (vision bf16 with quantized text) is not reachable: neither half has a quantized code path, so feat(models): Florence-2 vision-language fusion + full weight loading (sub of #850) #854 rejects quantized checkpoints at load rather than half-loading them.
    • dots_ocr and paddleocr contain no coordinate parsing at all, so they were not usable as the post-processing reference feat(models): Florence-2 processor, task prompts, and location tokens (sub of #850) #855 suggested.
    • The DaViT conv channels-last remap is a no-op on the mlx-community bf16 export, which already ships channels-last. It is implemented for raw PyTorch exports with both branches tested.
    • "mlxcel list reports florence2" resolves to mlxcel arch; list is the local model store listing.

    Two genuine upstream bugs in mlx-vlm were found and deliberately not copied: ModelConfig reads image_feature_source, image_pos_embed, and visual_temporal_embedding from the top level of config.json while real checkpoints store them under vision_config, and its image_feature_source default is the reverse of the checkpoint's, which would relocate the pooled token from the front of the 577-token sequence to the end. Separately, post_process_generation's docstring calls image_size "height x width" while the code proves it is (width, height), and the PATTERN in parse_od_from_text_and_spans has a doubled backslash that makes it unmatchable.

    Known limitations, with follow-ups filed

    Execution note

    The approved plan ran wave 0 as two parallel worktrees. That was abandoned after measurement: relocating target/ to a new path costs a full MLX C++ plus workspace rebuild per cargo profile (10m04s measured), and the MLX C++ build is per-profile, so making one worktree usable by a sub-agent costs roughly 20 minutes against a 600-second stream-idle watchdog. All five units ran sequentially in the pre-warmed primary working tree instead. No worktrees were created, so none needed cleanup.

    One unit still stalled: #1066 bumped the pinned Rust toolchain from 1.93.1 to 1.97.1 partway through the epic, invalidating the warmed cache, and #856's release build went cold and exceeded the watchdog during the silent fat-LTO link. The work was committed and the unit resumed without loss. The lesson for the next run is that a warmed cache is invalidated by the toolchain pin, not only by the commit range.

  4. added and removed
    status:backlogIn the backlog, not yet ready
    on Aug 7, 2026
  5. inureyes commented on Aug 7, 2026

    @inureyes
    MemberAuthor

    Epic stays open pending the two carved-out follow-ups

    All five sub-issues (#852, #853, #854, #855, #856) are merged and closed, and Florence-2 works from the CLI on a real checkpoint. The epic itself stays open because two of its touchpoints are not met and were split into their own issues rather than dropped:

    Label moved from status:done to status:in-progress to reflect that. Closing this epic waits on both.

    Not blocking this epic: #1074 is a pre-existing gemma4_unified defect unrelated to Florence-2, recorded here only because it is the one red test on main at the time of this run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:inferenceGeneration, sampling, decoding (incl. speculative, DRY)area:modelsModel architectures, weights, loading, metadatapriority:lowLow prioritystatus:in-progressCurrently being worked ontype:enhancementNew features, capabilities, or significant additions

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions