Vai al contenuto principale

Gradient Checkpointing: fine-tuning efficiente con Unsloth e NVIDIA

Scopri come il gradient checkpointing ottimizzato per NVIDIA di Unsloth e QLoRA riducono l’uso di VRAM durante il fine-tuning di Qwen3.5 4B per l’OCR di documenti medici.
Aggiornato 14 ago 2026  · 12 min leggi

Esplora con l'AI

ChatGPTClaudePerplexity

La collaborazione di Unsloth con NVIDIA punta a rendere il fine-tuning più veloce riducendo gli overhead nascosti dell’addestramento. Invece di affidarsi solo a GPU più grandi o modelli più piccoli, i miglioramenti colpiscono i colli di bottiglia interni al processo di training, come la costruzione ripetuta dei metadati, i ritardi nel ricaricamento delle attivazioni e il routing inefficiente dei token. Per te questo si traduce in training più rapido e un flusso di fine-tuning più fluido sulle GPU NVIDIA supportate.

In questa guida, scopriremo questi nuovi miglioramenti delle prestazioni di Unsloth e applicheremo un flusso di lavoro di fine-tuning ottimizzato a un compito pratico visione-linguaggio. Effettueremo il fine-tuning di Qwen3.5 Vision 4B per l’OCR medico, dove il modello impara a estrarre testo strutturato da immagini di documenti medici usando un piccolo sottoinsieme dall’aspetto “medicale” di un dataset OCR.

Useremo:

  • Qwen3.5 4B (Vision) come modello di base
  • QLoRA a 4 bit per ridurre l’uso di VRAM
  • Adapter LoRA per un fine-tuning efficiente
  • Gradient checkpointing di Unsloth per risparmiare memoria durante l’addestramento
  • Un sottoinsieme da 300 campioni di un dataset OCR medico
  • Preprocessing delle immagini a dimensione fissa per un training visivo più fluido
  • Valutazione prima e dopo per confrontare gli output del modello base e di quello fine-tunato

Uso del workflow di fine-tuning ottimizzato per NVIDIA di Unsloth

Prima di iniziare il fine-tuning, è utile capire cosa migliora la collaborazione tra Unsloth e NVIDIA e come si collega a questa guida.

Unsloth riporta che la collaborazione con NVIDIA rende l’addestramento degli LLM circa più veloce del 25%, senza perdita di accuratezza, oltre ai precedenti speedup di 2–5x nel fine-tuning. Questi guadagni derivano dalla riduzione dell’overhead nascosto attorno al processo principale di training, anziché dal cambiamento dell’obiettivo di apprendimento del modello. In altre parole, l’obiettivo è rendere il fine-tuning più veloce ed efficiente mantenendo invariata l’accuratezza.

Source: How to Make LLM Training Faster with Unsloth and NVIDIA

Fonte: How to Make LLM Training Faster with Unsloth and NVIDIA 

Prestazioni di training migliorate

La collaborazione riporta diversi miglioramenti prestazionali, tra cui:

  • +14,3% per batch su un benchmark Qwen3-14B QLoRA SFT grazie al caching dei metadati di sequenze impacchettate
  • +8,4% di velocità sui modelli 8B, +6,7% sui 14B e +4,6% sui 32B grazie al gradient checkpointing asincrono con doppio buffering
  • Circa +10–15% di speedup per il training GPT-OSS MoE, con +23% in forward e +13% in backward nel percorso di routing mirato

Alcuni dei guadagni maggiori della collaborazione Unsloth e NVIDIA si applicano al training testuale impacchettato e ai modelli Mixture-of-Experts. Non li usiamo in questa guida perché il nostro flusso di lavoro è incentrato sul fine-tuning OCR con Qwen3.5 Vision. 

In questa guida usiamo una GPU NVIDIA RTX 3090, quindi il workflow ruota attorno all’accelerazione GPU NVIDIA e al percorso di fine-tuning ottimizzato di Unsloth. Non stiamo effettuando benchmark di Unsloth rispetto ad altri trainer, quindi questa guida non va letta come prova indipendente degli speedup riportati. Stiamo invece applicando il workflow ottimizzato di Unsloth a un vero compito visione-linguaggio.

Gradient checkpointing

