A parameter-efficient fine-tune of LLaVA-OneVision (7B parameters) for
medical (radiology) image captioning, adapted with QDoRA. Given a
radiology image, it generates a short free-text caption describing the image.
Part of the CS_Morgan Lab submission to ImageCLEFmedical Caption 2026.
Project: this model is one of seven in the Radiology Image Captioning collection — LLaVA-OneVision-Qwen2 at 0.5B, 7B and 72B, each adapted with LoRA, QLoRA or QDoRA.
Code:medical-vlm-explainability — training, caption generation, and the attention-based explainability pipeline.
What this is (read first)
This repo contains a PEFT adapter, not a standalone model. The adapter is a
small set of low-rank weight updates; the ~~16 GB of original base-model
weights are not here. At load time you fetch two things:
Nothing in this repo runs without that exact base model.
DoRA adapter. This uses weight-decomposed low-rank adaptation
(use_dora: true), so it ships magnitude vectors in addition to the usual
lora_A / lora_B matrices. Loading it requires a peft build with DoRA
support (>= 0.9; trained on 0.18.1). Older versions will fail or silently
ignore the magnitude vectors.
Files in this repo
File
What it is
adapter_model.safetensors
The trained adapter weights
adapter_config.json
PEFT config: rank, alpha, target modules, base model id
tokenizer.json, tokenizer_config.json
Tokenizer, for reference (identical in behaviour to the base)
chat_template.jinja
Chat template used in training (identical to the base processor's)
processor_config.json
Processor settings, for reference
training_args.bin
The original TrainingArguments, for reference
Note there is no config.json and no preprocessor_config.json here, since
those belong to the base model. This is why the examples below load the
processor from the base — see Troubleshooting.
Installation
bash
1pip install"transformers>=4.45""peft>=0.9" accelerate safetensors pillow
2# 4-bit loading (optional, recommended for the 7B and 72B):3pip install bitsandbytes
1import torch
2from PIL import Image
3from transformers import AutoProcessor, LlavaOnevisionForConditionalGeneration
4from peft import PeftModel
56BASE ="llava-hf/llava-onevision-qwen2-7b-ov-hf"7ADAPTER ="HoqueMahmudul/llava-onevision-7b-qdora-radiology-image-caption"89# 1. base model10model = LlavaOnevisionForConditionalGeneration.from_pretrained(11 BASE, torch_dtype=torch.bfloat16, device_map="auto"12)13# 2. this adapter on top14model = PeftModel.from_pretrained(model, ADAPTER)15model.eval()1617# 3. processor from the BASE (not from this repo -- see Troubleshooting)18processor = AutoProcessor.from_pretrained(BASE)1920# 4. build the prompt with the SAME text used in training21image = Image.open("your_image.jpg").convert("RGB")22conversation =[23{24"role":"user",25"content":[26{"type":"image"},27{"type":"text","text":"Describe this medical image."},28],29}30]31prompt = processor.apply_chat_template(conversation, add_generation_prompt=True)32inputs = processor(images=image, text=prompt, return_tensors="pt").to(model.device)3334# 5. greedy decoding, as used for the ImageCLEF submission35with torch.no_grad():36 out = model.generate(**inputs, max_new_tokens=256, do_sample=False)3738caption = processor.decode(39 out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True40)41print(caption.strip())
Use the prompt "Describe this medical image." verbatim. The adapter was
trained with only that instruction; other phrasings are out of distribution and
degrade output quality.
The rendered prompt should look exactly like this:
<|im_start|>user <image>
Describe this medical image.<|im_end|><|im_start|>assistant
Lower memory: load the base in 4-bit
This adapter was trained against a 4-bit NF4-quantized base, so loading the base in 4-bit reproduces the training-time setup and cuts VRAM substantially:
The processor supports batching, but LLaVA-OneVision expands each image into a
variable number of visual tokens depending on resolution, so padded batches need
processor.tokenizer.padding_side = "left" for correct generation. Looping one
image at a time is simplest and is what the original evaluation did.
Hardware requirements
Measured on 500 validation images, batch size 1, greedy decoding with
max_new_tokens=256 (the setting used for the published captions), on the GPU
named below. Figures are torch.cuda.max_memory_allocated() on a single
device holding a complete model — the model is replicated, not sharded.
GPU measured on
Base weights (bf16)
Base loaded as
Peak inference VRAM
Latency
this variant (7B)
NVIDIA H200 (gpuH200x8)
~16 GB
4-bit
23.72 GB
5.63 s/image
Comparing these numbers across variants requires care. Scales were measured
on different GPUs (0.5B on A100, 7B and 72B on H200), so latency is not
comparable across scales. Within a scale it is comparable only when the base
loading also matches: QLoRA vs QDoRA is a fair comparison (same GPU, both
4-bit), but LoRA vs QLoRA is not — those differ in base quantization as well
as PEFT method, so the gap conflates the two.
Why peak memory exceeds the weight size. LLaVA-OneVision uses anyres tiling:
a large image expands into thousands of visual tokens (1024x768 -> ~5,100 tokens;
1920x1080 -> ~11,700), and those activations dominate. Quantizing the base to
4-bit shrinks weights but not activations, so 4-bit does not reduce peak
inference memory proportionally — budget from the measured figure, not from
parameter count.
Peak scales with input resolution and max_new_tokens; smaller images need much
less. The adapter itself adds only tens to hundreds of MB.
Serving several adapters on one base
Loading several adapters onto one resident base and switching with
set_adapter() was tested against separately-loaded references: 20 validation
images, 4-bit NF4 base, greedy decoding, max_new_tokens=256, across every
ordered pair in this series.
Result: adding a second adapter changed nothing — 0/20 captions differed.
Hot-swapping is safe for these adapters, in either load order.
One caveat, separate from hot-swapping. Loading an adapter under a
non-defaultadapter_name produced different captions than loading it with
the default name — 9/20 images on the same base with identical inputs and
greedy decoding. The outputs are not worse, but they are not bit-reproducible
across the two loading forms. If you need to reproduce a previous run exactly,
load the adapter the same way you did originally.
(peft 0.18.1, transformers 5.3.0, torch 2.11.0+cu128, bitsandbytes 0.49.2)
Inference cost of DoRA
DoRA recomputes weight column norms on every forward pass through every linear
adapter module, which makes it slower at inference than the equivalent QLoRA
adapter. At 72B — 8 target modules across 80 decoder layers — this overhead
dominates inference latency.
Scale
Hardware
QLoRA
QDoRA
Overhead
0.5B
A100
2.49 s/image
4.73 s/image
1.9x
7B
H200
1.63 s/image
5.63 s/image
3.4x
72B
A100 x8
12.5 s/image
71.6 s/image
5.7x
The penalty grows with decoder depth and the number of adapted modules.
If inference latency matters to you, prefer the QLoRA adapter at the same
scale. The two were trained with an identical recipe and differ only in the
adapter formulation.
Continuing fine-tuning from this adapter
You can keep training these weights on your own data. Two options:
Option A — continue training this adapter
python
1model = LlavaOnevisionForConditionalGeneration.from_pretrained(2 BASE, torch_dtype=torch.bfloat16, device_map="auto"3)4model = PeftModel.from_pretrained(model, ADAPTER, is_trainable=True)# <-- REQUIRED5model.print_trainable_parameters()# must report a non-zero trainable count
is_trainable=True is not optional. Without it, PEFT loads the adapter in
inference mode with requires_grad=False; training then runs to completion
without ever updating the adapter. Always confirm
print_trainable_parameters() reports non-zero.
Things to know:
This is a warm start, not an exact resume. Optimizer and scheduler state
are deliberately not published, so you begin with a fresh optimizer. Use a
learning rate lower than the original 1e-4 to avoid washing out the
learned weights.
The adapter's shape is fixed by adapter_config.json (r=16, alpha=32, and the
target-module list below). To change rank or target modules you must train a
new adapter — see Option B.
Option B — train a fresh adapter, using this one only as a reference
The SigLIP vision encoder, the language-model head, and the token embeddings
were kept frozen during the original training.
Data format and collation
Each training example is one image plus one target caption, rendered through the
same chat template. Sketch of a collator:
python
1defcollate(batch):2 prompts, images =[],[]3for ex in batch:# ex = {"image": PIL.Image, "caption": str}4 conv =[5{"role":"user","content":[6{"type":"image"},7{"type":"text","text":"Describe this medical image."},8]},9{"role":"assistant","content":[10{"type":"text","text": ex["caption"]},11]},12]13 prompts.append(processor.apply_chat_template(conv))14 images.append(ex["image"])1516 out = processor(images=images, text=prompts, return_tensors="pt", padding=True)17 labels = out["input_ids"].clone()18 labels[labels == processor.tokenizer.pad_token_id]=-10019 out["labels"]= labels # mask prompt tokens too, to train on the answer only20return out
The inference examples above were run and verified. The training snippets are
templates — adapt them to your dataset, and mask the prompt tokens in
labels if you want loss on the caption only.
To reproduce the original setup, mirror the recipe below: effective batch size
16, bf16, adamw_torch, warmup ratio 0.03, weight decay 0.01, validation loss
evaluated every 500 steps, early stopping after four evaluations without
improvement, best checkpoint restored by validation loss.
Merging the adapter into the base (optional)
For one-line loading later, you can bake the adapter into a full model:
Caveats: the result is the full model size (~16 GB), not the adapter
size. Because this adapter was trained against a 4-bit base, merging into a bf16 base introduces a small numerical mismatch; this is standard practice and usually harmless, but validate on your data. Merging into a 4-bit-quantized base is not supported —
load the base in bf16 to merge.
Troubleshooting
OSError / LocalEntryNotFoundError mentioning huggingface.co when loading
the processor. You called AutoProcessor.from_pretrained(ADAPTER). This repo
has no config.json/preprocessor_config.json, so the processor class can't be
resolved from it; online this fails over silently, offline it errors. Load the
processor from the base instead: AutoProcessor.from_pretrained(BASE). It is
equivalent — the base processor's chat template is byte-identical to
chat_template.jinja here, and its tokenizer encodes the training prompt to the
same token ids.
Training runs but the adapter never changes. You omitted is_trainable=True
in PeftModel.from_pretrained. See Option A.
TypeError: can only concatenate str (not "list") to str when applying the
chat template. You used the tokenizer's template (text-only) instead of the
processor's (multimodal). Call processor.apply_chat_template(...).
DoRA keys missing / unexpected, or magnitude vectors ignored. Upgrade to
peft >= 0.9.
Out of memory. Load the base in 4-bit (see above), reduce image resolution,
or use a smaller variant in this series.
Captions are generic or off-topic. Check that your prompt is exactly
"Describe this medical image." and that the adapter actually loaded
(model.peft_config should be populated).
Training data
The updated version of ROCOv2 (Radiology Objects in COntext v2), the dataset
adopted by ImageCLEFmedical 2026 — radiology figures from the PubMed Central
Open Access subset, each paired with a caption and UMLS concept metadata,
spanning chest X-ray, CT, MRI, ultrasound, echocardiography, angiography,
mammography, retinal fundus, dental panoramic, microscopy and pathology.
The partition is the one fixed by the task organizers, split by image-ID prefix
rather than randomly: a captioned development pool of 116,604 images divided
into 97,364 training / 19,240 validation, plus a separate 15,249-image test
set released without reference captions.
The dataset is not redistributed here. Obtain it from ImageCLEF / ROCOv2
directly, and credit both as the source of the training data.
Fine-tuning recipe (as trained)
Identical across all variants in this series except the learning rate.
Target modules: q_proj, k_proj, v_proj, gate_proj, up_proj,
down_proj, linear_1, linear_2 (LLM attention + MLP, and the multimodal
projector — the projector is included because it is where vision-to-language
alignment is formed)
Precision bf16, optimizer adamw_torch, warmup ratio 0.03, weight decay 0.01
Learning rate: 1e-4
Effective batch size 16 (per-device 1 x grad-accum 2 x 8 GPUs)
Early stopping on validation loss; best checkpoint restored
Inference: greedy decoding, max_new_tokens=256 (verified from the
published caption outputs; an earlier revision of this card said 128)
Hardware: one node of 8x A100 40 GB, bfloat16, gradient checkpointing, PyTorch
DistributedDataParallel via torchrun
Evaluation
This system was evaluated by the ImageCLEFmedical Caption 2026 organizers on
the official held-out test set of 15,249 radiology images (references not
publicly released). It was one of four systems submitted by the CS_Morgan Lab
(0.5B and 7B, each with QLoRA and QDoRA), one run per system, using greedy
decoding as described above.
The caption-prediction subtask was scored by six automated metrics, which the
organizers group into two aspects and average into a single overall score:
Relevance aspect
Metric
Notes
BERTScore
the official primary metric of the subtask
ROUGE-1
unigram overlap with the reference caption
Image–caption similarity
similarity between the image and the generated caption
BLEURT
learned evaluation metric
Factuality aspect
Metric
Notes
UMLS concept F1
clinical concept agreement, computed with MedCAT
AlignScore
claim-level factual consistency
Relevance is the mean of the four relevance metrics, factuality the mean of the
two factuality metrics, and the overall score the mean of the two aspects.
The same system family was additionally evaluated in the ImageCLEFmedical 2026
explainability subtask, where a radiologist scored submissions on a five-point
scale across nine criteria: readability, accuracy, level of detail, caption
focus, visualization consistency, comprehensiveness, visualization focus,
methodology, and clinician's favourite. That subtask scored each team's
per-image submission rather than each individual system.
Score values are not reproduced on this card.
Intended use
Research and educational use in medical image understanding, and as a starting
point for further fine-tuning.
Limitations and caveats
NOT a medical device. Outputs can be wrong or hallucinated. Do not use to inform patient care.
This is an adapter and does not run without the exact base model above.
Trained only on the ImageCLEFmedical 2026 / ROCOv2 distribution; performance on
other distributions is unknown.
Citation
If you use this model, please cite:
bibtex
1@inproceedings{hoque2026csmorgan,
2 title = {Model-Intrinsic Attention as Explanation for Radiology Image Captioning},
3 author = {Hoque, Mahmudul and Chowdhury, Raisa Nusrat and
4 Oluwafemi, Ejiga Peter Ojonugwa and Islam, Okib Ul and
5 Hoque, Rahmanul and Rahman, Md Mahmudur},
6 booktitle = {CLEF 2026 Working Notes},
7 series = {CEUR Workshop Proceedings},
8 publisher = {CEUR-WS.org},
9 address = {Jena, Germany},
10 year = {2026}
11}