Skip to content

feat(mm): serve image input on the Qwen families - #454

Merged
jason-fxz merged 2 commits into
mainfrom
feat/qwen-mm-squash
Sep 13, 2026
Merged

jason-fxz merged 2 commits into
mainfrom
feat/qwen-mm-squash

Conversation

@jason-fxz

@jason-fxz jason-fxz commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Frontend. Images become MMItems in the tokenizer workers; the engine never sees bytes.
    • OpenAI image_url, Anthropic image blocks and Responses input_image; sources are http(s), base64, and file:// behind --allowed-local-media-path; URL hosts are gated by --allowed-media-domains.
    • Images inside Anthropic tool_result are rejected with a 400 rather than dropped: chat templates render tool messages as text.
    • The family's MMProcessor runs 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).
    • Mrope positions are precomputed per request and travel with the message; the tokenizer workers stay free of model code.
    • Image resolution is a server setting, not a per-request one. --image-min-tokens / --image-max-tokens give a per-image token budget; the family's MMProcessor.get_mm_processor_kwargs converts 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 in size.shortest_edge / longest_edge), and --mm-processor-kwargs hands 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.
  • Registration. One registry row per family decides everything downstream.
    • EncoderSpec(kind, config_key, modalities) on ModelSpec: Qwen3-VL, Qwen3.6 and Qwen3.8-Flash-Next register a vision tower serving image; ModelSpec.mm_processor names 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_modalities derives from the built encoders and gates the API in the server process without asking the engine.
  • Scheduler and engine. Encoder work is planned per prefill batch and cached per image.
    • The encoder cache holds one [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.
    • Each prefill batch gets a plan: which items to encode (cache misses, one job per hash), which embedding rows to gather, and the batch rows they land on (scheduler/mm.py).
    • The engine encodes right before the LM forward, gathers into batch.mm_embeds and passes batch.mm_rows; the model does one index_copy_ and never scans ids.
  • Models. The Qwen VL tower reuses the text stack and stays small on the GPU.
    • Built on the text tower's TP-aware projection layers (fused qkv, column/row parallel MLP), so quantization config and TP sharding apply to it like any other layer; DeepStack taps for Qwen3-VL ride along as extra output columns.
    • Block weights stream from pinned host banks two blocks at a time behind the compute by default (--mm-encoder-weights host): about 60 MiB of VRAM instead of the whole tower, at a small-image latency cost.
    • Merger outputs are parked on the host between blocks, so the activation peak at 4096x4096 is the attention working set of one block (0.72 GiB on Qwen3-VL-8B).
    • The ids the model receives are the radix keys: image rows hold the content pseudo id (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's ple_input_ids.
    • SupportsMultimodal (encode, place_encoder_weights) is the engine's contract, checked at start-up; embed_input_ids in models/blocks.py is the shared step every multimodal text forward calls.
  • Server. Capability is reported, not inferred from the checkpoint.
    • GET /v1/stats carries model.input_modalities (["text"] or ["text", "image"]) so a client can gate its attachment controls.
    • A request with images against a server that serves none gets a client-facing reason naming the flag that disabled it.

Flags

Flag Default Meaning
--text-model-only off Build no encoder tower, reject every multimodal input (same as --mm-disable with every kind)
--mm-disable {vision,audio} none Leave the named towers unbuilt
--mm-encoder-weights {host,gpu} host Stream the tower's block weights from pinned host banks, or keep them resident
--mm-embed-cache-device {cpu,cuda} cpu Where encoded embeddings wait between prefill chunks
--image-min-tokens N, --image-max-tokens N processor defaults Per-image token budget: every image is resized to take between N_min and N_max tokens, in the family's own units (Qwen VL: N x 1024 pixels into size.shortest_edge / longest_edge; checkpoint defaults 64 to 16384 tokens). Families with fixed tiers honor the maximum only
--mm-processor-kwargs JSON none JSON object of extra keyword arguments for the checkpoint's image processor call, for knobs the budget does not cover (Qwen VL: {"size": {"longest_edge": 1048576}}); applied after the budget, so an explicit key wins

Verification

  • Vision tower bitwise equal to the HF reference (fp32 truth) on Qwen3-VL-8B at 448x448, 1024x1024 and 4096x4096, with resident and host-streamed weights, and with the parked-merger forward against a naive reference forward.
  • Greedy image smokes with identical outputs across every refactor: Qwen3-VL-8B (bf16), Qwen3.6-27B (bf16 and NVFP4), Qwen3.6-35B-A3B (FP8, MoE offload), Qwen3.8-Flash-Next (NVFP4, MoE offload); a request with the same image twice split across a chunk boundary; two image requests admitted into one prefill batch match their single-request outputs; --text-model-only on a VL checkpoint matches the text model.
  • --image-max-tokens 2048 caps a 3840x2160 image at 2048 tokens on Qwen3.6-27B and Qwen3-VL-8B where the default takes more; --image-max-tokens 64 on Qwen3-VL-8B still describes the smoke image correctly; the flags land in MultimodalConfig and a minimum above the maximum is rejected at start-up.
  • Memory on Qwen3-VL-8B at 4096x4096: activation peak 1.99 -> 0.72 GiB; resident tower weights 1099 -> 373 MiB with host streaming. Cost of host streaming: 448x448 encode 7.5 -> 17.0 ms, no change from 1024x1024 up. A rebuild to the current cache capacity succeeds with host-streamed weights.

Not in this PR

  • Video and audio input: the tower already accepts grid_thw with t > 1, the processor does not sample frames yet; EncoderSpec and --mm-disable are ready for an audio tower.
  • Tensor-parallel serving of the vision tower is wired (per-rank shards) but untested end to end.
  • Gemma-4 image input follows on a stacked branch.

- 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
@jason-fxz
jason-fxz merged commit 08d728d into main Sep 13, 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request multimodal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant