feat(mm): serve image input on the Qwen families - #454
Merged
Merged
Conversation
- frontend: OpenAI image_url, Anthropic image blocks and Responses input_image (http(s), base64, gated file://) become MMItems in the tokenizer workers; image spans carry content pseudo ids so the radix cache keys them by content; mrope positions are precomputed per request - registry: EncoderSpec per family (Qwen3-VL, Qwen3.6, Qwen3.8-Flash-Next register a vision encoder serving image) and ModelSpec.mm_processor; EngineConfig.active_encoders decides what a process builds, served_modalities what it accepts; --text-model-only, --mm-disable, --mm-encoder-weights, --mm-embed-cache-device, --mm-max-pixels - engine and scheduler: per-request encoder-cache row ownership, chunk planning on the batch (jobs, gather rows, scatter rows), items encoded right before the LM forward and gathered into batch.mm_embeds - models: the Qwen VL vision tower on the text tower's TP layers with DeepStack, block weights streamed from pinned host banks by default, merger outputs parked on the host between blocks; SupportsMultimodal (encode, place_encoder_weights) and a shared embed_input_ids - server: image inputs rejected with a client-facing reason when no vision encoder is served; GET /v1/stats reports model.input_modalities
- --image-min-tokens / --image-max-tokens replace --mm-max-pixels; MMProcessor.get_mm_processor_kwargs turns the MultimodalConfig into the processor call's arguments, the default passes mm.processor_kwargs through and Qwen VL converts the budget to pixel areas in size.shortest_edge / longest_edge - --mm-processor-kwargs hands a JSON object to the checkpoint's image processor call, applied after the token budget - MMProcessor is an ABC that owns the model path, the config and the lazily loaded image processor; get_mm_processor returns None when --mm-disable leaves no encoder to serve - the frontend tokenizer, the tokenizer worker and the offline LLM build one processor each and hand it to TokenizeManager, so the tokenize message carries no per-request pixel cap
This was referenced Sep 14, 2026
This was referenced Sep 15, 2026
lukascechovic
pushed a commit
to lukascechovic/FreeToken
that referenced
this pull request
Sep 15, 2026
19 commits and upstream's README is not something a stranger can act on. This adds README.gfx1201.md -- the branch's own documentation -- and prepends a banner to README.md so that whoever lands on the branch page sees it. ⛔ Upstream's README is PREPENDED TO, never replaced. Its text is byte-identical below the banner. What the documentation had to carry, or it would be worse than nothing: - The three settings without which it does not serve, with the reason each one is not discoverable: `--expert-load parallel` (auto picks serial here, ~100x slower load, and the flag exists partly to override a host-RAM guard that a 128 GiB box can never satisfy); PYTORCH_ALLOC_CONF=expandable_segments:False (the engine forces :True, which GPU-FAULTS on gfx1201 -- and torch's own OOM message advises :True); HIP_VISIBLE_DEVICES set explicitly, always. - The defects, named. Image token positions on this branch are wrong, and so is everything after them: the checkpoint declares M-RoPE and this branch feeds 1-D positions. Nothing errors. Text-only is provably exact. ⭐ Upstream merged a correct M-RoPE implementation in FlashML-org#454 on 2026-09-13, so for image quality upstream's vision path is better than this one, and the README says so. Nothing bounds the number of images in one request, so one legal request can exhaust host RAM mid-request; the mitigations that work are listed. TP=2 with images is the least-tested combination here. - What is actually still ours, checked against upstream main at 68a81ff rather than assumed: the RDNA LDS clamp (without it this model serves prompts of at most 15 tokens on RDNA before the worker dies), tensor parallelism for qwen4_exp (upstream's weight loader still raises TP=1-only), the relay handshake, and all-rank agreement on an encode failure. Vision and the multimodal scheduler work have upstream counterparts now and are credited as such. @gdevenyi's PRs FlashML-org#385 and FlashML-org#386 are credited by number: the same shard was arrived at independently on CUDA at the same time. - Host RAM is the gate, not VRAM: the expert banks stay host-resident at ~31.65 GiB per rank, at TP=1 and at TP=2 alike. ⛔ No quality or fidelity claim is made anywhere in it. This work has no fidelity instrument, and the README says that too. llm-server #1018.
lukascechovic
pushed a commit
to lukascechovic/FreeToken
that referenced
this pull request
Sep 15, 2026
19 commits and upstream's README is not something a stranger can act on. This adds README.gfx1201.md -- the branch's own documentation -- and prepends a banner to README.md so that whoever lands on the branch page sees it. ⛔ Upstream's README is PREPENDED TO, never replaced. Its text is byte-identical below the banner. What the documentation had to carry, or it would be worse than nothing: - The three settings without which it does not serve, with the reason each one is not discoverable: `--expert-load parallel` (auto picks serial here, ~100x slower load, and the flag exists partly to override a host-RAM guard that a 128 GiB box can never satisfy); PYTORCH_ALLOC_CONF=expandable_segments:False (the engine forces :True, which GPU-FAULTS on gfx1201 -- and torch's own OOM message advises :True); HIP_VISIBLE_DEVICES set explicitly, always. - The defects, named. Image token positions on this branch are wrong, and so is everything after them: the checkpoint declares M-RoPE and this branch feeds 1-D positions. Nothing errors. Text-only is provably exact. ⭐ Upstream merged a correct M-RoPE implementation in FlashML-org#454 on 2026-09-13, so for image quality upstream's vision path is better than this one, and the README says so. Nothing bounds the number of images in one request, so one legal request can exhaust host RAM mid-request; the mitigations that work are listed. TP=2 with images is the least-tested combination here. - What is actually still ours, checked against upstream main at 68a81ff rather than assumed: the RDNA LDS clamp (without it this model serves prompts of at most 15 tokens on RDNA before the worker dies), tensor parallelism for qwen4_exp (upstream's weight loader still raises TP=1-only), the relay handshake, and all-rank agreement on an encode failure. Vision and the multimodal scheduler work have upstream counterparts now and are credited as such. @gdevenyi's PRs FlashML-org#385 and FlashML-org#386 are credited by number: the same shard was arrived at independently on CUDA at the same time. - Host RAM is the gate, not VRAM: the expert banks stay host-resident at ~31.65 GiB per rank, at TP=1 and at TP=2 alike. ⛔ No quality or fidelity claim is made anywhere in it. This work has no fidelity instrument, and the README says that too. llm-server #1018.
Artemowka22
added a commit
to Artemowka22/FreeToken
that referenced
this pull request
Sep 15, 2026
Two conflicts: - python/freetoken/server/openai_api.py: upstream threads the configurable default output budget (default_max_tokens, FlashML-org#411) into the resolve_sampling call that this branch had extracted into a variable to attach the logprobs fields. Kept the variable; its construction now passes default_max_tokens. - python/freetoken/tokenizer/server.py: import conflict between upstream's get_mm_processor (image input, FlashML-org#454) and this branch's build_logprobs_entry. Kept both. Assisted-by: Claude
nomanoma121
pushed a commit
to nomanoma121/My-FreeToken
that referenced
this pull request
Sep 16, 2026
… stack Upstream FlashML-org#454 serves images on the Qwen families: requests carry MMItems whose placeholder rows hold content pad ids (so the radix cache keys them by content), the tokenizer precomputes 3-axis rope positions, and the engine encodes on the GPU from pinned host banks right before the LM forward. Kai had its own image path (a CPU vision tower in the tokenizer worker, per-request cos/sin tables, image prompts kept out of the prefix cache and scheduled alone). The two cannot coexist -- they define Req, Batch, UserMsg and the rope differently -- so this merge takes upstream's and removes Kai's: vision_cpu.py, mrope.py, tokenizer/mm_host.py, tokenizer/qwen_vl_lite.py, the scheduler's _encode_multimodal and rope tables, the data:-only image_url renderer, and their tests. What Kai keeps is adapted to the new shape: - --spec-mtp: the verify-window graph, the draft-head chain graph and the eager draft and check-step batches carry [3, n] rope positions (logical + the request's mrope_delta) on an mrope model; the draft head embeds the placeholder token where an image row's successor is a content pad id past the vocab. - --prefill-mixer-pieces: a piece takes its columns of the 3-axis positions; an image chunk splits like a text one (its soft tokens are in the stream before the pieces run). - --prefill-chunk-budget: the transient probe feeds 3-axis positions, like upstream's warmup. - PLE on disk: the hash windows go through upstream's placeholder restore, the verify window's drafts included. - The pipeline window, --dense-quant and --host-embedding config paths sit on upstream's active_encoders / hf_config stripping; the loader passes include_mtp and include_vision. - A chunk checkpoint and --prefix-disk-cache no longer exclude image requests: their ids now name the image. FlashML-org#462 fixes the W4A16_NVFP4 input_scale wait Kai had fixed in e4ffedf, more completely (it also infers W4A16 from config_groups and skips a stored input_scale the scheme does not declare); upstream's modelopt.py is taken as is and Kai's duplicate test is dropped. FlashML-org#463 does not touch a model Kai runs (qwen3_5_moe and qwen4_exp already compute the shared expert first). The merge base is upstream 9535656: the history rewrite of 2026-09-13 dropped GitHub's signature from the previous sync's upstream commits, so git would otherwise take fb7f732. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nomanoma121
pushed a commit
to nomanoma121/My-FreeToken
that referenced
this pull request
Sep 16, 2026
…and what is Qwen VL only - README.md, docs/kai.md: merged with upstream main at afd99cb. - docs/kai.md: the Flash-Next row reports upstream's image path on the two RTX 3060s (--mm-encoder-weights cpu and host in float32: colours 6/6, the description to the end, 2.6-5.8 accepted per step with --spec-mtp 5). - docs/image-input.md: upstream's families since FlashML-org#454, linking models.md. - --mm-encoder-dtype (docs/cli.md, docs/image-input.md, --help) and the pin budget row name the Qwen VL vision tower: other families' towers follow the model dtype and are not counted. - docs/kai.md, docs/image-input.md: --mm-encoder-weights cpu serves the Qwen3.5/3.6 and Qwen3.8-Flash-Next towers only; DeepStack and the other image families are refused at start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MT-z
added a commit
to MT-z/FreeToken
that referenced
this pull request
Sep 16, 2026
…lashML-org#454's Upstream's FlashML-org#454 (feat/qwen-mm-squash) implements the same feature on a different shape: a `freetoken/mm/` subsystem with per-family processors and an encoder cache, a `models/qwen3_vl/` package, and a row-index scatter in `models/blocks.py`. This branch reached it through `models/qwen3_5_moe/vision.py`, `tokenizer/tokenize.py` and the scheduler -- 22 commits, 2,170 lines. Merging the two produces both, not one. The merge base carries neither implementation, so git treats each side as an addition and keeps both wherever the text does not overlap: a trial merge left `models/qwen3_5_moe/model.py` importing two vision towers, and did it without a conflict. Removing this side first, as its own commit, is what makes the merge a merge. What goes, and what that costs: - the Qwen3.5 vision tower, the mm tokenize/scheduler path, the API image surfaces, `/img`, and the `FREETOKEN_LOAD_VISION` gate. FlashML-org#454 serves all of these, and `/img` comes back with it. - `af54d1c`, which closed gemma4's batch-wide scatter hole. It turned `Batch.mm_embeds` into a bool, which FlashML-org#454 needs as a tensor, so the two cannot both stand. FlashML-org#454 registers no `mm_processor` for gemma4, so upstream's `_merge_multimodal` -- hole and all -- becomes unreachable there. Nothing else was meant to move. The non-mm work in the same files stays: the ping-pong donations and prefix-cache diagnostics in `scheduler/cache.py`, the GDN fork tracking in `prefill.py`, the developer-role mapping in `tokenize.py`, the pidfile and stop-signal chain in `api_server.py`. `models/config.py` keeps only upstream's `VISION_KEY_PREFIXES`; `scheduler/cache.py` keys the prefix cache on `req.input_ids` again, as upstream does. Verified by blame rather than by the suite: a test cannot fail for a line it never covered. The check is that no line owned by the 22 removed commits survives, and that every other commit's line count is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MT-z
added a commit
to MT-z/FreeToken
that referenced
this pull request
Sep 16, 2026
…image input on the Qwen VL families Takes the vision implementation this branch removed in the previous commit, plus the two of its own commits that survive it: the checkpoint-role fix Ornith needs to load, and the bounded uvicorn stop. `/img` comes back with the merge. Nine files conflicted, all of them this branch's own non-vision work meeting FlashML-org#454's: - `stats.py`: kept `effective_context_length` and the three-argument `derive_model_card`; FlashML-org#454's `input_modalities` had already merged into the body. Losing the first would re-advertise the rope ceiling over a smaller KV pool, which is what made Claude Code's first turn 400. - `openai_api.py`, `generation.py`: kept this branch's richer forms (n > 1, echo, FIM suffix, the validated `resolved_*` sampling, presence penalty) and threaded FlashML-org#411's `default_max_tokens` through both API paths -- `/v1/completions` reached `_resolve_sampling` without it, which would have ignored `--max-output-tokens` on exactly the surface FlashML-org#411 exists to fix. - `args.py`: both sides' fields, including FlashML-org#454's media allowlists next to `--sampling-override`, `--template-kwarg`, `--pidfile` and `--no-system-in-place`. - `kvcache/`: `kv_quant` and `mrope` are independent additions to the same signature. - `api_server.py`: kept the `with stack:` teardown around uvicorn.run. - `prefill.py`: took FlashML-org#454's `mm_items`/`mrope_*` admission and dropped the `image_token_id` field the removal left without a reader. The duplicate-implementation check that motivated the removal commit: the four files that carried both towers after a trial merge -- `qwen3_5_moe/model.py`, its `weight.py`, `scheduler/scheduler.py`, `tokenizer/tokenize.py` -- now carry only FlashML-org#454's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MT-z
added a commit
to MT-z/FreeToken
that referenced
this pull request
Sep 16, 2026
… tests still drive it FlashML-org#454 reads five config attributes directly. Every one of them exists on a real EngineConfig/ServerArgs, so FlashML-org#454's own suite never notices -- but this branch drives the same code from tests that build the config as a SimpleNamespace, and those stubs predate the fields. Five failures, all AttributeError, none of them about multimodal: kvcache/__init__.py model_config.model_is_mrope test_qsa_pool_fp8 (factory) kvcache/qsa_pool.py same, inside kv_cost test_qsa_pool_fp8 (kv_cost) server/stats.py config.served_modalities test_effective_context_length server/generation.py state.config.mm.max_pixels test_logprobs_api, test_openai_extras server/generation.py the second call site This is the shape FlashML-org#300 flagged for the scheduler hooks and that this branch already carries a fix for: a hook reached from a stub must not require the whole config object. derive_model_card is additionally a metadata route, which must never raise. Only these five. The direct reads in engine.py (nine) and mm/media.py (three) are on paths that see a real config and nothing else -- turning those defensive would hide a genuinely missing field rather than tolerate a deliberately partial stub. Also threads FlashML-org#411's default_max_tokens from _resolve_sampling into resolve_sampling. The merge kept this branch's richer _resolve_sampling body, which did not carry the argument, so /v1/chat/completions and /v1/completions both fell back to the 32k built-in and ignored --max-output-tokens. FlashML-org#411's own test caught it. Full suite against try/all before the vision swap: 8 failed / 2004 passed vs 8 failed / 1996 passed, same failure set -- the three fp8 ones are this branch's standing failures from the FlashML-org#354 take, not new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MT-z
added a commit
to MT-z/FreeToken
that referenced
this pull request
Sep 16, 2026
45 ファイルの衝突。方針は Dr.GD の fork に倣った ── **上流が自前の実装を持った所は こちらを捨てて上流へ寄せる**(gdevenyi は上流が FlashML-org#454 等を入れた時点で自分の vision PR FlashML-org#386 を 5080deploy へ運ばず、テスト 1 本を上流実装に合わせただけだった)。 この木も既に `f37575b refactor(vision): drop this branch's own image stack, ahead of taking FlashML-org#454's` で同じ判断をしており、今回はその続きにあたる。 上流へ寄せたもの(mm 層は上流が上位互換: block_ends、image span を切らない chunk 境界、 token 予算): mm/{config,processor,processors/qwen_vl}.py, scheduler/mm.py, models/qwen3_vl/*, models/{config,weight}.py, models/gemma4/*, llm/llm.py, tokenizer/*, server/{generation,anthropic_api}.py, message/tokenizer.py, mm 系テスト一式 - `--mm-max-pixels` は上流の `--image-min-tokens` / `--image-max-tokens` / `--mm-processor-kwargs` が置き換えたので、argparse の定義ごと外した - `models/qwen3_5_moe/weight.py` は**上流を採った**。上流の scheme reader は input_scale 欠落を `missing()` で報告する形に変わっており、こちらの 「optional role を埋める finalize()」(Ornith-1.5 の shared_expert / lm_head が input_scale を持たない件)とは別方式。**Ornith の実ロードで要確認** 両方を残したもの(独立した 2 機能が同じ位置に足さっていた): kernel/triton/attention.py と attention/triton.py —— MT の fp8 KV scale と 上流の mm block_ends。docstring は手で併合 engine/engine.py —— MT の geometry 込み benchbw 判定と、上流の FlashML-org#445(GB10 で unified memory なら fused)。`default_backend == "offload" and not unified_memory and recommendation == "hybrid"` へ手で併合 server/args.py(import / system_in_place / sampling_override 解析は MT、画像予算は上流)、 server/stats.py、tokenizer/server.py、scheduler/prefill.py server/openai_api.py —— MT の n>1 と上流の GenerationError を手で併合 こちらを残したもの: kvcache/{__init__,qsa_pool}.py(kv_quant と、namespace stub 用の getattr 保護)、engine.py の MOE_STATS_INTERVAL、 tests/models/test_muse_glimmer.py の toy 次元(8.7GiB -> 4.9MiB)に vision=True を追加 pyproject の transformers ピンは上流へ(>=5.16,<5.17。この箱は 5.16.1)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
trcwebdesign
pushed a commit
to trcwebdesign/FreeToken
that referenced
this pull request
Sep 21, 2026
* feat(mm): serve image input on the Qwen VL families - frontend: OpenAI image_url, Anthropic image blocks and Responses input_image (http(s), base64, gated file://) become MMItems in the tokenizer workers; image spans carry content pseudo ids so the radix cache keys them by content; mrope positions are precomputed per request - registry: EncoderSpec per family (Qwen3-VL, Qwen3.6, Qwen3.8-Flash-Next register a vision encoder serving image) and ModelSpec.mm_processor; EngineConfig.active_encoders decides what a process builds, served_modalities what it accepts; --text-model-only, --mm-disable, --mm-encoder-weights, --mm-embed-cache-device, --mm-max-pixels - engine and scheduler: per-request encoder-cache row ownership, chunk planning on the batch (jobs, gather rows, scatter rows), items encoded right before the LM forward and gathered into batch.mm_embeds - models: the Qwen VL vision tower on the text tower's TP layers with DeepStack, block weights streamed from pinned host banks by default, merger outputs parked on the host between blocks; SupportsMultimodal (encode, place_encoder_weights) and a shared embed_input_ids - server: image inputs rejected with a client-facing reason when no vision encoder is served; GET /v1/stats reports model.input_modalities * feat(mm): replace the pixel cap with a token budget and processor kwargs - --image-min-tokens / --image-max-tokens replace --mm-max-pixels; MMProcessor.get_mm_processor_kwargs turns the MultimodalConfig into the processor call's arguments, the default passes mm.processor_kwargs through and Qwen VL converts the budget to pixel areas in size.shortest_edge / longest_edge - --mm-processor-kwargs hands a JSON object to the checkpoint's image processor call, applied after the token budget - MMProcessor is an ABC that owns the model path, the config and the lazily loaded image processor; get_mm_processor returns None when --mm-disable leaves no encoder to serve - the frontend tokenizer, the tokenizer worker and the offline LLM build one processor each and hand it to TokenizeManager, so the tokenize message carries no per-request pixel cap
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.
Image input for the Qwen VL families: Qwen3-VL (dense and MoE), Qwen3.6 (27B dense, 35B-A3B), Qwen3.8-Flash-Next. Images arrive on all three protocols, are encoded by the family's vision tower inside the engine and served through the existing prefix cache, chunked prefill and offload paths. Text-only serving is unchanged: a checkpoint without a vision section, or one started with
--text-model-only, builds no tower and takes no extra VRAM.How it fits together
MMItems in the tokenizer workers; the engine never sees bytes.image_url, Anthropicimageblocks and Responsesinput_image; sources are http(s), base64, andfile://behind--allowed-local-media-path; URL hosts are gated by--allowed-media-domains.tool_resultare rejected with a 400 rather than dropped: chat templates render tool messages as text.MMProcessorruns the HF image processor, expands the placeholder to the image's token count, and writes a content-derived pseudo id on the image span so the radix cache keys it by content (the scheme sglang uses).--image-min-tokens/--image-max-tokensgive a per-image token budget; the family'sMMProcessor.get_mm_processor_kwargsconverts it to its HF image processor's own limits (Qwen VL: one token is a 32x32 patch of the resized image, so the budget becomes pixel areas insize.shortest_edge/longest_edge), and--mm-processor-kwargshands a JSON object straight to that call for family-specific knobs, applied after the budget. The processor is built once per process with these settings and injected into the tokenizer, so the same image always yields the same tokens, hash and cache key.EncoderSpec(kind, config_key, modalities)onModelSpec: Qwen3-VL, Qwen3.6 and Qwen3.8-Flash-Next register avisiontower servingimage;ModelSpec.mm_processornames the processor.EngineConfig.active_encoders= registered by the family, present in the checkpoint config, not disabled by--mm-disable; the parser never sees the config section of a tower that is not built, so a text-only process also has no mrope.served_modalitiesderives from the built encoders and gates the API in the server process without asking the engine.[rows, D]embedding per image, owned by the requests that still have rows to gather: an image survives chunk boundaries, repeats within a request and sharing across requests, and dies when the last row is gathered or the request aborts.scheduler/mm.py).batch.mm_embedsand passesbatch.mm_rows; the model does oneindex_copy_and never scans ids.--mm-encoder-weights host): about 60 MiB of VRAM instead of the whole tower, at a small-image latency cost.MM_PAD_SHIFT_VALUE + hash % 2^30, above every real token), so the same image under the same prefix is a cache hit and a different image forks the tree with no extra key. The embedding lookup clamps those ids into the vocab and the copy overwrites the rows, so no layer ever sees them; the one feature that hashes token ids besides the embedding, Qwen3.8's per-layer n-gram embedding, restores<|image_pad|>on image rows first and matches HF'sple_input_ids.SupportsMultimodal(encode,place_encoder_weights) is the engine's contract, checked at start-up;embed_input_idsinmodels/blocks.pyis the shared step every multimodal text forward calls.GET /v1/statscarriesmodel.input_modalities(["text"]or["text", "image"]) so a client can gate its attachment controls.Flags
--text-model-only--mm-disablewith every kind)--mm-disable {vision,audio}--mm-encoder-weights {host,gpu}--mm-embed-cache-device {cpu,cuda}--image-min-tokens N,--image-max-tokens Nsize.shortest_edge/longest_edge; checkpoint defaults 64 to 16384 tokens). Families with fixed tiers honor the maximum only--mm-processor-kwargs JSON{"size": {"longest_edge": 1048576}}); applied after the budget, so an explicit key winsVerification
--text-model-onlyon a VL checkpoint matches the text model.--image-max-tokens 2048caps a 3840x2160 image at 2048 tokens on Qwen3.6-27B and Qwen3-VL-8B where the default takes more;--image-max-tokens 64on Qwen3-VL-8B still describes the smoke image correctly; the flags land inMultimodalConfigand a minimum above the maximum is rejected at start-up.Not in this PR
grid_thwwitht > 1, the processor does not sample frames yet;EncoderSpecand--mm-disableare ready for an audio tower.