Per questo flusso di lavoro, l’ottimizzazione più rilevante è il gradient checkpointing di Unsloth. Aiuta a ridurre l’uso di memoria durante l’addestramento evitando di dover conservare ogni attivazione nella memoria della GPU. È particolarmente utile per il fine-tuning visione-linguaggio, dove il modello deve processare sia input immagine sia output testuali.

1. Configurare Unsloth per un fine-tuning più veloce

Per seguire questa guida, ti serve l’accesso a una GPU NVIDIA. Puoi noleggiarne una su piattaforme come RunPod, Vast.ai o qualsiasi altro provider cloud di GPU. Inizialmente ho provato a usare RunPod perché di solito è veloce e affidabile, ma in quel momento le opzioni con RTX 3090 erano limitate. Ho quindi usato una macchina Vast.ai con GPU RTX 3090 per questo workflow. 

Per un confronto tra le diverse piattaforme, dai un’occhiata alla nostra guida sui migliori provider cloud di GPU.

Vast.ai RTX 3090 instance

Fonte:  Vast.ai | Console 

Dopo aver avviato l’istanza, ho aperto Jupyter Notebook e creato un nuovo notebook. Su Vast.ai, ho selezionato il kernel dell’ambiente principale disponibile per installare i pacchetti Python necessari nell’ambiente del notebook senza toccare le dipendenze a livello di sistema. 

Installare i pacchetti necessari

Per prima cosa, installa i pacchetti richiesti per Unsloth, PyTorch, training di modelli di visione, caricamento dataset e integrazione con Hugging Face: 

!pip install --upgrade \
    "torch>=2.8.0" "triton>=3.4.0" \
    numpy pillow torchvision bitsandbytes \
    unsloth "unsloth_zoo>=2026.4.6" \
    "datasets>=4.0.0" huggingface_hub hf_transfer pandas \
    transformers==5.2.0 torchcodec timm

Questi pacchetti si basano sulla configurazione ufficiale del notebook Unsloth e includono le librerie principali necessarie per caricare Qwen3.5 Vision, preparare dati immagine-testo e fare fine-tuning del modello con Unsloth. 

Configurare il dispositivo CUDA

Configuriamo quindi il dispositivo CUDA e verifichiamo che la GPU NVIDIA corretta sia disponibile. Poiché questa guida usa una RTX 3090, il codice controlla se CUDA è abilitato, conferma la GPU selezionata, stampa le versioni di CUDA e PyTorch e verifica che la macchina abbia abbastanza VRAM per questo esperimento. 

import os
import platform

CUDA_DEVICE_INDEX = 0
TARGET_GPU_NAME = "3090"

# Must be set before CUDA / Unsloth are initialized. Restart the kernel if you change these.
os.environ["CUDA_VISIBLE_DEVICES"] = str(CUDA_DEVICE_INDEX)

# RunPod + Qwen3.5 Vision OCR can hit Torch Dynamo fullgraph recompile limits.
# This disables Unsloth's torch.compile path while keeping Unsloth model loading,
# LoRA, gradient checkpointing, collator, and 8-bit optimizer benefits.
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
os.environ["TORCH_COMPILE_DISABLE"] = "1"

import torch

DEVICE = torch.device("cuda:0")

print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())

if not torch.cuda.is_available():
    raise RuntimeError("CUDA is not available. Select a GPU instance before continuing.")

torch.cuda.set_device(0)
props = torch.cuda.get_device_properties(0)
gpu_name = torch.cuda.get_device_name(0)
total_gpu_memory_gb = props.total_memory / 1024**3

print("Selected device:", DEVICE)
print("GPU:", gpu_name)
print("CUDA version:", torch.version.cuda)
print("BF16 supported:", torch.cuda.is_bf16_supported())
print("Total GPU memory:", round(total_gpu_memory_gb, 2), "GB")

if TARGET_GPU_NAME not in gpu_name:
    raise RuntimeError(f"Expected an RTX {TARGET_GPU_NAME}, but CUDA device 0 is: {gpu_name}")

if total_gpu_memory_gb < 20:
    raise RuntimeError(f"Expected a 24 GB class 3090, but only found {total_gpu_memory_gb:.2f} GB VRAM.")

Nel mio setup, l’ambiente ha restituito la seguente configurazione GPU:

Python: 3.12.13
PyTorch: 2.12.0+cu130
CUDA available: True
Selected device: cuda:0
GPU: NVIDIA GeForce RTX 3090
CUDA version: 13.0
BF16 supported: True
Total GPU memory: 23.56 GB

