Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
af84f44
feat(ds4v): MIX converter for Vision-Exp safetensors plus codec test
marcelormendes Sep 4, 2026
a53acc9
test(ds4v): reproduce collapsed fitter and repair clean-build test wi…
marcelormendes Sep 4, 2026
9e403cd
fix(ds4v): separate collapsed BF16 codebook levels and stamp repairs
marcelormendes Sep 4, 2026
07e3284
test(ds4v): replay an individual source expert through calibration
marcelormendes Sep 4, 2026
fd5a994
feat(ds4v): export lossless vision projector GGUF
marcelormendes Sep 4, 2026
ef64f62
chore(ds4v): trim exporter test commentary
marcelormendes Sep 4, 2026
c563fbf
feat(ds4v): implement isolated BF16 vision tower runtime and CPU probes
marcelormendes Sep 4, 2026
dfa9719
fix(ds4v): reserve measured scratch before graph allocation
marcelormendes Sep 4, 2026
076de6e
test(ds4v): compare parent stages and reject malformed projectors
marcelormendes Sep 4, 2026
8758e09
fix(ds4v): pin diagnostic snapshots independently of tensor views
marcelormendes Sep 4, 2026
8bac576
test(ds4v): isolate block kernels with native inputs and BF16 ULP met…
marcelormendes Sep 4, 2026
d26f1c9
test(ds4v): verify transactional reload and strict tensor table rejec…
marcelormendes Sep 4, 2026
da3fa6e
test(ds4v): measure parent BF16 Math SDPA sensitivity
marcelormendes Sep 4, 2026
1cf2b26
test(ds4v): identify actual parent SDPA dispatch and parser rejection
marcelormendes Sep 4, 2026
4573248
fix(ds4v): preserve parent Math SDPA operand scaling order
marcelormendes Sep 4, 2026
2bfba86
test(ds4v): cover multihead attention and BF16 halfway rounding
marcelormendes Sep 4, 2026
a23d7cb
docs(ds4v): record CPU qualification and remaining numerical limits
marcelormendes Sep 4, 2026
8e28cfb
test(ds4v): isolate vendored GGUF reader import
marcelormendes Sep 4, 2026
782bf15
Revert "test(ds4v): isolate vendored GGUF reader import"
marcelormendes Sep 4, 2026
5845ed5
test(ds4v): cover complete image token budget
marcelormendes Sep 4, 2026
5bf705e
fix(ds4v): bound the complete image block before encoding
marcelormendes Sep 4, 2026
e30c1b0
test(ds4v): specify image routing and raw visibility policy
marcelormendes Sep 4, 2026
a238e8e
feat(ds4v): add exact RGB vision preprocessing
marcelormendes Sep 4, 2026
60ce979
feat(ds4v): add bounded image routing and raw visibility helpers
marcelormendes Sep 4, 2026
0306a3f
test(ds4v): specify bounded image data URL transport
marcelormendes Sep 4, 2026
e09f8da
feat(ds4v): parse bounded image data URLs and preserve content order
marcelormendes Sep 4, 2026
5da9272
docs(ds4v): record image policy contracts and CPU proof
marcelormendes Sep 4, 2026
4acc20d
docs(ds4v): record image transport contract and proof
marcelormendes Sep 4, 2026
1fa0a2e
feat(ds4v): add bounded JPEG and PNG decode gate
marcelormendes Sep 4, 2026
127bb61
build(ds4v): prepare explicit HIP tower qualification probe
marcelormendes Sep 4, 2026
2af9862
test(ds4v): bound image message nesting before JSON copy
marcelormendes Sep 4, 2026
60af19f
fix(ds4v): isolate libjpeg probe build
marcelormendes Sep 4, 2026
25f6105
fix(ds4v): validate message depth and placeholders before copying
marcelormendes Sep 4, 2026
724e4ef
chore(ds4v): keep codec probe dependency focused
marcelormendes Sep 4, 2026
4bf7270
fix(ds4v): reuse server HIP compatibility definitions in probe
marcelormendes Sep 4, 2026
6a2b381
test(ds4v): cover RGB conversion and EXIF handling
marcelormendes Sep 4, 2026
a28abfd
test(ds4v): capture decoder review regressions
marcelormendes Sep 4, 2026
c24e823
test(ds4v): specify transactional image prompt preparation
marcelormendes Sep 4, 2026
6d28845
fix(ds4v): bound PNG inflate and match source decoding
marcelormendes Sep 4, 2026
a2489a4
feat(ds4v): prepare owned image prompts with checked expansion
marcelormendes Sep 4, 2026
859f2f7
test(ds4v): prove prompt ownership and document CPU qualification
marcelormendes Sep 4, 2026
edb3b0e
docs(ds4v): correct LodePNG notice
marcelormendes Sep 4, 2026
52c6ba7
feat(ds4v): compose verified prompt and preprocessing units
marcelormendes Sep 4, 2026
101f69c
feat(ds4v): compose verified image transport
marcelormendes Sep 4, 2026
20ae935
feat(ds4v): compose verified image routing policy
marcelormendes Sep 4, 2026
1096fab
test(ds4v): specify CPU image preparation composition
marcelormendes Sep 4, 2026
a5aa8a3
test(ds4v): link existing Jinja unicode helper in composition probe
marcelormendes Sep 4, 2026
15a10c5
feat(ds4v): compose accepted image preparation in CPU probe
marcelormendes Sep 4, 2026
0065158
test(ds4v): freeze bounded CPU preparation composition proof
marcelormendes Sep 4, 2026
a353f1e
test(ds4v): isolate biased linear BF16 boundary regression
marcelormendes Sep 5, 2026
43652c1
test(ds4v): allocate probe scratch after size preflight
marcelormendes Sep 5, 2026
8bec967
fix(ds4v): retain biased linear products until final BF16 round
marcelormendes Sep 5, 2026
be8b0f1
fix(ds4v): scope biased product workaround to GPU backends
marcelormendes Sep 5, 2026
983be86
test(ds4v): compare shared vision linears to frozen HIP source fixtures
marcelormendes Sep 5, 2026
ee085f3
fix(ds4v): use explicit HIP Lt fused BF16 bias operation
marcelormendes Sep 5, 2026
b3ca801
fix(ggml): align RPC ABI guard and reject HIP op before CPU dispatch
marcelormendes Sep 5, 2026
969318c
test(ds4v): enforce local HIP op contract and preserved CPU graph
marcelormendes Sep 5, 2026
add215f
test(ds4v): verify full-tower Lt dispatch and retained workspace acco…
marcelormendes Sep 5, 2026
6137f43
test(ds4v): require explicit HIP capability in full qualification probe
marcelormendes Sep 5, 2026
3191e7e
fix: reserve buffers in DS4V source probe before allocation
marcelormendes Sep 5, 2026
f64d945
fix: match source HIP math for unbiased vision projections
marcelormendes Sep 5, 2026
53bf07d
test: retain the generic linear rounding oracle
marcelormendes Sep 5, 2026
4e601d0
Match original DS4V HIP vision normalization arithmetic
marcelormendes Sep 5, 2026
ed661d0
Verify both HIP vision dispatch counters in source probes
marcelormendes Sep 5, 2026
4102da0
Match HIP vision rotary tables to source GPU math
marcelormendes Sep 5, 2026
686285f
Match HIP vision softmax and attention products to source order
marcelormendes Sep 5, 2026
e1ccfbe
Merge qualified DS4V HIP vision encoder into image integration
marcelormendes Sep 5, 2026
edc61af
test(ds4): expose BF16 norm affine compatibility regression
marcelormendes Sep 5, 2026
c574604
fix(ds4): widen BF16 norm vectors at the affine boundary
marcelormendes Sep 5, 2026
03edb38
feat(ds4v): integrate bounded image prompts into HIP serving
marcelormendes Sep 5, 2026
4f388d1
Fix DS4V startup accounting for reclaimable HIP memory pools
marcelormendes Sep 5, 2026
14754e4
Revert "Fix DS4V startup accounting for reclaimable HIP memory pools"
marcelormendes Sep 5, 2026
91cc90a
Reclaim copied mmap source pages during hybrid GPU loading
marcelormendes Sep 5, 2026
439c8c3
Release copied model file cache during hybrid loading
marcelormendes Sep 5, 2026
6914f9d
Stage HIP dense uploads through bounded host scratch
marcelormendes Sep 5, 2026
5dc0fdd
Release cached GPU temporaries before image admission
marcelormendes Sep 6, 2026
dbaeb1e
Trim cached GPU pools before bulk heterogeneous prefill
marcelormendes Sep 6, 2026
5296836
Release bulk prefill arenas before ordinary hybrid decode
marcelormendes Sep 6, 2026
5fe8713
Bound paired decode attention cache to the current shape
marcelormendes Sep 6, 2026
be48d1b
fix(deepseek4): trim returned pool blocks between long prefill chunks
marcelormendes Sep 6, 2026
c76edfa
fix(deepseek4): acknowledge every HC worker job generation
marcelormendes Sep 6, 2026
7e851cb
feat(ds4v): add bounded IQ85 quantization from original weights
marcelormendes Sep 6, 2026
39c0791
docs(ds4v): record vision continuation status, decision log and IQ85 …
marcelormendes Sep 10, 2026
6d762d8
Merge upstream/main (2f0eff05) into ds4v/quant85
marcelormendes Sep 10, 2026
c6030da
chore(ds4v): ship the vision qualification kit and reference receipts
marcelormendes Sep 10, 2026
344713d
Merge main (e0048e019) into ds4v/quant85
Sep 21, 2026
94856ca
build(ds4v): make hipBLASLt optional for the HIP backend
Sep 21, 2026
6f784d4
chore(ds4v): drop research logs and evidence receipts
Sep 21, 2026
7651512
fix(ds4v): keep the text path identical to main without --mmproj
Sep 21, 2026
f248dfd
chore(ds4v): drop the rejected IQ85 recipe
Sep 21, 2026
212b3a6
test(ds4v): run the image unit tests in the main build, drop standalo…
Sep 21, 2026
c42b579
fix(ggml): correct the RPC op-count guard and shorten the vendor note
Sep 21, 2026
fa7ff8f
docs(ds4v): state the real verification status of image serving
Sep 21, 2026
633df96
fix(ds4v): scan for image markers only with a projector loaded; use m…
Sep 21, 2026
5c81edc
fix(server): leave image parts alone on backends without image input
Sep 21, 2026
02b510f
test(ds4): follow the generalised image-bias loader message
Sep 21, 2026
89fd666
fix(server): run image extraction and redaction only for an image-cap…
Sep 21, 2026
f9cc9e3
fix(ds4v): refuse --mmproj at startup when the build has no vision ops
Sep 21, 2026
37dfa4d
fix(ds4v): ignore the MTP block's image bias in the loader check; dro…
Sep 21, 2026
7d4d7ac
test(ds4): the loader no longer pins the DS4V layer count and vocabulary
Sep 21, 2026
4196a70
fix(ds4v): check for the vision ops before loading weights; report th…
Sep 21, 2026
13a5dae
build(ds4v): pass DOWNLOAD_EXTRACT_TIMESTAMP only on CMake >= 3.24
Sep 21, 2026
84591eb
build(ds4v): build the MIX converter only when its source is in the tree
Sep 21, 2026
a6e4e72
refactor(vision): move the model-independent image pieces out of deep…
Sep 21, 2026
57fe97d
feat(ds4v): load the image router bias from published GGUFs
Sep 21, 2026
8544d76
feat(ds4): load llama.cpp-converted DeepSeek4 GGUFs without deepseek4…
Sep 21, 2026
b91eea9
fix(ds4v): count the GPU driver's page pool in the image memory admis…
Sep 21, 2026
f023a8a
build(docker): build the DS4V vision ops in the ROCm image; keep the …
Sep 21, 2026
49cabab
feat(ds4v): image input on one GPU holding the whole model
Sep 21, 2026
b3409fc
fix(ds4v): review fixes - images outside the last prefill chunk, 16-b…
Sep 21, 2026
38e0cfd
Merge remote-tracking branch 'lucebox/main' into pr722-clean
Sep 21, 2026
12a0b29
build(vision): vendor lodepng instead of downloading it at configure …
Sep 21, 2026
d1d5be7
refactor(vision): share the Pillow-exact resize, add a projector file…
Sep 21, 2026
b52c981
feat(qwen35): image input for Qwen3.5 / Qwen3.8 through --mmproj
Sep 21, 2026
47e9259
Merge main: engine rename from dflash to luce
Sep 21, 2026
cbf8691
fix: review findings in the page pool estimate and the MIX converter
Sep 21, 2026
f18750f
fix(qwen35): write the request-visible vision config once, never snap…
Sep 21, 2026
8cca0e9
fix(vision): review findings in the Qwen image path, the projector re…
Sep 21, 2026
27b08e7
fix(vision): release the projector before its backend, smaller review…
Sep 21, 2026
47e4703
perf(qwen35): fused attention in the vision tower
Sep 22, 2026
901ef2d
docs(vision): Q8_0 projector measurement
Sep 22, 2026
c202867
perf(tools): quantize experts on every core in the MIX converter
Sep 22, 2026
6e5a0ca
fix(deepseek4): grouped output projection only for a quantized tensor
Sep 22, 2026
0a7ed37
fix(tools): name the MIX file after its checkpoint
Sep 22, 2026
61c07ff
docs(vision): DS4V on our own ROCMFP MIX conversion
Sep 22, 2026
6461136
feat(tools): write the MIX file with the shipped DeepSeek-V4-Flash re…
Sep 22, 2026
0d349b8
feat(tools): per-expert importance and embedded codebooks in the MIX …
Sep 22, 2026
a098ecd
docs(vision): DS4V MIX results with the shipped recipe
Sep 22, 2026
25735a3
docs(vision): Qwen3.8 results with the Lucebox model file and a Q8_0 …
Sep 22, 2026
b35966e
build(vision): fetch lodepng at a pinned commit instead of vendoring it
Sep 22, 2026
34d0b06
docs(vision): quick start from the published Lucebox files, LUCE_MMPR…
Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile.rocm
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ ENV PATH=/opt/rocm/bin:/opt/rocm/lib/llvm/bin:${PATH}
# hipCUB/rocPRIM: ggml's ROCm argsort/top-k kernels use their headers for the
# large-N device-sort path. rocWMMA is also required by the DS4 indexed
# prefill kernel. The ROCm apt repository ships these headers separately from
# the base HIP toolchain.
# the base HIP toolchain. hipBLASLt is optional for ggml-hip: with its -dev
# package present the DS4V vision ops are built, without it --mmproj is refused.
# (deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt does find_package(hipblas)
# for its BLAS matmul path). The rocm/dev-ubuntu base ships the HIP toolchain
# but NOT the math libs, so they are installed explicitly from the ROCm apt
Expand All @@ -59,6 +60,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
git \
git-lfs \
hipblas-dev \
hipblaslt-dev \
hipcub-dev \
libcurl4-openssl-dev \
ninja-build \
Expand Down Expand Up @@ -151,6 +153,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
hipblas \
hipblaslt \
libgomp1 \
pciutils \
rocblas \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ See [Continuous batching in Lucebox](https://www.lucebox.com/blog/continuous-bat
| OpenAI Chat Completions, Responses, and Anthropic Messages | [API reference](server/docs/API.md) |
| CUDA, HIP, and mixed-device placement | [Mixed-backend guide](server/docs/MIXED_BACKEND.md) |
| DeepSeek V4 single-device and heterogeneous profiles | [DeepSeek V4 guide](server/docs/DS4.md) |
| Image input (Qwen3.8, DeepSeek V4 Flash Vision) | [Image input guide](docs/image-input.md) |
| Environment variables | [Environment reference](server/docs/ENVIRONMENT.md) |
| Server internals | [Architecture](server/docs/ARCHITECTURE.md) |
| Client integration and qualification | [Harness guide](harness/README.md) |
Expand Down
37 changes: 37 additions & 0 deletions docs/ds4v-mmproj.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# DeepSeek-V4 vision projector export

`server/tools/export_ds4v_mmproj.py` extracts the vision tower and aligner from
the verified DeepSeek-V4 parent model without loading or changing tensor values.
It requires only Python's standard library. The output is published as
[`DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf`](https://huggingface.co/Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3);
run the exporter only to rebuild it.

```bash
python3 server/tools/export_ds4v_mmproj.py \
/models/DeepSeek-V4-Flash-Vision-Uncensored \
/models/ds4v-mmproj.gguf
```

The output path must not exist. The exporter validates `config.json`, the
safetensors index, all 267 required names and shapes, BF16 payload lengths,
source bounds, and shard paths before it writes. It publishes the finished file
atomically and removes its temporary file after any pre-publication failure.

The GGUF keeps every `vision.*`, `aligner.*`, and `image_*` name, shape, dtype,
and payload byte unchanged. `general.architecture` is `deepseek4_vision`.
Required `deepseek4.vision.*` metadata records the 32-block, width-1024,
16-head tower; head width 64; patch size 14; intermediate width 2816; 2D RoPE
layout and theta 10000; ratio-3, width-9216 aligner; language width 4096;
vocabulary 129280; RMS epsilon `1e-6`; image token/pixel/aspect bounds; RGB
mean/std `0.5`; channel-major patches; N-layout recipe version 1 with
compression alignment 4; bottom/right aligner padding; channel-first unfold;
and exact GELU.

Run the focused synthetic suite with:

```bash
python3 -m unittest -v server.tests.test_export_ds4v_mmproj
```

When NumPy is available, the suite also opens the result with llama.cpp's
vendored `GGUFReader`, independently of the exporter's writer.
300 changes: 300 additions & 0 deletions docs/image-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
# Image input

The server accepts JPEG and PNG images through OpenAI chat completions when a
model is started with its vision projector, `--mmproj <file>`. Without
`--mmproj` nothing in the text serving path changes.

| Model | Decoder | Projector | Runs on |
| --- | --- | --- | --- |
| Qwen3.8-27B | [`Qwen3.8-27B-IQ4_XS-pure.gguf`](https://huggingface.co/Lucebox/Qwen3.8-27B-IQ4_XS-fast-GGUF) (any Qwen3.5 / Qwen3.8 dense GGUF works) | [`Qwen3.8-27B-mmproj-Q8_0.gguf`](https://huggingface.co/Lucebox/Qwen3.8-27B-IQ4_XS-fast-GGUF), or any published `qwen3vl_merger` mmproj | one GPU, any backend |
| DeepSeek V4 Flash Vision (DS4V) | [`DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf`](https://huggingface.co/Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3) | [`DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf`](https://huggingface.co/Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3) | HIP: a Strix Halo alone, or R9700 + Strix Halo |

**Status: experimental.** Both models answer image questions correctly end to
end; see each model's verification notes for what has and has not been
measured.

## Quick start

Build the server as in the [README](../README.md#run-the-server). DS4V also
needs hipBLASLt at build time (`hipblaslt-dev` on ROCm; CMake prints
`hipBLASLt found: building the DS4V vision ops`).

### Qwen3.8-27B on one GPU (R9700)

```bash
hf download Lucebox/Qwen3.8-27B-IQ4_XS-fast-GGUF \
Qwen3.8-27B-IQ4_XS-pure.gguf Qwen3.8-27B-mmproj-Q8_0.gguf --local-dir models
hf download Lucebox/Qwen3.8-27B-DFlash2-GGUF \
Qwen3.8-27B-DFlash2-Q8_0.gguf --local-dir models

./server/build-hip/luce_server models/Qwen3.8-27B-IQ4_XS-pure.gguf \
--target-device hip:0 \
--draft models/Qwen3.8-27B-DFlash2-Q8_0.gguf --draft-device hip:0 \
--draft-block-size 16 --max-ctx 32768 \
--cache-type-k q8_0 --cache-type-v q8_0 \
--mmproj models/Qwen3.8-27B-mmproj-Q8_0.gguf \
--port 8216
```

About 21 GiB of VRAM at the peak of an image request. Text requests keep the
DFlash2 drafter; image requests decode without it.

### DeepSeek V4 Flash Vision on a Strix Halo

```bash
hf download Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3 \
DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf \
DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf --local-dir models
hf download Lucebox/DeepSeek-V4-Flash-0731-DSpark-GGUF \
DeepSeek-V4-Flash-0731-DSpark-draft-Q4RMFP4-denseF16.gguf --local-dir models

LUCE_DS4_SPEC=1 \
LUCE_DS4_DRAFT=models/DeepSeek-V4-Flash-0731-DSpark-draft-Q4RMFP4-denseF16.gguf \
LUCE_DS4_SPARSE_DECODE_FLASH=1 \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This quick start presents LUCE_DS4_SPARSE_DECODE_FLASH=1 as a plain part of the launch, but the rest of the repo documents it as an experimental single-HIP opt-in that can change generated tokens (ENVIRONMENT.md, DS4.md, RECOMMENDED_SETUPS.md all say so). Users copying the canonical snippet will silently enable a non-default, output-changing verifier path, which conflicts with the 'published launch plus --mmproj' framing. Add the same experimental/single-HIP/may-change-tokens comment used in RECOMMENDED_SETUPS.md.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/image-input.md, line 53:

<comment>This quick start presents `LUCE_DS4_SPARSE_DECODE_FLASH=1` as a plain part of the launch, but the rest of the repo documents it as an experimental single-HIP opt-in that can change generated tokens (ENVIRONMENT.md, DS4.md, RECOMMENDED_SETUPS.md all say so). Users copying the canonical snippet will silently enable a non-default, output-changing verifier path, which conflicts with the 'published launch plus --mmproj' framing. Add the same experimental/single-HIP/may-change-tokens comment used in RECOMMENDED_SETUPS.md.</comment>

<file context>
@@ -4,15 +4,84 @@ The server accepts JPEG and PNG images through OpenAI chat completions when a
+
+LUCE_DS4_SPEC=1 \
+LUCE_DS4_DRAFT=models/DeepSeek-V4-Flash-0731-DSpark-draft-Q4RMFP4-denseF16.gguf \
+LUCE_DS4_SPARSE_DECODE_FLASH=1 \
+./server/build-hip/luce_server models/DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf \
+  --target-device hip:0 --max-ctx 131072 --chunk 8192 \
</file context>
Suggested change
LUCE_DS4_SPARSE_DECODE_FLASH=1 \
# LUCE_DS4_SPARSE_DECODE_FLASH=1 stays an explicit experimental opt-in
# (single HIP target; may change generated tokens — see DS4.md).
LUCE_DS4_SPARSE_DECODE_FLASH=1 \

./server/build-hip/luce_server models/DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf \
--target-device hip:0 --max-ctx 131072 --chunk 8192 \
--cache-type-k q4_0 --cache-type-v q4_0 \
--ds4-fused-decode --ds4-fused-verify-f16-kv \
--ds4-expert-top-k 6 --ds4-prefill sparse \
--mmproj models/DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf \
--port 8216
```

`hip:0` must be the Strix Halo; on a host with a discrete GPU too, expose the
Strix Halo alone with `HIP_VISIBLE_DEVICES`. This is the text model's published
launch plus `--mmproj`: the Vision file replaces
`DeepSeek-V4-Flash-0731-ROCMFPX-MIX-STRIX.gguf` for text as well and decodes
at least as fast (numbers below). For R9700 + Strix Halo see [DS4V](#ds4v) below.

### Send an image

```bash
IMG=$(base64 < photo.png | tr -d '\n')
curl -s http://127.0.0.1:8216/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":[
{"type":"text","text":"What does this chart show?"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,'"$IMG"'"}}]}],
"max_tokens":256}'
```

`GET /props` reports `capabilities.image_input_supported: true` once the
projector has loaded. In the Docker images, set `LUCE_MMPROJ` to the projector
path inside the container.

## Request contract

Use `POST /v1/chat/completions` with user-message content parts in display order:

```json
{
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image."},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
]
}],
"max_tokens": 128
}
```

Only base64 JPEG/PNG data URLs are supported. Remote URLs, images outside user
content arrays, and image parts through other API formats are rejected. A
request carries at most four images, 16 MiB encoded each and 32 MiB combined.
Decoder pixel and aspect limits also apply. A model's image marker cannot be supplied
as ordinary text.

The server expands image markers after final rendering and tokenization, and
the expanded image tokens count toward context and usage. Image requests use
plain autoregressive decoding and bypass the token-keyed prefix, disk and
agent-turn caches and prompt compression: tokens alone do not identify an
image. Text requests on the same server keep speculative decoding and caching.

Layer or tensor splitting across GPUs, remote target shards, concurrent
sequence scheduling (`--max-concurrency`) and upstream forwarding do not
support images. `/props` reports the effective capability in
`capabilities.image_input_supported` after backend initialization.

## Qwen3.5 / Qwen3.8

The launch is in the [quick start](#qwen38-27b-on-one-gpu-r9700).

The projector is read directly from the published `clip` file, BF16, F16 or
Q8_0; Q8_0 is recommended (below). Projectors with
deepstack branches (Qwen3-VL) are refused. An image is resized the way the
model was trained (bicubic, both sides to a multiple of 32 pixels) and costs
one token per 32x32 pixels, between 64 and 1,024 tokens; larger images are
scaled down to the cap.

Image tokens take two-dimensional rotary positions, so positions run behind
token counts after an image. Prefill handles that in its normal chunk loop;
decoding carries the offset for the rest of the request.

### Verification status

Covered by `test_qwen35_image`: target sizes against the model's reference
resize rule, the tower's patch order and position table sampling, marker
expansion, rotary positions, and image rows that straddle prefill chunks.

Measured on an R9700 alone with the DFlash2 drafter, thinking off. With the
Lucebox `Qwen3.8-27B-IQ4_XS-pure` file and a Q8_0 projector:

- 220 seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with
lmms-eval prompts: AI2D 90/100, ChartQA relaxed accuracy 56/60 (augmented)
and 42/60 (human). Image prompts prefill in 0.56 s on average; one to four
images per request all answer correctly (four images, 2,495 tokens: 3.2 s).
- Text decodes at 56 to 117 tok/s on 256-token answers (84 on average); image
requests decode without the drafter at about 36 tok/s.

With unsloth's UD-IQ4_XS file and the published BF16 projector:

- 220 seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with
lmms-eval prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented)
and 43/60 (human), no errors. Image prompts average 448 tokens and prefill in
0.71 s (largest 1,068 tokens, 1.8 s); decode runs at 31 to 35 tok/s.
- A projector with its weight matrices in Q8_0 (rows that are not a multiple
of 32 stay F16) encodes a 975-token image in 443 ms instead of 677 ms with
the BF16 file, with the same scores on the 220 questions and 216 identical
answers. Prefer one when available.
- llama.cpp (HIP build, `-fa on`, same GGUF, projector and image cap) answers
the same on every test image; on a 1,012-token image prompt it prefills in
1.65 s to our 1.68 s, on a 323-token one in 0.61 s to our 0.50 s.
- An image inside a 6,271-token prompt, two images in one request, and a
follow-up turn after an image all answer correctly.
- Text requests are byte-identical to a build without image support, at the
same speed, with or without a projector loaded (five prompts up to 19.6K
tokens). The projector adds 0.9 GiB of VRAM; the peak during image requests
was 21.6 GiB against 20.8 GiB for text.
- The same requests answer correctly on a Strix Halo alone, where a
1,012-token image prompt prefills in 4.6 s and decodes at 14 tok/s.

Not yet established: a comparison against the reference implementation on the
same questions, and CUDA. The tower uses only standard ggml
operators, so nothing in it is HIP specific.

## DS4V

The server must be built with hipBLASLt available (the `hipblaslt-dev` package
on ROCm). CMake reports `hipBLASLt found: building the DS4V vision ops`; a build
without it refuses `--mmproj` for this model at startup.

Image input needs Linux HIP, a DeepSeek4 decoder whose GGUF carries the image
router biases, `--ds4-prefill sparse`, and `--mmproj` pointing at the published projector (or one [exported with our
tool](ds4v-mmproj.md)). Two layouts work:

- **One GPU holding the whole model** (for example a Strix Halo): nothing else
to set. The projector is loaded after the weights and must fit beside them.
- **Two GPUs splitting the experts in process** (for example R9700 + Strix
Halo): `LUCE_DS4_MOE_TP=1`, `LUCE_DS4_MOE_TP_INPROC=1`, and
`LUCE_DS4_MOE_TP_GPU` selecting the second device, with `--target-device`
on the first. Device ordinals must match the host's actual topology.

Remote expert IPC, all-on-secondary placement, experts kept on the CPU and
dense prefill do not support images.

Published llama.cpp conversions of the decoder load directly (image router
bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split
GGUF files and llama.cpp's `clip` projector files are not read yet.

A decoder in our own ROCMFP MIX format comes from `tools/ds4_mix_converter`
run on the Vision-Exp checkpoint. It follows the shipped DeepSeek-V4-Flash
recipe: routed gate and up experts in fp2, down experts in fp2 on the shipped
layer set and fp3 elsewhere, dense projections in ROCmFP4, the token embedding
in Q6_K, codebooks embedded in the GGUF (one file, about 100 GB). It keeps the
image router biases. Pass `--imatrix` with an importance matrix (llama.cpp's
per-expert layout is used expert by expert; the community publishes one for
this model) or `--absmax-only`. The converter uses every core: about 40 minutes
for this checkpoint on 32 cores.

One image request may be outstanding per backend. Its admission lease remains
with the immutable payload through queueing and generation; another image
request is rejected until that payload is released. This bounds simultaneous
preprocessing and prepared-image memory. Text requests retain the normal queue.

The server expands image markers after final rendering and tokenization.
Expanded image tokens count toward context and usage. Image blocks remain
whole during prefill, image rows use their learned routing bias, and raw
attention is bidirectional within each image's visible span. The projector's
tile permutation is applied once when assembling rows with named sentinel
embeddings. All chunks are capped at 1,024 tokens while a projector is loaded.

The image payload survives request copies and retry paths. Failed or cancelled
multi-image encoding publishes no partial embedding matrices.

### Memory

The projector is validated and loaded before expert placement. Admission counts
actual selected owner tensor sizes, allocation alignment, MIX tables, copy
staging, future KV, and explicit execution reserves. Host and integrated-device
charges share one physical-memory budget. Before image decoding, the server
checks host availability; before encoding, it synchronizes and releases
disposable decoder, owner, and draft graphs and checks live device/host
availability again. KV, saved snapshots, and draft weights remain reflected in
that live measurement. Reservations are conservative policy, not a guarantee
against unrelated concurrent allocations.

### Verification status

Covered by unit tests in the main build: image transport and request policy,
prompt expansion and ownership, embedding assembly and cancellation, image
spans and the expert budget, plus the decoder loader and image-batch admission
tests in `test_deepseek4_unit`.

Measured with the public `DeepSeek-V4-Flash-Vision-Exp` Q2_K_S decoder and
the exported projector, on a Strix Halo alone and on R9700 + Strix Halo, 220
seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with lmms-eval
prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) and 43/60
(human). Both layouts score the same and give word-identical answers on 213 of
220 questions. An image request prefills in about 4 s and decodes at about
23 tok/s.

With our own ROCMFP MIX conversion of the same checkpoint (per-expert
importance matrix, the shipped recipe above), on a Strix Halo alone at top-k 6:

- Against the MXFP4 reference (native FP4 experts) on 8,176 wikitext-2 tokens:
KL 0.464 mean, 0.102 median, top-1 agreement 78.4%, perplexity 4.14 against
2.82. The community Q2_K_S scores KL 0.511 in our engine (0.523 in
llama.cpp) and perplexity 4.22.
- AI2D 84/100, ChartQA 54/60 and 40/60 (the Q2_K_S: 85, 55, 43); the sanity
and one-to-four-image sets are all correct.
- With the published DSpark drafter and fused decode and verify, text decodes
at 25 to 37 tok/s on 256-token answers (30 mean), as fast as the shipped
text model; image requests decode without the drafter at about 22 tok/s.

Not yet established:

- The vision tower misses the fixed 0.9995 feature-cosine gate against the
reference implementation: 0.99906 on the Radeon RX 7900 XT it was developed
on, 0.99823 on CPU. Embeddings pass; features do not.
- No comparison against the reference implementation on the same questions.

## Code layout

Shared by every model:

| Piece | Where |
| --- | --- |
| Reading images out of a request, limits, redaction | `server/src/server/image_input.*` |
| JPEG and PNG decoding to RGB | `server/src/common/vision/image_decode.*`, codecs in `server/cmake/ImageCodecs.cmake` |
| Bicubic resizing that matches Pillow byte for byte | `server/src/common/vision/image_resize.*` |
| Reading a published `clip`-format projector file | `server/src/common/vision/mmproj_file.*` |
| Image positions in a prompt, batches that keep an image whole | `server/src/common/vision/image_spans.h` |
| The backend contract | `supports_images`, `image_placeholder`, `prepare_images` in `server/src/common/model_backend.h`; `GenerateRequest::images` in `server/src/common/generation_types.h` |

DS4V only, all under `server/src/deepseek4/`: resizing and patching
(`deepseek4_vision_preprocess`), the vision tower (`deepseek4_vision`), marker
expansion and embedding assembly (`deepseek4_image_prompt`,
`deepseek4_image_assembly`), attention visibility and expert routing for image
rows (`deepseek4_image_policy`), and memory admission
(`deepseek4_image_admission`).

Qwen3.5 / Qwen3.8 only, all under `server/src/qwen35/`: the vision tower and
its preprocessing (`qwen35_vision`), marker expansion and rotary positions
(`qwen35_image_prompt`), what a request carries (`qwen35_image_request.h`), and
the backend's three contract methods (`qwen35_backend_images.cpp`). Prefill and
decode changes are a few lines in `qwen35_backend.cpp`.

Another model needs its own preprocessing, tower and prompt expansion, and its
backend implements the three contract methods. Nothing in the HTTP server or in
`common/vision` names a model.
Loading
Loading