qwen36-vision-tower
The vision tower of Qwen3.6-35B-A3B, extracted unmodified from the base checkpoint and
packaged so it can be grafted onto sprappcom/malay35b-pqm
(or any other Qwen3.6-35B-A3B-derived .pqm) through the prism-engine vision bridge.
| Source | Qwen/Qwen3.6-35B-A3B (Apache-2.0), model.visual.* tensors only |
| Parameters | 446.6 M |
| Precision | BF16 |
| File | vision_tower.safetensors β 893,175,200 bytes, sha256 160afeed128baaa9918c8c87da4d6d8892c179eba8d0079b2f1697ddfc42773b |
| Architecture | Qwen3-VL style ViT: depth 27, hidden 1152, 16 heads, patch 16, spatial merge 2, projected to out_hidden_size=2048 (the text model's n_embd) |
| Output | [num_image_tokens, 2048] embeddings that prism-engine splices into the prompt in place of `< |
What is in this repo
| File | Purpose |
|---|---|
vision_tower.safetensors |
The weights (strict-loaded, no missing/unexpected keys) |
config.json |
Minimal config: only the vision_config block + the vision token ids the bridge/engine read. Deliberately not a full text-model config |
preprocessor_config.json |
Qwen2VLImageProcessor settings (patch 16, merge 2, mean/std 0.5) |
server.py |
The vision bridge server (Python, Unix-socket, length-prefixed protocol). Includes the 1024 px longest-side resize cap |
requirements.txt |
Exact pinned Python deps that were verified working |
LICENSE |
Proprietary terms (same as the .pqm repos) + Apache-2.0 attribution for the base weights |
Requirements
- prism-engine
prism_serverbuilt with vision-bridge wiring (PRISMX_VISION_BRIDGE_SOCKsupport). prism-engine is proprietary and is not included here β contact sprappcom@gmail.com. - A Qwen3.6-35B-A3B-derived text model in
.pqmform, e.g.sprappcom/malay35b-pqm(malay35b.pqm+ itsmalay35b.toktokenizer sidecar). - Python 3.10+ with the pinned deps below (verified on Python 3.12.3). CUDA optional (CPU mode works, see footprints).
- The Python venv must live on a filesystem that allows executing shared libraries
(torch ships
.sofiles) β not on anoexectmpfs such as/dev/shmon many containers.
Run
Every command below was run literally, in this order, on an A100 80 GB box (2026-08-26) and produced the outputs shown β see Verified end-to-end at the bottom.
1. Bridge (Python)
# Either the HF CLI ...
hf download sprappcom/qwen36-vision-tower --local-dir ./qwen36-vision-tower
# ... or plain curl (no CLI needed):
mkdir -p qwen36-vision-tower && cd qwen36-vision-tower
T=https://huggingface.co/sprappcom/qwen36-vision-tower/resolve/main
for f in config.json preprocessor_config.json server.py requirements.txt LICENSE vision_tower.safetensors; do
curl -fL -o "$f" "$T/$f"
done
sha256sum vision_tower.safetensors # 160afeed128baaa9918c8c87da4d6d8892c179eba8d0079b2f1697ddfc42773b
python3 -m venv .venv && source .venv/bin/activate # put .venv on a normal (exec) filesystem
pip install --index-url https://download.pytorch.org/whl/cu121 torch==2.5.1 torchvision==0.20.1
pip install -r requirements.txt
export PRISMX_VISION_TOWER_SD=$PWD/vision_tower.safetensors # the weights
export PRISMX_VISION_CONFIG_DIR=$PWD # dir holding config.json + preprocessor_config.json
export PRISMX_VISION_DEVICE=cuda:0 # or "cpu"
export PRISMX_VISION_BRIDGE_SOCK=/tmp/prismx_vision_bridge.sock
python3 server.py
# -> [vision_bridge] real vision tower loaded on cuda:0: 446571248 params, strict-load verified, processor=Qwen2VLImageProcessor
# -> [vision_bridge] listening on /tmp/prismx_vision_bridge.sock
Set CUDA_VISIBLE_DEVICES if you need to pin the GPU (PRISMX_VISION_DEVICE=cuda:0 then means
"the first visible GPU"). On a single GPU the bridge and prism_server share the same device.
The bridge is a long-lived process: supervise it (systemd/supervisord) next to prism_server.
2. prism_server (proprietary)
export PRISMX_PQM_STANDALONE=1
export PRISMX_PQM=/path/to/malay35b.pqm
export PRISMX_TOKENIZER=/path/to/malay35b.tok
export PRISMX_VISION_BRIDGE_SOCK=/tmp/prismx_vision_bridge.sock # same path as the bridge
export PRISMX_STRIP_THINK_OPENAI=1 # hide <think>β¦</think> reasoning from message.content (recommended)
export PRISMX_MIN_TEMPERATURE=0.15 # greedy decoding can collapse on this stack
prism_server /path/to/malay35b.pqm 0.0.0.0:8080 --max-batch 1 --max-seq 8192
# A100 80 GB with CPU-RAM expert offload (~10.9 GB VRAM): add --n-cpu-moe 36 --moe-cache-experts 2048
# small GPU (8 GB): add --n-cpu-moe 38 --moe-cache-experts 512
# -> [prism_server] GPU lane ready; max_batch=1 max_seq=8192 ...
# -> [prism_server] listening on http://0.0.0.0:8080
Argument order matters: the first positional argument is the model path (in standalone mode it is
only used as the served model id, the weights come from PRISMX_PQM), the second is the bind
address. Passing the address first makes the server treat it as the model path and silently bind
the default 0.0.0.0:8080.
--max-seq must hold the image tokens + your text + the model's reasoning + the answer:
one image contributes ceil(W/32) * ceil(H/32) tokens after the 1024 px cap (640x488 β 300,
1024x768 β 768) plus <|vision_start|>/<|vision_end|>. 4096 is the practical minimum; 8192 is
what the verified setup used.
3. Request shape (OpenAI content-array)
# Put the JSON in a file: a base64 photo is easily >100 KB, which overflows the shell's
# argument limit if you inline it in `curl -d "..."` ("Argument list too long").
B64=$(base64 -w0 photo.jpg)
cat > req.json <<REQ
{
"model": "malay35b",
"max_tokens": 2000,
"temperature": 0.3,
"messages": [{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,$B64"}},
{"type": "text", "text": "Describe this image."}
]
}]
}
REQ
curl -s http://127.0.0.1:8080/v1/chat/completions -H 'content-type: application/json' -d @req.json
Content parts are spliced in array order. image_url.url must be a
data:image/<fmt>;base64,<payload> URI (a bare base64 payload is also accepted);
remote http(s):// image URLs are not fetched. Any format PIL decodes (JPEG/PNG/WebP)
works. Max encoded image size: 20 MiB. usage.prompt_tokens in the response includes the image
tokens (a 640x488 photo + a 12-token question β prompt_tokens: 312), which is how you can tell the
image was actually spliced.
max_tokens must cover the model's reasoning. The engine always primes a <think> block; for
image prompts the model reasons for ~450β1200 tokens before answering. With
PRISMX_STRIP_THINK_OPENAI=1 that reasoning is stripped from message.content, so if max_tokens
runs out inside it you get content: "" with finish_reason: "length". Measured on the NYT photo:
max_tokens: 300 β empty every time; max_tokens: 1500 β finish_reason: "stop" in 7 of 9 runs and
still empty in 2 of 9 (the reasoning length varies run to run at temperature 0.3). Use
max_tokens >= 2000 for image requests. Streaming ("stream": true) works with images but the
first content delta only arrives after the reasoning closes (12β19 s measured for a 312-token image
prompt); the SSE stream itself is well-formed (data: {...} chunks, final usage, data: [DONE]).
A per-request enable_thinking: false switch (closed <think> prime, first content in ~2.4 s,
5β8Γ fewer completion tokens) exists in prism-engine PR #11 but is not in the shipped build yet β
and in the measured runs it made image answers noticeably more prone to invented headlines/dates, so
keep thinking on when grounding matters.
Malay answers: a Malay question alone ("Apakah yang anda lihat dalam gambar ini?") was answered
in English in every measured run, as was "Terangkan gambar ini dalam Bahasa Melayu.". Add a Malay
system message β {"role":"system","content":"Anda ialah pembantu yang sentiasa menjawab dalam Bahasa Melayu."} β
and the image description comes back in coherent Malay.
If the bridge is down, requests containing an image fail immediately (4 ms measured) with an
explicit HTTP 400:
{"error":{"message":"vision bridge unreachable: connect <sock>: No such file or directory (os error 2)","type":"invalid_request_error","code":"invalid_image"}}
β the engine never silently drops the image. Text-only requests keep working while the bridge is
down, and image requests resume as soon as the bridge is back (no engine restart needed).
Measured footprints
Measured 2026-08-26 on prism-a100b (A100 SXM4 80 GB, driver 595.71, the pinned venv above,
malay35b.pqm standalone with --n-cpu-moe 36 --moe-cache-experts 2048 --max-batch 1 --max-seq 8192,
bridge and engine on the same GPU). VRAM is the nvidia-smi delta for that GPU.
| Setup | Footprint |
|---|---|
Bridge alone, GPU (BF16, PRISMX_VISION_DEVICE=cuda:0) |
1,304 MiB VRAM (nvidia-smi per-process 1.3 GB) |
+ prism_server malay35b (A100, 36 CPU-MoE blocks) |
+10,890 MiB VRAM, +15.2 GiB page-locked host RAM |
| Peak during a 780-token image request (4000x3000 photo, capped to 1024x768) | +282 MiB over idle (12,476 MiB total for bridge+engine) |
| Bridge embed time, 640x488 JPEG β 300 tokens | 33β38 ms (3 reps) |
| Bridge embed time, 4000x3000 JPEG β resized 1024x768 β 768 tokens | 260β290 ms (3 reps, includes decode + LANCZOS resize) |
Engine prefill-only (max_tokens: 1), text 22 tokens |
0.21 s |
| Engine prefill-only, 312 tokens (300 image) | 1.20 s β image adds β +1.0 s |
| Engine prefill-only, 780 tokens (768 image) | 2.21 s β image adds β +2.0 s |
| Decode with an image in context | ~40 tok/s, same as text-only (706 completion tokens in 17.7 s; text-only 83 tokens in 2.05 s) |
Tower alone, CPU (PRISMX_VISION_DEVICE=cpu) |
works; noticeably slow per image, fine for single-user |
malay35b .pqm with --n-cpu-moe 38 + tower on an RTX 4060 Laptop 8 GB |
Verified end-to-end (2026-08-26, A100)
All via the OpenAI content-array request above, temperature 0.3, max_tokens 1500 (use 2000, see above):
| Test | prompt_tokens | Result |
|---|---|---|
| Real NYT front-page photo (640x488), "Describe this image. What kind of document is it?" | 312 | "This is the front page of The New York Times β¦ Masthead β¦ slogan All the News That's Fit to Print β¦ LATE CITY EDITION" β 706 completion tokens, 17.7 s, no CJK/garbage |
| 4000x3000 solid blue JPEG, "What colour is this image?" | 780 | "solid blue background β¦ royal blue or cobalt" β no OOM, 7.8 s |
| 4000x3000 solid red JPEG | 780 | "solid red background" β 8.7 s |
| Same NYT photo, Malay system prompt + "Apakah yang anda lihat dalam gambar ini?" | 333 | Coherent Malay: "Ini adalah foto muka surat depan The New York Times β¦ Nama Surat Khabar β¦ Edisi: Late City Edition β¦" (29 s, 1182 tokens) |
| Text-only prompts before and after the image requests (EN + MS) | 18β22 | Unaffected, finish_reason: "stop", ~40 tok/s |
| Streaming image request | 312 | 173 SSE chunks, 0 malformed, final usage, data: [DONE] |
Known limitations
- Dense small text is not read reliably. The NYT photo's masthead, slogan and edition line were read correctly every time, but the body headlines were described as "placeholder / blank / pixelated" and one Malay run hallucinated a date. Treat the model as a describer, not an OCR engine.
- Flat vector-style logo images graft poorly (near-uniform colour fields, no photographic texture): the model tends to describe them vaguely or wrongly. Photographs and screenshots with real texture work markedly better. (Solid single-colour test images are identified correctly.)
- Large-photo resize cap: 1024 px longest side.
server.pydownscales anything larger (LANCZOS) before patching. Without this cap a 4000x3000 phone photo produced enough patch tokens to OOM/hang the bridge; the cap bounds VRAM and latency. Fine detail in very large images is lost. - Reasoning tax. The shipped engine has no per-request "thinking off" switch; every image answer
costs ~450β1200 reasoning tokens first (see
max_tokensabove). Theenable_thinking: falseengine change (prism-engine PR #11) removes the tax but measurably increases confabulation on images. - Two crash domains. The bridge is a separate Python process talking to
prism_serverover a Unix socket. If either dies, image requests fail until it is restarted; supervise both (e.g. supervisord). The bridge serialises tower forward passes with a lock β one image at a time. - Images only; no video path is wired even though
temporal_patch_size=2is in the config. - This is the milestone-1 Python bridge, not a native Rust/CUDA port. Expect one Python/torch process (~2-3 GB host RAM) alongside the engine.
License
other / bcz-proprietary β see LICENSE. Same terms as the sprappcom/*-pqm repos.
The weights themselves are an unmodified extraction from Qwen/Qwen3.6-35B-A3B (Apache-2.0);
that attribution is preserved. The bridge, packaging and prism-engine integration are
proprietary to BCZ Singapore Pte Ltd. Contact: sprappcom@gmail.com.
- Downloads last month
- 22
Model tree for sprappcom/qwen36-vision-tower
Base model
Qwen/Qwen3.6-35B-A3B