Questo conferma che il notebook sta girando su una NVIDIA GeForce RTX 3090 con sufficiente VRAM per l’esperimento di fine-tuning.

Definire impostazioni di training e prompt

Dopo aver verificato la GPU, definiamo modello, dataset, impostazioni di training, directory di output, dimensione delle immagini e prompt per l’OCR.

MODEL_NAME = "unsloth/Qwen3.5-4B"
DATASET_NAME = "naazimsnh02/medocr-vision-dataset"

SAMPLE_COUNT = 300
EVAL_INDEX = 0
MAX_LENGTH = 4096
MAX_STEPS = 30

PER_DEVICE_BATCH_SIZE = 4
GRADIENT_ACCUMULATION_STEPS = 2
LEARNING_RATE = 2e-4
SEED = 3407

OUTPUT_DIR = "outputs/qwen35_vision_medical_ocr"
ADAPTER_DIR = "qwen35-vision-medical-ocr-lora"

# Medical document images vary heavily in size. Fixed-size canvases avoid
# repeated Torch Dynamo recompiles during vision training.
# 768x1024 is a practical portrait-page compromise for a 24 GB 3090 smoke test.
FIXED_IMAGE_SIZE = (768, 1024)

# Official Unsloth Qwen3.5 Vision notebook uses False here for 16-bit LoRA.
# Set True only if you hit VRAM limits.
LOAD_IN_4BIT = True

SYSTEM_PROMPT = "You are a medical OCR transcription engine. Return only the exact text visible in the medical document image."
INSTRUCTION = "Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning."

Qui usiamo Qwen3.5 Vision 4B di Unsloth e il dataset di visione per OCR medico. Per questa guida selezioniamo 300 campioni e addestriamo per 30 step, mantenendo l’esecuzione leggera ma sufficiente a mostrare come il modello si adatta al formato OCR di destinazione.

La dimensione fissa 768×1024 aiuta a mantenere coerenti gli input immagine durante l’addestramento. I documenti medici possono variare molto in risoluzione e rapporto d’aspetto, quindi ridimensionarli su una tela fissa rende il workflow più fluido e riduce i problemi legati alle forme durante il fine-tuning visione-linguaggio.

2. Caricare il modello

Ora che l’ambiente è pronto, possiamo caricare il modello Qwen3.5 Vision 4B usando FastVisionModel di Unsloth

import unsloth
from unsloth import FastVisionModel
torch.cuda.set_device(0)

model, tokenizer = FastVisionModel.from_pretrained(
    MODEL_NAME,
    load_in_4bit=LOAD_IN_4BIT,
    use_gradient_checkpointing="unsloth",
)

print("Loaded:", MODEL_NAME)
print("4-bit:", LOAD_IN_4BIT)
print("Model device:", next(model.parameters()).device)

Dopo il caricamento, l’output conferma che è stato caricato il modello corretto, che la modalità a 4 bit è attiva e che il modello è sulla GPU: 

Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0

Qui, FastVisionModel.from_pretrained() carica il modello visione-linguaggio e applica le ottimizzazioni di Unsloth per un fine-tuning più veloce ed efficiente in termini di memoria. Abilitiamo anche load_in_4bit, che riduce l’uso di VRAM caricando il modello in precisione a 4 bit. È utile quando si lavora con una GPU da 24 GB come la RTX 3090.

Abilitiamo anche il gradient checkpointing di Unsloth con use_gradient_checkpointing="unsloth"

Questo aiuta a ridurre l’uso di memoria durante l’addestramento, particolarmente importante per i modelli visione-linguaggio perché processano sia immagini sia testo. 

3. Aggiungere gli adapter LoRA

Successivamente aggiungiamo gli adapter LoRA al modello. LoRA permette di fine-tunare un sottoinsieme più piccolo di parametri allenabili invece di aggiornare l’intero modello. Ciò rende l’addestramento più veloce, più efficiente in memoria e più semplice da eseguire su una singola GPU. 

 
model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers=True,
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    random_state=SEED,
    use_rslora=False,
    loftq_config=None,
)

Per questa guida, gli adapter sono aggiunti sia alla parte visiva sia a quella linguistica del modello. Questo aiuta il modello a leggere le immagini di documenti medici e a produrre il testo OCR strutturato atteso. Dopo questo passaggio, il modello è pronto per essere addestrato sul dataset OCR medico. 

