Repository navigation
feat(llmjpvl): port LLM-jp-4 VL and Jagle-VL - #1737
Merged
Merged
Conversation
Adds `model_type: "llmjpvl"`, one architecture over two released checkpoints that differ only in the decoder `llm_config.model_type` names: llm-jp-4-vl-9B-beta (Llama, 196608-token vocab, untied head) and Jagle-VL-2.2B (Qwen3, tied embeddings, per-head q/k RMSNorm). A SigLIP2-so400m tower runs behind InternVL-style dynamic tiling, `pixel_shuffle(0.5)` folds each 512px tile to 256 vectors of width 4608, and the `mlp1` connector projects them into the decoder width where they replace the `<|image_pad|>` positions.
The InternVL runtime is reused unchanged (`InternVLConnector`, `InternVLProcessor`, `insert_internvl_image_tokens`, `SigLipVisionModel`). Four things differ and each is silent when got wrong: the decoder config lives under `llm_config` and there is no `text_config`; the SigLIP tower has no CLS token, so InternViT's `[:, 1:, :]` slice must not be copied or a real patch row is dropped; the `mlp1` LayerNorm takes torch's default 1e-5 rather than `vision_config.layer_norm_eps`; and the runtime carries either decoder behind `LlmJpTextModel`.
Image and stop ids are resolved from the checkpoint's own `added_tokens_decoder` and `generation_config.json`, because the two checkpoints agree on none of them (14/15/16 and stops 2, 11 versus 151655/151669/151670 and stops 151675, 151645, 151672).
The framed image block is spliced where upstream's `<image>` sits, immediately after the Harmony `<|start|>user<|message|>` opener; InternVL's "after the first token" fallback would place it inside the system turn here. The split point is located in the rendered text and verified against the caller's token vector before use, so a tokenizer disagreement degrades instead of shifting the block.
The shipped template ends a generation render at `<|start|>assistant` and the checkpoint's processor appends `<|channel|>final<|message|>`. That completion now lives in `ChatTemplateProcessor`, the seam both the CLI and the server share, keyed on the model type because other Harmony templates end the same way and must not be completed. `prepare_chat_request` also now takes the raw-JSON render only when the template actually reads typed media items; this template never does, so minijinja was being handed a content list for `{{ message['content'] }}`.
Also aligns `InternVLProcessor::find_closest_aspect_ratio`'s tie-break with the upstream area threshold both families' `processing_*.py` use. The previous closest-area rule returned 1 tile for a 768x768 image at tile size 512 where the reference returns 2x2 plus a thumbnail.
Refs #1362
8 of 9 tasks
The released bf16 originals ship `patch_embedding.weight` in the HF `[O, I, kH, kW]` layout and an MLX conversion would ship `[O, kH, kW, I]`. `VisionEmbeddings::from_weights` sanitizes the first into the second by a shape test, so the two must reach the encoder as the same weight; until now only the HF branch was exercised, and only indirectly through a real-checkpoint run. The gate builds the tiny tower twice from seed-identical weights, one map pre-transposed, and demands bit-identical features. The fixture also carries the attention-pooling `head.probe` the loader drops, so the test doubles as evidence that the tower never reads it. Refs #1362
5 tasks
5 tasks
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
Adds
model_type: "llmjpvl", the LLM-jp lab's Japanese VLM family. One architecture covers two released checkpoints that differ only in the decoderllm_config.model_typenames:llm-jp/llm-jp-4-vl-9B-beta(Llama, 196608-token vocab, untied head) andllm-jp/Jagle-VL-2.2B-Jagle-FineVision(Qwen3, tied embeddings, per-head q/k RMSNorm). A SigLIP2-so400m tower runs behind InternVL-style dynamic tiling,pixel_shuffle(0.5)folds each 512px tile to 256 vectors of width 4608, and themlp1connector projects them into the decoder width where they replace the<|image_pad|>positions.The InternVL runtime is reused as the issue asked (
InternVLConnector,InternVLProcessor,insert_internvl_image_tokens,SigLipVisionModel). Four things are genuinely different and each is silent when got wrong: the decoder config lives underllm_configand there is notext_config; the SigLIP tower has no CLS token, so InternViT's[:, 1:, :]slice must not be copied or a real patch row is dropped; themlp1LayerNorm takes torch's default1e-5, notvision_config.layer_norm_eps; and the runtime carries either decoder.What changed
src/vision/llmjp_vl.rswith the two-arm text backbonesrc/vision/llmjp_vl_text.rs, and the loadersrc/loading/vlm_llmjp_vl.rs. Image and stop ids are resolved from the checkpoint's ownadded_tokens_decoderandgeneration_config.json, because the two checkpoints agree on none of them (14/15/16 and stops 2, 11 versus 151655/151669/151670 and stops 151675, 151645, 151672).src/multimodal/llmjp_vl_prompt.rs: the framed image block is spliced where upstream's<image>sits, immediately after the Harmony<|start|>user<|message|>opener. InternVL's "after the first token" fallback would put it inside the system turn here. The split point is located in the rendered text and verified against the caller's token vector before use, so a tokenizer disagreement degrades instead of shifting the block.<|start|>assistantand the checkpoint's processor appends<|channel|>final<|message|>. That completion now lives inChatTemplateProcessor, the one seam both front ends already share, keyed on the model type because other Harmony templates end the same way and must not be completed.prepare_chat_requestnow takes the raw-JSON render only when the template actually reads typed media items. This template never does, so minijinja was being handed a content list for{{ message['content'] }}and would have printed the list into the prompt.InternVLProcessor::find_closest_aspect_ratio's aspect-ratio tie-break now uses the upstream area threshold that both families'processing_*.pyread. The previous closest-area rule returned 1 tile for a 768x768 image at tile size 512 where the reference returns 2x2 plus a thumbnail.Validation
Linux aarch64 / GB10 / CUDA sm_121,
--profile test-fast --features cuda, both released bf16 originals.Greedy against the checkpoints' own
modeling_llmjpvl.pyunder torch 2.14.0+cpu bf16 and transformers 4.57.1, on a 512x512 solid red PNG. Jagle-VL: 301 prompt tokens and赤色です。on both sides. llm-jp-4-vl-9B: 297 prompt tokens andこの画像は、単色の赤色で塗りつぶされた正方形の図形です。on both sides, repeated identically across three CLI runs. Tile counts match the reference: 224x224 gives 1 tile and 256 image tokens, 1024x768 gives 13 tiles and 3328 image tokens.Server: Jagle-VL returns 301 prompt tokens and the identical text. The 9B returns 297 prompt tokens and differs at one token (
キャンバスfor図形), stably across three requests; the same server run with--max-batch-size 1returns図形and matches the CLI exactly, so the difference is the batched decode path's reduction order on a near-tie token, not the prompt. That the prompt is identical is pinned rather than argued: one test compares every id of the assembled prompt against the reference processor'sinput_idson both checkpoints, and another asserts byte equality between the server and CLI renders.cargo clippy --lib --tests --features cuda -- -D warningsandcargo fmt --allcleancargo check --lib --tests --features cudaclean--profile test-fast --features cuda:vision::llmjp_vl7/7,multimodal::llmjp_vl_prompt8/8,loading::vlm::llmjp_vl7/7,server::llmjp_chat_template_tests5/5, detection 1/1,models::metadata_tests4/4vision::processors::internvl5/5,server::chat_request104/104,server::chat_template154/154,vision::internvl2/2,multimodal::internvl_prompt4/4cargo test --workspace --profile test-fast --features metal,accelerateis not runnable on this host (Linux/CUDA, no Metal and no Accelerate); unrun, left to the merge gateCloses #1362