Skip to content

DeepSeek-VL Hybrid

The dual-tower DeepSeek-VL variant, ported to pure Keras 3. It runs the SigLIP tower alongside a high-resolution SAM-style tower (high_res_* arguments) and fuses both feature streams before the text decoder, which is what lets it read fine detail such as small text.

Build note: the high-res tower only builds when image inputs are passed, so a text-only forward leaves those sublayers unbuilt.

Links:

See also deepseek_vl.md, janus.md.

Variants

Load any of these with from_weights("<variant>").

Variant Hub
deepseek_vl_7b_chat kerasformers release
deepseek_vl_7b_base kerasformers release

API

DeepseekVLHybridModel

DeepSeek-VL Hybrid (7B) backbone: dual vision (SigLIP @384 + SAM @1024) + 3-way aligner + Llama-7B decoder.

Arg Default Meaning
vocab_size 102400 token vocabulary size
embed_dim 4096 text model width
mlp_dim 11008 MLP inner width
num_layers 30 decoder blocks
num_heads 32 query heads
num_kv_heads 32 key/value heads (GQA)
head_dim 128 per-head width
norm_eps 1e-06 normalization epsilon
rope_theta 10000.0 rotary base frequency
tie_embeddings False reuse embeddings as the LM head
vision_embed_dim 1024 vision tower width
vision_mlp_dim 4096 vision MLP width
vision_num_layers 24 vision tower depth
vision_num_heads 16 vision attention heads
image_size 384 expected image resolution
patch_size 16 patch size
vision_norm_eps 1e-06 vision tower norm epsilon
high_res_embed_dim 768
high_res_mlp_dim 3072
high_res_num_layers 12
high_res_num_heads 12
high_res_image_size 1024
high_res_patch_size 16
high_res_output_channels 256
high_res_window_size 14
high_res_global_attn_indexes (2, 5, 8, 11)
high_res_norm_eps 1e-06
image_token_id 100015 placeholder token id expanded per image

DeepseekVLHybridGenerate

DeepSeek-VL Hybrid with an LM head + fast .generate().

generate(input_ids, attention_mask=None, max_new_tokens=None,
         eos_token_id=None, sampler=None, seed=None, **prefill_inputs)

Image and video tensors ride along as **prefill_inputs; the processor produces them for you.

DeepseekVLHybridImageProcessor

Dual-resolution preprocessing for DeepSeek-VL Hybrid (7B).

Arg Default Meaning
size 384 target resolution
high_res_size 1024
min_size 14 smallest allowed edge
image_mean (0.5, 0.5, 0.5) per-channel normalization mean
image_std (0.5, 0.5, 0.5) per-channel normalization std
high_res_image_mean (0.48145466, 0.4578275, 0.40821073)
high_res_image_std (0.26862954, 0.26130258, 0.27577711)
background_color (127, 127, 127) pad colour for letterboxing
high_res_background_color (122, 116, 104)
data_format None channels_last or channels_first

DeepseekVLHybridProcessor

Image + text -> model inputs for DeepSeek-VL Hybrid (7B).

Arg Default Meaning
variant None variant whose tokenizer/processor files to fetch
hf_id None Hub repo to pull tokenizer/processor files from
num_image_tokens 576 tokens each image expands to
tokenizer None override the default tokenizer
image_processor None override the default image processor

DeepseekVLHybridTokenizer

DeepSeek-VL Hybrid (7B) tokenizer.

Arg Default Meaning
variant None variant whose tokenizer/processor files to fetch
hf_id None Hub repo to pull tokenizer/processor files from
tokenizer_file None explicit path to a tokenizer.json

End-to-end example

Single input (image + text)

import os
os.environ["KERAS_BACKEND"] = "torch"   # or "jax" / "tensorflow"

from PIL import Image
from kerasformers.models.deepseek_vl_hybrid import DeepseekVLHybridGenerate, DeepseekVLHybridProcessor

model = DeepseekVLHybridGenerate.from_weights("deepseek_vl_7b_chat")
processor = DeepseekVLHybridProcessor.from_weights("deepseek_vl_7b_chat")

image = Image.open("photo.jpg")
inputs = processor(conversation=[{
    "role": "user",
    "content": [
        {"type": "image", "image": image},
        {"type": "text", "text": "Describe this image in one sentence."},
    ],
}])
outputs = model.generate(**inputs, max_new_tokens=64)

print(processor.decode(outputs[0]))

Several images in one conversation

Add one image content item per image. The processor expands each marker to that image's own patch count:

inputs = processor(conversation=[{
    "role": "user",
    "content": [
        {"type": "image", "image": Image.open("a.jpg")},
        {"type": "image", "image": Image.open("b.jpg")},
        {"type": "text", "text": "What differs between these two images?"},
    ],
}])
outputs = model.generate(**inputs, max_new_tokens=64)

Batch

Pass a list of conversations. Each one is rendered separately and takes only the images its own markers claim, so the conversations do not need the same number of images or images of the same size:

conversations = [
    [{"role": "user", "content": [
        {"type": "image", "image": Image.open("a.jpg")},
        {"type": "text", "text": "What is in this image?"}]}],
    [{"role": "user", "content": [
        {"type": "image", "image": Image.open("b.jpg")},
        {"type": "image", "image": Image.open("c.jpg")},
        {"type": "text", "text": "What differs between these?"}]}],
]
inputs = processor(conversation=conversations)
outputs = model.generate(**inputs, max_new_tokens=64)

for text in processor.batch_decode(outputs):
    print(text)

Text-only prompts batch the same way: pass text=[...] with no images.

Text only

DeepseekVLHybridTokenizer encodes raw text: it has no chat template, so pass a prompt you have rendered yourself (or go through the processor above).

from kerasformers.models.deepseek_vl_hybrid import DeepseekVLHybridTokenizer

tokenizer = DeepseekVLHybridTokenizer.from_weights("deepseek_vl_7b_chat")
inputs = tokenizer("Who wrote Dune?")
outputs = model.generate(**inputs, max_new_tokens=32)
print(tokenizer.decode(outputs[0]))

Lower memory

Larger checkpoints load in bf16 or weight-only quantized. See quantization.md:

model = DeepseekVLHybridGenerate.from_weights(
    "deepseek_vl_7b_chat", quantization="int8", low_memory=True, load_dtype="bfloat16"
)