4. Caricare il dataset OCR medico

Ora carichiamo il dataset OCR medico da Hugging Face e prepariamo un piccolo sottoinsieme per il fine-tuning.

from datasets import load_dataset
from PIL import Image

raw_dataset = load_dataset(DATASET_NAME, split="train")

MEDICAL_KEYWORDS = [
    "doctor", "dr.", "clinic", "hospital", "patient", "medication",
    "medications", "prescription", "signature", "department", "report",
    "diagnosis", "lab", "laboratory", "blood", "hemoglobin", "mg", "dose",
    "<s_ocr>",
]

Il dataset contiene immagini di documenti e il relativo testo OCR. Poiché per questa guida vogliamo solo esempi di OCR in stile medico, filtriamo il dataset con un semplice approccio basato su parole chiave. Il codice cerca termini comuni nei documenti medici, come doctor, clinic, patient, medication, prescription, diagnosis e parole legate al dosaggio. 

def looks_medical(sample):
    text = str(sample.get("text", "")).lower()
    return any(keyword in text for keyword in MEDICAL_KEYWORDS)

medical_indices = []
for idx, sample in enumerate(raw_dataset):
    if looks_medical(sample):
        medical_indices.append(idx)
        if len(medical_indices) >= SAMPLE_COUNT:
            break

if not medical_indices:
    raise RuntimeError("No medical-looking OCR samples found. Broaden MEDICAL_KEYWORDS or inspect the dataset text field.")

print(f"Selected {len(medical_indices)} medical-looking samples.")

Questo ci offre un modo leggero per selezionare esempi pertinenti al compito di OCR medico. Per questa esecuzione, selezioniamo 300 campioni dall’aspetto medicale.

Poi normalizziamo ogni immagine su una tela fissa 768×1024. Le immagini dei documenti medici possono avere dimensioni e rapporti d’aspetto diversi, quindi questo passaggio aiuta a rendere i dati di training più coerenti. L’immagine viene ridimensionata mantenendo il suo rapporto d’aspetto originale, quindi posizionata su uno sfondo bianco. 

def normalize_ocr_image(image, size=FIXED_IMAGE_SIZE):
    image = image.convert("RGB")
    target_w, target_h = size
    scale = min(target_w / image.width, target_h / image.height)
    new_w = max(1, int(image.width * scale))
    new_h = max(1, int(image.height * scale))
    resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS)

    canvas = Image.new("RGB", size, "white")
    left = (target_w - new_w) // 2
    top = (target_h - new_h) // 2
    canvas.paste(resized, (left, top))
    return canvas

Invece di usare datasets.map, costruiamo manualmente una semplice lista Python. Questo evita possibili blocchi in alcuni ambienti notebook cloud quando si riscrivono immagini PIL. 

dataset = []
for idx in medical_indices:
    sample = raw_dataset[idx]
    dataset.append(
        {
            "image": normalize_ocr_image(sample["image"]),
            "text": sample["text"],
        }
    )

print("Examples:", len(dataset))
print("Columns:", list(dataset[0].keys()))
print("Fixed image size:", dataset[EVAL_INDEX]["image"].size)
print("Sample text:", dataset[EVAL_INDEX]["text"])

Dopo il preprocessing, ogni esempio contiene due campi: l’immagine normalizzata e il testo OCR di destinazione. 

Examples: 300
Columns: ['image', 'text']
Fixed image size: (768, 1024)
Sample text: <s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Possiamo anche visualizzare un’anteprima di uno degli esempi ridimensionati:

dataset[EVAL_INDEX]["image"].resize((384, 512))

L’anteprima mostra un’immagine di documento in stile medico con dettagli della clinica, nome del medico, informazioni del paziente, farmaci e firma. Questo conferma che il dataset è adatto al compito di fine-tuning OCR. 

Doctors prescription.

5. Convertire i campioni in conversazioni visive

Ora che il dataset è caricato e le immagini sono normalizzate, dobbiamo convertire ogni esempio nel formato di conversazione atteso da Qwen3.5 Vision.

Ogni campione di training dovrebbe includere tre parti:

  • Un messaggio di sistema che definisce il ruolo del modello come motore di trascrizione OCR medico
  • Un messaggio utente che contiene l’immagine e l’istruzione OCR
  • Un messaggio dell’assistente che contiene l’output OCR atteso
