Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.png filter=lfs diff=lfs merge=lfs -text
assets/banner.png -filter -diff -merge -text
assets/docker.png -filter -diff -merge -text
server/docs/images/vision-mmproj-meme-example.png -filter -diff -merge -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
Expand Down
41 changes: 41 additions & 0 deletions server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,23 @@ endif()
# Use only the ggml subtree of llama.cpp (skip libllama).
add_subdirectory(deps/llama.cpp/ggml EXCLUDE_FROM_ALL)

# Optional native mmproj vision (mtmd + llama for tokenization/vocab).
option(DFLASH27B_MMPROJ "Build native mmproj vision support via mtmd" OFF)
if(DFLASH27B_MMPROJ)
set(LLAMA_BUILD_COMMON OFF CACHE BOOL "" FORCE)
set(LLAMA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(LLAMA_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(LLAMA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(LLAMA_BUILD_SERVER OFF CACHE BOOL "" FORCE)
add_subdirectory(deps/llama.cpp EXCLUDE_FROM_ALL)

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.

P1: Enabling DFLASH27B_MMPROJ cannot configure from this checkout because the referenced top-level llama.cpp project and tools/mtmd are not vendored. Adding the full llama.cpp/mtmd sources (or pointing these calls at a real supplied source tree) is needed before exposing this option.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/CMakeLists.txt, line 214:

<comment>Enabling `DFLASH27B_MMPROJ` cannot configure from this checkout because the referenced top-level llama.cpp project and `tools/mtmd` are not vendored. Adding the full llama.cpp/mtmd sources (or pointing these calls at a real supplied source tree) is needed before exposing this option.</comment>

<file context>
@@ -203,6 +203,23 @@ endif()
+    set(LLAMA_BUILD_TOOLS     OFF CACHE BOOL "" FORCE)
+    set(LLAMA_BUILD_EXAMPLES  OFF CACHE BOOL "" FORCE)
+    set(LLAMA_BUILD_SERVER    OFF CACHE BOOL "" FORCE)
+    add_subdirectory(deps/llama.cpp EXCLUDE_FROM_ALL)
+    # mtmd is added as a sibling of llama.cpp (not via tools/) so
+    # LLAMA_INSTALL_VERSION must be visible in the parent scope.
</file context>

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.

P2: When DFLASH27B_MMPROJ=ON, this adds the full deps/llama.cpp tree as a CMake subdirectory, but the same source is already added a few lines above as add_subdirectory(deps/llama.cpp/ggml EXCLUDE_FROM_ALL). The vendored llama.cpp root CMakeLists normally calls add_subdirectory(ggml) unconditionally, so with this option enabled the ggml directory would be configured twice under the same source/binary pair — a CMake hard error at configure time or, if it passes, duplicate ggml/ggml-* target/symbol definitions when dflash_server links the standalone ggml ${DFLASH27B_GGML_BACKEND_TARGET} alongside the llama/mtmd that bundle their own ggml. This is an opt-in build path, but it would make the documented -DDFLASH27B_MMPROJ=ON + server build fail rather than being a no-op. Worth confirming the exact llama.cpp snapshot handles this (e.g. gating the standalone ggml add, or renaming targets) before relying on the demo build.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/CMakeLists.txt, line 214:

<comment>When `DFLASH27B_MMPROJ=ON`, this adds the full `deps/llama.cpp` tree as a CMake subdirectory, but the same source is already added a few lines above as `add_subdirectory(deps/llama.cpp/ggml EXCLUDE_FROM_ALL)`. The vendored llama.cpp root CMakeLists normally calls `add_subdirectory(ggml)` unconditionally, so with this option enabled the ggml directory would be configured twice under the same source/binary pair — a CMake hard error at configure time or, if it passes, duplicate `ggml`/`ggml-*` target/symbol definitions when `dflash_server` links the standalone `ggml ${DFLASH27B_GGML_BACKEND_TARGET}` alongside the `llama`/`mtmd` that bundle their own ggml. This is an opt-in build path, but it would make the documented `-DDFLASH27B_MMPROJ=ON` + server build fail rather than being a no-op. Worth confirming the exact llama.cpp snapshot handles this (e.g. gating the standalone ggml add, or renaming targets) before relying on the demo build.</comment>

<file context>
@@ -203,6 +203,23 @@ endif()
+    set(LLAMA_BUILD_TOOLS     OFF CACHE BOOL "" FORCE)
+    set(LLAMA_BUILD_EXAMPLES  OFF CACHE BOOL "" FORCE)
+    set(LLAMA_BUILD_SERVER    OFF CACHE BOOL "" FORCE)
+    add_subdirectory(deps/llama.cpp EXCLUDE_FROM_ALL)
+    # mtmd is added as a sibling of llama.cpp (not via tools/) so
+    # LLAMA_INSTALL_VERSION must be visible in the parent scope.
</file context>

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.

P1: The advertised mmproj build cannot configure because the vendored deps/llama.cpp tree has neither the top-level CMake project nor the tools/mtmd subtree referenced here. Vendor the full llama.cpp/mtmd sources or point these calls at a checked-in mtmd integration before enabling this option.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/CMakeLists.txt, line 214:

<comment>The advertised mmproj build cannot configure because the vendored `deps/llama.cpp` tree has neither the top-level CMake project nor the `tools/mtmd` subtree referenced here. Vendor the full llama.cpp/mtmd sources or point these calls at a checked-in mtmd integration before enabling this option.</comment>

<file context>
@@ -203,6 +203,23 @@ endif()
+    set(LLAMA_BUILD_TOOLS     OFF CACHE BOOL "" FORCE)
+    set(LLAMA_BUILD_EXAMPLES  OFF CACHE BOOL "" FORCE)
+    set(LLAMA_BUILD_SERVER    OFF CACHE BOOL "" FORCE)
+    add_subdirectory(deps/llama.cpp EXCLUDE_FROM_ALL)
+    # mtmd is added as a sibling of llama.cpp (not via tools/) so
+    # LLAMA_INSTALL_VERSION must be visible in the parent scope.
</file context>

# mtmd is added as a sibling of llama.cpp (not via tools/) so
# LLAMA_INSTALL_VERSION must be visible in the parent scope.
if(NOT DEFINED LLAMA_INSTALL_VERSION)
set(LLAMA_INSTALL_VERSION "0.0.0")
endif()
add_subdirectory(deps/llama.cpp/tools/mtmd EXCLUDE_FROM_ALL)
endif()

if(DFLASH27B_GPU_BACKEND STREQUAL "hip")
# The vendored ggml HIP shim still uses a few CUDA spellings that are not
# mapped in this upstream snapshot. Keep the compatibility layer in this
Expand Down Expand Up @@ -238,6 +255,7 @@ set(DFLASH27B_SRC_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/src/gemma4
${CMAKE_CURRENT_SOURCE_DIR}/src/deepseek4
${CMAKE_CURRENT_SOURCE_DIR}/src/server
${CMAKE_CURRENT_SOURCE_DIR}/src/vision
)

add_library(dflash_common STATIC
Expand Down Expand Up @@ -319,6 +337,7 @@ add_library(dflash_common STATIC
src/qwen35/qwen35_backend.cpp
src/qwen35/qwen35_tensor_parallel.cpp
src/qwen35/qwen35_layer_split_adapter.cpp
src/qwen35/qwen35_layer_split_vision.cpp
src/qwen35/qwen35_dflash_target.cpp
src/qwen35/qwen35_layer_split_dflash_target.cpp
src/qwen35/layer_split_daemon_loop.cpp
Expand All @@ -344,6 +363,7 @@ add_library(dflash_common STATIC
src/server/prefix_cache.cpp
src/server/disk_prefix_cache.cpp
src/server/freeze_history.cpp
src/vision/vision_input.cpp
# ── Jinja chat-template engine (vendored under deps/llama.cpp/common/) ──
# Used by render_chat_template_jinja() to support --chat-template-file
# in dflash_server. Mirrors llama.cpp's common_chat_template plumbing.
Expand All @@ -357,6 +377,9 @@ add_library(dflash_common STATIC
deps/llama.cpp/common/jinja/caps.cpp
deps/llama.cpp/common/unicode.cpp
)
if(DFLASH27B_MMPROJ)
target_sources(dflash_common PRIVATE src/vision/vision_encoder.cpp)
endif()
# BSA (Block-Sparse Attention) backs the speculative-prefill drafter scoring
# path. Default ON so prefill is fast out of the box. Turn OFF if you don't
# run the spec-prefill stack or are building without CUDA BF16 WMMA support
Expand Down Expand Up @@ -579,6 +602,16 @@ if(DFLASH27B_ENABLE_BSA)
endif()
endif()

if(DFLASH27B_MMPROJ)
target_compile_definitions(dflash_common PRIVATE DFLASH_HAVE_MMPROJ=1)
set(DFLASH27B_MMPROJ_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/include
${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/tools/mtmd
${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/vendor)
target_include_directories(dflash_common PRIVATE ${DFLASH27B_MMPROJ_INCLUDE_DIRS})
target_link_libraries(dflash_common PRIVATE llama mtmd)
endif()

target_include_directories(dflash_common
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
Expand Down Expand Up @@ -1133,6 +1166,10 @@ if(DFLASH27B_TESTS)
else()
target_link_libraries(test_server_unit PRIVATE hip::host)
endif()
if(DFLASH27B_MMPROJ)
target_compile_definitions(test_server_unit PRIVATE DFLASH_HAVE_MMPROJ=1)
target_include_directories(test_server_unit PRIVATE ${DFLASH27B_MMPROJ_INCLUDE_DIRS})
endif()
if(CMAKE_CROSSCOMPILING)
# Runtime discovery cannot execute a target binary on the build
# host. Register one aggregate test instead; CMake prepends the
Expand Down Expand Up @@ -1312,6 +1349,10 @@ if(DFLASH27B_SERVER)
DFLASH27B_BACKEND_CUDA=1
DFLASH27B_CUDA_MIN_SM=${_dflash_cuda_min_sm})
endif()
if(DFLASH27B_MMPROJ)
target_compile_definitions(dflash_server PRIVATE DFLASH_HAVE_MMPROJ=1)
target_include_directories(dflash_server PRIVATE ${DFLASH27B_MMPROJ_INCLUDE_DIRS})
endif()
if(NOT WIN32)
target_link_libraries(dflash_server PRIVATE dflash_common ggml ${DFLASH27B_GGML_BACKEND_TARGET} pthread)
else()
Expand Down
108 changes: 108 additions & 0 deletions server/docs/VISION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Native mmproj vision

Optional build (`-DDFLASH27B_MMPROJ=ON`) wires llama.cpp **mtmd** so
`dflash_server` can load a GGUF multimodal projector alongside the text model
and accept OpenAI-style `image_url` content in chat completions.

## Quick start

**Need:** CUDA GPU, a Qwen3.5/3.6 GGUF + matching `mmproj-F16.gguf`, and a
**full** [`lucebox-ggml`](https://github.com/Luce-Org/lucebox-ggml) tree.
Hub only vendors the ggml subset — `tools/mtmd` is not in-tree — so a stock
configure with `-DDFLASH27B_MMPROJ=ON` will fail until mtmd sources are present.

```bash
# 1) Checkout this PR
git fetch origin pull/571/head:pr-571 && git checkout pr-571

# 2) Supply full llama.cpp (mtmd) for the build
cd server/deps
mv llama.cpp llama.cpp.vendored-ggml-only
git clone --depth 1 -b luce-dflash https://github.com/Luce-Org/lucebox-ggml.git llama.cpp
# Keep hub-local ggml patches (e.g. rocmfp4) on top of the full tree
cp -a llama.cpp.vendored-ggml-only/ggml/. llama.cpp/ggml/

# 3) Build with vision
cd ..
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CUDA_ARCHITECTURES=<your_sm> \
-DDFLASH27B_MMPROJ=ON -DDFLASH27B_SERVER=ON
cmake --build build --target dflash_server -j"$(nproc)"

# 4) Run (same flags you already use for Qwen35, plus mmproj)
./build/dflash_server \
--model /path/to/Qwen….gguf \
--mmproj /path/to/mmproj-F16.gguf \
# …draft / layer-split / port as usual…

# Container equivalent:
# DFLASH_MMPROJ=/path/to/mmproj-F16.gguf
# Optional: --no-mmproj-offload / DFLASH_MMPROJ_NO_OFFLOAD=1
```

After the build you can restore the slim vendor so the tree stays pullable:

```bash
cd server/deps
rm -rf llama.cpp
mv llama.cpp.vendored-ggml-only llama.cpp
```

### Smoke

```bash
# Capability flag
curl -s localhost:8080/props | jq '.capabilities.vision_supported'
# expect: true

# Multimodal chat (data URI only today)
IMG_B64=$(base64 -w0 /path/to/test.jpg) # macOS: base64 -i test.jpg
curl -s localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d "{
\"model\": \"qwen\",
\"messages\": [{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"What do you see?\"},
{\"type\": \"image_url\", \"image_url\": {
\"url\": \"data:image/jpeg;base64,${IMG_B64}\"
}}
]
}],
\"max_tokens\": 128
}"
```

Also check a plain text turn still works (and still uses DFlash when a draft is
configured). Without `--mmproj`, image requests should 400 cleanly.

## Runtime

Multimodal turns run AR decode; text-only turns keep DFlash speculative decode
when configured. `/props` reports `capabilities.vision_supported: true` when
the projector is loaded.

Supported on both monolithic Qwen35 and layer-split backends
(`supports_multimodal()` is delegated through `LayerSplitBackend`).

## Example

Chat completion with an attached meme image — the model reads the visual
layout and answers in natural language:

![Native mmproj vision: meme interpretation](images/vision-mmproj-meme-example.png)

Request shape (abbreviated):

```json
{
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What do you think this image means?"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
]
}]
}
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions server/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ fi
# the KV footprint. Only emitted to the server CLI when nonzero so
# unset reproduces the server's own default unchanged.
: "${DFLASH_FA_WINDOW:=0}"
# Native mmproj vision projector (Qwen3-VL etc.). When set, passed as --mmproj.
: "${DFLASH_MMPROJ:=}"

# ── auto-detect target ─────────────────────────────────────────────────────
# Target .gguf is typically 10-30 GB (Q4_K_M). Drafts are 1-2 GB (Q8_0 / Q4)
Expand Down Expand Up @@ -499,6 +501,8 @@ CMD=("$DFLASH_SERVER_BIN" "$DFLASH_TARGET"

[ -n "$DRAFT_ARG" ] && CMD+=(--draft "$DRAFT_ARG")
[ -n "$DRAFT_ARG" ] && CMD+=(--ddtree --ddtree-budget "$DFLASH_BUDGET")
[ -n "$DFLASH_MMPROJ" ] && CMD+=(--mmproj "$DFLASH_MMPROJ")
[ "${DFLASH_MMPROJ_NO_OFFLOAD:-0}" = "1" ] && [ -n "$DFLASH_MMPROJ" ] && CMD+=(--no-mmproj-offload)
[ -n "$DFLASH_DEFAULT_MAX_TOKENS" ] && CMD+=(--default-max-tokens "$DFLASH_DEFAULT_MAX_TOKENS")
[ -n "$DFLASH_MODEL_NAME" ] && CMD+=(--model-name "$DFLASH_MODEL_NAME")
# `--lazy-draft` is silently dropped by the C++ server unless both
Expand Down
20 changes: 20 additions & 0 deletions server/src/common/attn_masks.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,24 @@ inline void build_tree_mask(const DDTree & tree, int past_length,
}
}

// Bidirectional mask for vision image chunks (full attention within chunk +
// to all prior KV). Matches Qwen35Backend::build_bidirectional_mask.
inline void build_bidirectional_mask(std::vector<uint16_t> & out,
int kv_len, int n_tokens, int kv_pos,
int kq_stride_pad,
int kv_pad_override = 0) {
const int kv_pad = (kv_pad_override > 0) ? kv_pad_override
: align_up(kv_len, kq_stride_pad);
const int q_pad = align_up(n_tokens, KQ_MASK_PAD);
out.assign((size_t)kv_pad * q_pad, F16_NEG_INF);
for (int q = 0; q < n_tokens; q++) {
for (int k = 0; k < kv_pos; k++) {

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.

P1: Layer-split multimodal prefill can attend to padded KV rows when --fa-window is combined with a padded FA stride. Building this mask relative to win_start and the valid window span, or disabling windowed multimodal prefill, avoids unmasking invalid cache rows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/common/attn_masks.h, line 89:

<comment>Layer-split multimodal prefill can attend to padded KV rows when `--fa-window` is combined with a padded FA stride. Building this mask relative to `win_start` and the valid window span, or disabling windowed multimodal prefill, avoids unmasking invalid cache rows.</comment>

<file context>
@@ -75,4 +75,24 @@ inline void build_tree_mask(const DDTree & tree, int past_length,
+    const int q_pad  = align_up(n_tokens, KQ_MASK_PAD);
+    out.assign((size_t)kv_pad * q_pad, F16_NEG_INF);
+    for (int q = 0; q < n_tokens; q++) {
+        for (int k = 0; k < kv_pos; k++) {
+            out[(size_t)q * kv_pad + k] = F16_ZERO;
+        }
</file context>

out[(size_t)q * kv_pad + k] = F16_ZERO;
}
for (int k = kv_pos; k < kv_pos + n_tokens; k++) {

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.

P1: Multimodal image chunks lose bidirectional attention when layer-split prefill subdivides them: queries in an earlier sub-batch cannot attend to image tokens in later sub-batches. Process a non-causal image chunk as one batch (or otherwise make all chunk K/V available before applying this mask) instead of limiting the visible range to the current n_tokens.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/common/attn_masks.h, line 92:

<comment>Multimodal image chunks lose bidirectional attention when layer-split prefill subdivides them: queries in an earlier sub-batch cannot attend to image tokens in later sub-batches. Process a non-causal image chunk as one batch (or otherwise make all chunk K/V available before applying this mask) instead of limiting the visible range to the current `n_tokens`.</comment>

<file context>
@@ -75,4 +75,24 @@ inline void build_tree_mask(const DDTree & tree, int past_length,
+        for (int k = 0; k < kv_pos; k++) {
+            out[(size_t)q * kv_pad + k] = F16_ZERO;
+        }
+        for (int k = kv_pos; k < kv_pos + n_tokens; k++) {
+            out[(size_t)q * kv_pad + k] = F16_ZERO;
+        }
</file context>

out[(size_t)q * kv_pad + k] = F16_ZERO;
}
}
}

} // namespace dflash::common
4 changes: 4 additions & 0 deletions server/src/common/backend_args.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ struct BackendArgs {
// Optional: speculative decode draft model (qwen35 only)
const char * draft_path = nullptr;

// Native mmproj vision (qwen35)
const char * mmproj_path = nullptr;
bool mmproj_use_gpu = true;

// Device placement
DevicePlacement device;
DevicePlacement draft_device;
Expand Down
7 changes: 7 additions & 0 deletions server/src/common/backend_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ std::unique_ptr<ModelBackend> create_backend(
? std::max<int>(DFLASH27B_DRAFT_BLOCK_SIZE, args.ddtree_budget + 1)
: DFLASH27B_DRAFT_BLOCK_SIZE;
cfg.run_dflash = args.draft_path != nullptr;
cfg.mmproj_path = args.mmproj_path;
cfg.mmproj_use_gpu = args.mmproj_use_gpu;
if (const char * mt = std::getenv("DFLASH_MMPROJ_THREADS")) {
cfg.mmproj_threads = std::max(1, std::atoi(mt));
}
Comment on lines +247 to +249

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: Layer-split projector loading treats empty, malformed, or non-positive DFLASH_MMPROJ_THREADS as one thread, while monolithic Qwen35 uses the default of four; this can unnecessarily reduce image-encoding throughput on split deployments. Apply the same validated positive-value/default-four parsing in both paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/common/backend_factory.cpp, line 247:

<comment>Layer-split projector loading treats empty, malformed, or non-positive `DFLASH_MMPROJ_THREADS` as one thread, while monolithic Qwen35 uses the default of four; this can unnecessarily reduce image-encoding throughput on split deployments. Apply the same validated positive-value/default-four parsing in both paths.</comment>

<file context>
@@ -242,6 +242,11 @@ std::unique_ptr<ModelBackend> create_backend(
             cfg.run_dflash         = args.draft_path != nullptr;
+            cfg.mmproj_path        = args.mmproj_path;
+            cfg.mmproj_use_gpu     = args.mmproj_use_gpu;
+            if (const char * mt = std::getenv("DFLASH_MMPROJ_THREADS")) {
+                cfg.mmproj_threads = std::max(1, std::atoi(mt));
+            }
</file context>
Suggested change
if (const char * mt = std::getenv("DFLASH_MMPROJ_THREADS")) {
cfg.mmproj_threads = std::max(1, std::atoi(mt));
}
if (const char * mt = std::getenv("DFLASH_MMPROJ_THREADS");
mt && *mt) {
char * end = nullptr;
const long parsed = std::strtol(mt, &end, 10);
if (end != mt && *end == '\0' && parsed > 0) {
cfg.mmproj_threads = static_cast<int>(parsed);
}
}


auto adapter = std::make_unique<Qwen35LayerSplitAdapter>(cfg);
auto backend = std::make_unique<LayerSplitBackend>(std::move(adapter));
Expand Down Expand Up @@ -270,6 +275,8 @@ std::unique_ptr<ModelBackend> create_backend(
cfg.ddtree_temp = args.ddtree_temp;
cfg.ddtree_chain_seed = args.ddtree_chain_seed;
cfg.use_feature_mirror = args.use_feature_mirror;
cfg.mmproj_path = args.mmproj_path;
cfg.mmproj_use_gpu = args.mmproj_use_gpu;

auto backend = std::make_unique<Qwen35Backend>(cfg);
if (!backend->init()) {
Expand Down
Loading