def build_ocr_messages(image=None, target_text=None, instruction=INSTRUCTION):
    user_content = [
        {"type": "image"},
        {"type": "text", "text": instruction},
    ]

    if image is not None:
        user_content[0]["image"] = image

    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
        {"role": "user", "content": user_content},
    ]

    if target_text is not None:
        messages.append(
            {
                "role": "assistant",
                "content": [{"type": "text", "text": target_text}],
            }
        )

    return messages

La funzione di supporto sopra crea la struttura dei messaggi sia per il training sia per l’inferenza. Durante il training, includiamo il testo OCR di destinazione come risposta dell’assistente. In inferenza, forniamo solo immagine e istruzione e chiediamo al modello di generare il testo OCR.

Successivamente convertiamo ogni campione del dataset in questo formato di conversazione:

def convert_to_conversation(sample):
    return {
        "messages": build_ocr_messages(
            image=sample["image"],
            target_text=sample["text"],
        )
    }


converted_dataset = [convert_to_conversation(sample) for sample in dataset]
converted_dataset[0]

Dopo la conversione, ogni campione contiene un elenco di messaggi. Il primo esempio include il prompt di sistema, l’immagine del documento medico, l’istruzione OCR e la trascrizione OCR strutturata attesa. Questo formato consente al modello di imparare a mappare un’immagine e un’istruzione al testo corretto. 

{'messages': [{'role': 'system',
   'content': [{'type': 'text',
     'text': 'You are a medical OCR transcription engine. Return only the exact text visible in the medical document image.'}]},
  {'role': 'user',
   'content': [{'type': 'image',
     'image': <PIL.Image.Image image mode=RGB size=768x1024>},
    {'type': 'text',
     'text': 'Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning.'}]},
  {'role': 'assistant',
   'content': [{'type': 'text',
     'text': '<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>'}]}]}

6. Valutare il modello base prima del fine-tuning

Prima del training, dovremmo testare il modello base su un esempio di OCR. Questo ci offre un punto di riferimento per confrontare l’output del modello prima e dopo il fine-tuning.

Per prima cosa, definiamo una funzione di supporto per applicare il chat template del modello. Alcune versioni del tokenizer supportano enable_thinking=False, altre no, quindi la funzione include un fallback per mantenere il codice compatibile.

def render_ocr_chat_template(tokenizer, messages):
    try:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=False,
        )
    except TypeError:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
        )

Poi definiamo la funzione di generazione. Costruisce il prompt OCR, passa sia l’immagine sia le istruzioni testuali al tokenizer, genera l’output del modello e decodifica solo i token appena generati. 

def generate_ocr_text(model, tokenizer, image, instruction=INSTRUCTION, max_new_tokens=512):
    messages = build_ocr_messages(instruction=instruction)
    input_text = render_ocr_chat_template(tokenizer, messages)
    inputs = tokenizer(
        images=image,
        text=input_text,
        add_special_tokens=False,
        return_tensors="pt",
    ).to(DEVICE)

    with torch.inference_mode():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            use_cache=True,
            do_sample=False,
            temperature=None,
            top_p=None,
        )

    prompt_length = inputs["input_ids"].shape[-1]
    generated_tokens = outputs[:, prompt_length:]
    return tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]

Ora passiamo il modello in modalità inferenza e generiamo il testo OCR per la prima immagine di valutazione:

FastVisionModel.for_inference(model)
eval_image = dataset[EVAL_INDEX]["image"]
base_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)

L’output del modello base è leggibile, ma non segue esattamente la struttura di destinazione:

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith

È un buon punto di partenza. Il modello base riesce già a leggere gran parte del documento, ma restituisce il testo in uno stile OCR naturale piuttosto che nel formato strutturato usato nel dataset. Il fine-tuning dovrebbe aiutare ad allineare il modello al formato di destinazione e rendere le risposte più coerenti.

7. Addestrare il modello

Ora che il dataset è nel formato conversazionale visivo corretto, possiamo addestrare il modello usando l’SFTTrainer di TRL con il data collator per visione di Unsloth. 

from unsloth.trainer import UnslothVisionDataCollator
from trl import SFTTrainer, SFTConfig

FastVisionModel.for_training(model)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    data_collator=UnslothVisionDataCollator(model, tokenizer),
    train_dataset=converted_dataset,
    args=SFTConfig(
        per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        warmup_steps=5,
        max_steps=MAX_STEPS,
        learning_rate=LEARNING_RATE,
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.001,
        lr_scheduler_type="linear",
        seed=SEED,
        output_dir=OUTPUT_DIR,
        report_to="none",
        remove_unused_columns=False,
        dataset_text_field="",
        dataset_kwargs={"skip_prepare_dataset": True},
        max_length=MAX_LENGTH,
    ),
)
trainer_stats = trainer.train()

Per prima cosa, passiamo il modello in modalità training con FastVisionModel.for_training(model). Poi creiamo il trainer usando il dataset OCR convertito.

La parte importante qui è l’UnslothVisionDataCollator. Poiché si tratta di un compito visione-linguaggio, il trainer deve gestire correttamente sia le immagini dei documenti medici sia il testo OCR di destinazione. Il collator prepara questi esempi multimodali per il passaggio al modello durante il fine-tuning supervisionato.

Per questa guida, addestriamo per 30 step con batch size per dispositivo pari a 4 e accumulo dei gradienti di 2, per un batch effettivo di 8. Mantiene l’esecuzione leggera, mostrando comunque come il modello inizi ad adattarsi al formato OCR strutturato.

Fine-tuning the Qwen 3.5 4b Vision language model

Durante l’addestramento, Unsloth stampa informazioni utili sulla configurazione, tra cui numero di esempi, step e batch, numero di parametri allenabili e funzionalità di risparmio memoria. In questa esecuzione, Unsloth riporta che il double buffering è abilitato per la backward pass, il che aiuta a ridurre i tempi di attesa durante il gradient checkpointing.

8. Valutare il modello fine-tunato

Dopo l’addestramento, riportiamo il modello in modalità inferenza e generiamo il testo OCR per la stessa immagine di valutazione usata prima del fine-tuning.

FastVisionModel.for_inference(model)
fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)
print("\nFine-tuned output:")
print(fine_tuned_output)

Dopo il fine-tuning, l’output del modello è molto più vicino alla struttura di destinazione del dataset:

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith


Fine-tuned output:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Questo mostra che il modello fine-tunato ha imparato il formato di risposta OCR atteso. Il modello base riusciva già a estrarre la maggior parte del testo visibile, ma il fine-tuning ha aiutato ad allineare l’output al formato strutturato usato nei dati di training.

Possiamo anche testare il modello su un altro esempio del dataset:

EVAL_INDEX_2 = 35
eval_image_2 = dataset[EVAL_INDEX_2]["image"]

fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image_2)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nFine-tuned output:")
print(fine_tuned_output)

Per questo secondo esempio, il modello segue la struttura attesa, ma commette un piccolo errore di OCR generando Amoxicillin invece di Amlodipine:

Target:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amlodipine 20 mg - After meals signature: Dr. C. Rossi </s>

Fine-tuned output:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amoxicillin 20 mg - After meals signature: Dr. C. Rossi </s>

È un promemoria utile: il modello migliora nell’allineamento del formato, ma l’accuratezza OCR dipende ancora da qualità dei dati, nitidezza delle immagini, dimensione del training e numero di step di fine-tuning. Per un sistema OCR in produzione, dovresti addestrare su un dataset più ampio e diversificato e valutare l’accuratezza su una vasta gamma di esempi.

9. Salvare l’adapter fine-tunato

Una volta completato il training, salviamo localmente l’adapter LoRA e il tokenizer.

model.save_pretrained(ADAPTER_DIR)
tokenizer.save_pretrained(ADAPTER_DIR)

print("Saved adapter to:", ADAPTER_DIR)

L’output conferma che l’adapter è stato salvato:

Saved adapter to: qwen35-vision-medical-ocr-lora

Questo salva solo i pesi dell’adapter fine-tunato, non una copia completa del modello base. Più avanti potrai ricaricare il modello base Qwen3.5-4B e applicare questo adapter per riutilizzare il comportamento OCR fine-tunato. In questo modo il modello salvato è leggero e più facile da archiviare, condividere o distribuire.

Considerazioni finali

Il processo di training è stato leggero e pratico su una singola NVIDIA RTX 3090. Anche se il fine-tuning visione-linguaggio è di solito intensivo in memoria, l’esecuzione ha usato molta meno VRAM del previsto. Il picco è stato intorno ai 14 GB, mentre la media è rimasta più vicina ai 9 GB, il che è notevole per il fine-tuning di un modello Qwen3.5 Vision.

Anche l’adattamento del modello è stato rapido. Dopo pochi step di training, l’output è diventato molto più vicino alla struttura OCR di destinazione. Il modello base riusciva già a leggere il documento, ma dopo il fine-tuning ha seguito il formato del dataset in modo più coerente. 

Detto questo, l’esperienza di setup non è stata perfetta. L’installazione di Unsloth ha richiesto diversi tentativi. Può essere difficile da configurare correttamente, soprattutto lavorando tra ambienti locali diversi, ambienti virtuali, versioni CUDA e provider cloud di GPU. 

In alcuni casi, problemi di compatibilità CUDA possono rompere l’ambiente e il debug può richiedere più tempo del previsto. Anche partire da un’immagine Docker di Unsloth su una piattaforma GPU cloud può richiedere tempo se l’ambiente non funziona al primo colpo.

Un’altra lezione importante è che il template del modello conta. Se il dataset non viene convertito nel formato corretto di chat o conversazione visiva, il modello potrebbe non apprendere correttamente. Per Qwen3.5 Vision, usare la corretta struttura messaggi immagine-testo è essenziale. Senza il template giusto, il training può avviarsi, ma il modello potrebbe non adattarsi davvero al compito.

Nel complesso, Unsloth è un’ottima opzione per chi ha accesso limitato alle GPU e vuole fare fine-tuning in modo efficiente su macchine locali o GPU a noleggio. Riduce l’uso di memoria, rende l’hardware più piccolo più utile e può velocizzare la sperimentazione. Tuttavia, per chi effettua spesso fine-tuning e training, la complessità di setup può essere frustrante. Il training basato su Transformers standard è spesso più stabile, più facile da installare e più semplice da riprodurre tra ambienti.

Se è l’attrito dell’installazione a scoraggiarti, ti consiglio di leggere la nostra guida a Unsloth Studio, che mostra come fare fine-tuning di Qwen3.5-9B senza configurazione manuale dell’ambiente nell’interfaccia web locale di Unsloth.


Abid Ali Awan's photo
Author
Abid Ali Awan
LinkedIn
Twitter

In quanto data scientist certificato, sono appassionato di sfruttare tecnologie all’avanguardia per creare applicazioni di machine learning innovative. Con una solida esperienza in riconoscimento vocale, analisi e reportistica dei dati, MLOps, AI conversazionale e NLP, ho affinato le mie competenze nello sviluppo di sistemi intelligenti in grado di avere un impatto concreto. Oltre alla mia expertise tecnica, sono anche un comunicatore efficace, con il talento di rendere chiari e sintetici concetti complessi. Di conseguenza, sono diventato un blogger molto seguito in ambito data science, condividendo idee ed esperienze con una community in crescita di professionisti dei dati. Attualmente mi concentro sulla creazione e sull’editing di contenuti, lavorando con large language model per sviluppare contenuti potenti e coinvolgenti che possano aiutare aziende e singoli a valorizzare al meglio i propri dati.

Argomenti

I migliori corsi di AI

Corso

Introduzione al Deep Learning con PyTorch

4 h
88.5K
Impara a creare la tua prima rete neurale, a regolare gli iperparametri e ad affrontare problemi di classificazione e regressione in PyTorch.
Vedi dettagliRight Arrow
Inizia Il Corso
Mostra altroRight Arrow
Correlato

blog

Tokenizzazione nel NLP: come funziona, sfide e casi d'uso

Guida al preprocessing NLP nel machine learning. Copriamo spaCy, i transformer di Hugging Face e come funziona la tokenizzazione in casi d'uso reali.
Abid Ali Awan's photo

Abid Ali Awan

10 min

blog

I 15 migliori server MCP remoti che ogni AI builder dovrebbe conoscere nel 2026

Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Abid Ali Awan's photo

Abid Ali Awan

15 min

blog

Che cos'è Snowflake? Guida per principianti alla piattaforma dati cloud

Esplora le basi di Snowflake, la piattaforma dati cloud. Scopri la sua architettura, le sue funzionalità e come integrarla nelle tue pipeline di dati.
Tim Lu's photo

Tim Lu

12 min

Mostra AltroMostra Altro