Hoppa till huvudinnehållet

Hur du finjusterar DiffusionGemma på PubMedQA med Unsloth

Lär dig att finjustera DiffusionGemma 26B-A4B för biomedicinsk frågesvar på PubMedQA med Unsloth, LoRA, Hugging Face och en RunPod H100-GPU.
Uppdaterad 3 aug. 2026  · 9 min läsa

Utforska med AI

Öppna i ChatGPTÖppna i ClaudeÖppna i Perplexity

DiffusionGemma är en experimentell språkmodell från Google DeepMind som genererar text på ett annat sätt än traditionella stora språkmodeller. I stället för att förutsäga en token i taget från vänster till höger börjar den med en fast duk av brusiga token och förfinar dem gradvis genom flera avbrusningssteg. Det gör att modellen kan uppdatera flera tokenpositioner parallellt och revidera delar av sitt svar under genereringen.

I den här guiden kommer vi att finjustera diffusiongemma-26B-A4B-it på datasetet PubMedQA med en NVIDIA H100-GPU. Modellen får en biomedicinsk fråga och ett stödjande sammanhang och förutspår sedan yes, no eller maybe. Vi kommer att förbereda datan, träna en LoRA-adapter, utvärdera modellen före och efter finjustering och ladda upp den slutliga adaptern till Hugging Face.

Jag har också publicerat den kompletta notebooken så att du kan granska originalkoden, följa med och köra experimentet själv.

Obs: Detta projekt är endast för lärande och experiment och ska inte användas för riktiga medicinska beslut.

1. Öppna en RunPod Jupyter Notebook

Skapa en ny RunPod-pod med en NVIDIA H100-GPU och välj en PyTorch/Jupyter-mall. Konfigurera minst 100 GB beständig lagring så att dina modellfiler och träningsutdata inte går förlorade när poden stoppas.

Lägg till din Hugging Face-åtkomsttoken som en miljövariabel:

HF_TOKEN=your_hugging_face_token

Editing the Runpod Pytorch template

Detta gör att modeller och dataset kan laddas ner snabbare och låter dig ladda upp den sparade LoRA-adaptern till Hugging Face utan att logga in manuellt från notebooken.

Den konfigurerade poden bör kosta cirka 3 dollar per timme, men det slutliga priset kan variera beroende på GPU-tillgänglighet och vald podtyp.

Runpod H100 GPU summary

När poden körs öppnar du JupyterLab eller Jupyter Notebook från RunPod-gränssnittet och skapar en ny notebook med namnet diffusiongemma_pubmedqa.ipynb.

2. Installera nödvändiga paket

Kör följande kommandon i den första notebook-cellen för att installera Unsloth och de bibliotek som krävs för att ladda, finjustera och spara DiffusionGemma.

%%capture
%pip install --upgrade pip wheel setuptools packaging ninja
%pip install unsloth
%pip install --no-deps --upgrade --force-reinstall git+https://github.com/unslothai/unsloth-zoo.git git+https://github.com/unslothai/unsloth.git
%pip install sentencepiece protobuf "datasets==4.3.0" "huggingface_hub>=0.34.0" hf_transfer
%pip install --no-deps bitsandbytes accelerate peft trl triton
%pip install --no-deps --upgrade "torchao>=0.16.0"
%pip install --no-deps transformers==5.11.0 "tokenizers>=0.22.0,<=0.23.0"

Kommandot %%capture döljer den långa installationsutskriften. Paketversionerna är låsta för att undvika kompatibilitetsproblem mellan DiffusionGemma, Transformers, Unsloth och träningsbiblioteken.

När installationen är klar, starta om notebook-kärnan innan du fortsätter.

3. Importera biblioteken

Importera biblioteken som krävs för datapreparering, modellinläsning, träning och utvärdering.

import copy
import os
import random
import time

import torch
from datasets import load_dataset
from unsloth import FastModel

os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
torch._dynamo.config.recompile_limit = 64

print("Torch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())
print(
    "GPU:",
    torch.cuda.get_device_name(0)
    if torch.cuda.is_available()
    else "None",
)

HF_HUB_ENABLE_HF_TRANSFER aktiverar snabbare nedladdningar från Hugging Face Hub, medan en högre gräns för Dynamo-ompilering hjälper till att förhindra avbrott när du arbetar med modellen.

När cellen körs korrekt kommer Unsloth att patcha träningsmiljön, och utskriften ska bekräfta att CUDA är tillgängligt och att H100-GPU:n har upptäckts.

🦥 Unsloth: Will patch your computer to enable 2x faster free fine-tuning.
🦥 Unsloth Zoo will now patch everything to make training faster!
Torch: 2.10.0+cu128
CUDA available: True
GPU: NVIDIA H100 80GB HBM3

4. Ställ in konfigurationen

Definiera modellen, dataset, träningsparametrar, utvärderingsinställningar och utmatningskatalog på ett ställe.

MODEL_NAME = "unsloth/diffusiongemma-26B-A4B-it"
DATASET_NAME = "qiaojin/PubMedQA"

TRAIN_SUBSET = "pqa_artificial"
EVAL_SUBSET = "pqa_labeled"

N_TRAIN = 3000
N_EVAL = 200

MAX_CONTEXT_CHARS = 2500

STEPS = 60
GRAD_ACCUM = 4
LR = 1e-4
T_LO = 0.1

EVAL_TOTAL = 50
EVAL_DENOISING_STEPS = 16

OUTPUT_DIR = "diffusiongemma_pubmedqa_lora"

Vi kommer att använda 3 000 artificiella exempel för träning och 200 manuellt märkta exempel för utvärdering. För att hålla experimentet snabbt tränas modellen i 60 steg och utvärderar 50 exempel med 16 avbrusningssteg.

Hur DiffusionGemma fungerar

Innan vi laddar modellen är det värt att föreställa sig hur DiffusionGemma faktiskt producerar ett svar. I stället för att skriva token efter token börjar den med en duk av fast längd och förfinar dem över flera avbrusningssteg, uppdaterar många positioner samtidigt tills texten landar i ett sammanhängande svar. 

Koden nedan rapporterar en duklängd på 256, storleken på ett enda block. För våra korta yes/no/maybe-svar räcker en duk mer än väl, medan längre utdata genereras genom att kedja dukar block för block. Diagrammet nedan visar denna förfiningsprocess på hög nivå:

Text Diffusion Explanation

5. Ladda DiffusionGemma

Ladda den instruktionsfintrimmade DiffusionGemma-modellen i bfloat16-precision. Vi använder inte 4-bitars kvantisering eftersom H100-GPU:n har tillräckligt med minne för att ladda modellen i högre precision.

model, tokenizer = FastModel.from_pretrained(
    model_name=MODEL_NAME,
    dtype=torch.bfloat16,
    load_in_4bit=False,
)

processor = tokenizer
tok = processor.tokenizer if hasattr(processor, "tokenizer") else processor

vocab = model.config.text_config.vocab_size
canvas_len = model.config.canvas_length

dev = next(
    (p.device for p in model.parameters() if p.device.type != "meta"),
    torch.device("cuda"),
)

print("Vocab size:", vocab)
print("Canvas length:", canvas_len)
print("Model device:", dev)

Ordförrådsstorleken används när slumpmässigt brus läggs till under diffusionsträningen. Duklängden avgör det maximala antalet token som modellen kan förfina i ett enda generationsblock.

Du bör se en utskrift liknande:

Vocab size: 262144
Canvas length: 256
Model device: cuda:0

6. Lägg till en LoRA-adapter

Lägg till en LoRA-adapter så att endast en liten mängd extra parametrar tränas i stället för att uppdatera hela modellen med 26 miljarder parametrar.

model = FastModel.get_peft_model(
    model,
    r=64,
    lora_alpha=128,
    use_gradient_checkpointing=False,
)

Detta minskar avsevärt minnes- och beräkningskraven för finjusteringen. Gradientcheckpointing är avaktiverat eftersom H100 har tillräckligt GPU-minne för detta experiment.

7. Ladda PubMedQA

Ladda den artificiella PubMedQA-delmängden för träning och den manuellt märkta delmängden för utvärdering.

train_data = load_dataset(
    DATASET_NAME,
    TRAIN_SUBSET,
    split="train",
)

eval_data = load_dataset(
    DATASET_NAME,
    EVAL_SUBSET,
    split="train",
)

print("Train size:", len(train_data))
print("Eval size:", len(eval_data))
print(train_data[0])

Träningsdelmängden innehåller automatiskt genererade exempel, medan utvärderingsdelmängden innehåller expertmärkta biomedicinska frågor. 

Genom att skriva ut den första raden kan vi granska frågan, sammanhanget (abstract) och det slutliga beslutet innan vi formaterar datan.

Du bör se:

Train size: 211269
Eval size: 1000

Varje exempel innehåller en biomedicinsk fråga, ett eller flera stödjande stycken ur abstract och ett slutligt svar: yes, no eller maybe.

Loading the PubMedQA

8. Konvertera datasetet

Konvertera varje PubMedQA-exempel till ett chattformat med en användarprompt och ett assistentsvar.

def make_prompt(row):
    context = " ".join(row["context"]["contexts"])
    context = context[:MAX_CONTEXT_CHARS]
    question = row["question"]

    return f"""Answer the biomedical research question using only the context.

Context:
{context}

Question:
{question}

Answer with only one word: yes, no, or maybe."""


def make_answer(row):
    return row["final_decision"].strip().lower()


def convert_row(row):
    answer = make_answer(row)

    if answer not in ["yes", "no", "maybe"]:
        return None

    return {
        "messages": [
            {"role": "user", "content": make_prompt(row)},
            {"role": "assistant", "content": answer},
        ]
    }


train_rows = []

for row in train_data.select(range(N_TRAIN)):
    item = convert_row(row)

    if item is not None:
        train_rows.append(item)


eval_rows = []

for row in eval_data.select(range(N_EVAL)):
    item = convert_row(row)

    if item is not None:
        eval_rows.append(item)


print("Prepared train examples:", len(train_rows))
print("Prepared eval examples:", len(eval_rows))
print(train_rows[0]["messages"][0]["content"])
print("Answer:", train_rows[0]["messages"][1]["content"])

Sammanhangsstyckena kombineras till en enda sträng och begränsas till 2 500 tecken för att hålla inmatningen hanterbar. Varje svar konverteras till gemener, och exempel med etiketter utanför yes, no eller maybe tas bort.

Genom att skriva ut det första konverterade exemplet kan vi bekräfta att sammanhang, fråga och svar har formaterats korrekt innan träningen.

Formatting the PubMedQA dataset in chat format

9. Bygg diffusions-tränings-exemplen

DiffusionGemma kräver att målsvaret placeras i en duk med fast längd. Denna funktion tokeniserar prompten, konverterar svaret till token-ID:n, fyller ut till modellens duklängd och skapar en mask som visar vilka token som ska bidra till förlusten.

eos = model.generation_config.eos_token_id or [1]
eos = eos[0] if isinstance(eos, (list, tuple)) else eos

pad = tok.pad_token_id if tok.pad_token_id is not None else eos


def build_examples(rows):
    examples = []

    for row in rows:
        user_message = row["messages"][0]
        assistant_message = row["messages"][1]

        prompt_ids = processor.apply_chat_template(
            [user_message],
            tokenize=True,
            add_generation_prompt=True,
            return_tensors="pt",
        )[0]

        answer_ids = tok.encode(
            assistant_message["content"],
            add_special_tokens=False,
        )

        content = answer_ids + [eos]
        n = len(content)

        if n > canvas_len:
            continue

        x0 = torch.tensor(
            content + [pad] * (canvas_len - n),
            dtype=torch.long,
        )

        loss_mask = torch.zeros(canvas_len, dtype=torch.bool)
        loss_mask[:n] = True

        examples.append((prompt_ids, x0, loss_mask))

    return examples


examples = build_examples(train_rows)

Slutsekvens-token läggs till efter varje svar, medan återstående dukpositioner fylls med utfyllnadstoken. Förlustmasken säkerställer att träningen fokuserar endast på svaren och slutsekvens-token i stället för de utfyllda positionerna.

10. Skapa inferens- och utvärderingsfunktionerna

Definiera sedan funktionerna som används för att generera svar, rensa modellens utdata och beräkna utvärderingsnoggrannheten.

Generera ett svar

Funktionen answer_question() formaterar prompten, genererar ett svar genom flera avbrusningssteg och avkodar de genererade token till text.

def answer_question(prompt, steps=64):
    input_ids = processor.apply_chat_template(
        [{"role": "user", "content": prompt}],
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt",
    ).to(dev)

    gen_config = copy.deepcopy(model.generation_config)
    gen_config.max_denoising_steps = steps
    gen_config.max_new_tokens = canvas_len

    model.eval()

    with torch.no_grad():
        output = model.generate(
            input_ids=input_ids,
            generation_config=gen_config,
        )

    generated = output.sequences[0, input_ids.shape[1]:]
    text = tok.decode(
        generated.tolist(),
        skip_special_tokens=True,
    )

    return text.strip().lower()

Extrahera prediktionen

Även om prompten begär ett enstaka ord kan modellen ibland generera ytterligare text. Den här funktionen extraherar den första giltiga prediktionen av yes, no eller maybe.

def clean_prediction(text):
    text = text.lower().strip()

    if text.startswith("yes"):
        return "yes"

    if text.startswith("no"):
        return "no"

    if text.startswith("maybe"):
        return "maybe"

    words = text.replace(".", " ").replace(",", " ").split()

    for word in words:
        if word in ["yes", "no", "maybe"]:
            return word

    return "unknown"

Utvärdera noggrannheten

Utvärderingsfunktionen jämför varje rensad prediktion med det korrekta svaret, skriver ut resultatet för varje exempel och returnerar den övergripande noggrannheten tillsammans med de individuella prediktionerna.

def evaluate_model(
    rows,
    total=50,
    steps=64,
    title="Evaluation",
):
    correct = 0
    results = []
    total = min(total, len(rows))

    print(title)
    print("-" * len(title))

    for i, row in enumerate(rows[:total], start=1):
        prompt = row["messages"][0]["content"]
        gold = row["messages"][1]["content"]

        raw_pred = answer_question(prompt, steps=steps)
        pred = clean_prediction(raw_pred)

        is_correct = pred == gold
        correct += int(is_correct)

        results.append({
            "index": i,
            "gold": gold,
            "prediction": pred,
            "raw_prediction": raw_pred,
            "correct": is_correct,
        })

        print(
            f"{i:02d}. Gold: {gold} | "
            f"Pred: {pred} | Correct: {is_correct}"
        )

    accuracy = correct / total if total else 0

    print()
    print("Accuracy:", accuracy)
    print()

    return {
        "accuracy": accuracy,
        "correct": correct,
        "total": total,
        "results": results,
    }

11. Utvärdera modellen före finjustering

Kör utvärderingen före träning för att fastställa en baslinje.

before_eval = evaluate_model(
    eval_rows,
    total=EVAL_TOTAL,
    steps=EVAL_DENOISING_STEPS,
    title="Before Fine-Tuning Evaluation",
)

Detta utvärderar 50 exempel med 16 avbrusningssteg per svar. I det här experimentet besvarade basmodellen 30 av 50 frågor korrekt.

Baseline result of the DiffusionGemma

Denna baslinje kommer senare att jämföras med modellens noggrannhet efter finjustering.

12. Konfigurera träningen

Växla modellen till träningsläge, skapa optimeraren och inlärningshastighetsschemat och definiera hur rena svarstoken ska korrumperas under diffusionsträningen.

model.config.use_cache = True
model.train()

opt = torch.optim.AdamW(
    [p for p in model.parameters() if p.requires_grad],
    lr=LR,
    betas=(0.9, 0.95),
    weight_decay=0.0,
)

sched = torch.optim.lr_scheduler.OneCycleLR(
    opt,
    max_lr=LR,
    total_steps=STEPS,
    pct_start=0.03,
    anneal_strategy="cos",
)

Endast parametrar med requires_grad=True skickas till optimeraren, vilket innebär att träningsprocessen uppdaterar LoRA-adaptern i stället för hela modellen.

Skapa sedan en korrumperingsfunktion som ersätter en slumpmässig andel av svarsduken med slumpmässiga token.

def corrupt(x0):
    noise_level = random.uniform(T_LO, 1.0)

    xt = x0.to(dev).clone()

    noise_mask = (
        torch.rand(canvas_len, device=dev) < noise_level
    )

    xt[noise_mask] = torch.randint(
        0,
        vocab,
        (canvas_len,),
        device=dev,
    )[noise_mask]

    return xt.unsqueeze(0)

Mängden brus ändras för varje exempel. Under träningen lär sig modellen att återskapa det ursprungliga svaret från dessa korrumperade duk-token.

13. Träna modellen

Följande loop tränar LoRA-adaptern i 60 steg med gradientackumulering.

order = list(range(len(examples)))
ptr = 0
start_time = time.time()

opt.zero_grad(set_to_none=True)

for step in range(1, STEPS + 1):
    step_loss = 0.0

    for _ in range(GRAD_ACCUM):
        if ptr >= len(order):
            random.shuffle(order)
            ptr = 0

        prompt_ids, x0, loss_mask = examples[order[ptr]]
        ptr += 1

        output = model(
            input_ids=prompt_ids.unsqueeze(0).to(dev),
            canvas_ids=corrupt(x0),
            self_conditioning_logits=None,
        )

        logits = output.logits[0].float()
        mask = loss_mask.to(dev)

        loss = torch.nn.functional.cross_entropy(
            logits[mask],
            x0.to(dev)[mask],
        )

        (loss / GRAD_ACCUM).backward()
        step_loss += loss.item() / GRAD_ACCUM

    torch.nn.utils.clip_grad_norm_(
        [
            p
            for p in model.parameters()
            if p.requires_grad
        ],
        1.0,
    )

    opt.step()
    sched.step()
    opt.zero_grad(set_to_none=True)

    if step % 20 == 0:
        elapsed = time.time() - start_time

        print(
            f"step {step}/{STEPS} | "
            f"loss {step_loss:.4f} | "
            f"{elapsed:.0f}s"
        )

För varje träningsexempel får modellen biomedicinsk prompt och en korrumperad svarstduk. Korsentropiförlust beräknas endast för de verkliga svarstoken som valts av förlustmasken.

Gradientackumulering kombinerar fyra exempel innan modellen uppdateras. Gradientklippning tillämpas också för att hålla träningen stabil.

I det här experimentet slutfördes träningen på ungefär två minuter:

step 20/60 | loss 0.0019 | 43s
step 40/60 | loss 0.0003 | 85s
step 60/60 | loss 0.0001 | 126s

Den stadigt sjunkande förlusten indikerar att adaptern lär sig återskapa de förväntade svaren från den korrumperade duken. Under träningen kan du också köra nvidia-smi i RunPod-terminalen för att övervaka GPU-minnesanvändning och utnyttjande.

Nvidia model statistic while model training

14. Utvärdera den finjusterade modellen

Kör samma utvärdering igen efter träningen för att mäta om finjusteringen förbättrade modellens prestanda.

after_eval = evaluate_model(
    eval_rows,
    total=EVAL_TOTAL,
    steps=EVAL_DENOISING_STEPS,
    title="After Fine-Tuning Evaluation",
)

Den finjusterade modellen utvärderas på samma 50 exempel och med samma 16 avbrusningssteg som i baslinjeutvärderingen.

Fine-tune result of the DiffusionGemma

Jämför sedan noggrannheten före och efter finjustering.

before_accuracy = before_eval["accuracy"]
after_accuracy = after_eval["accuracy"]
improvement = after_accuracy - before_accuracy

print("Before fine-tuning accuracy:", before_accuracy)
print("After fine-tuning accuracy:", after_accuracy)
print("Improvement:", improvement)
Before fine-tuning accuracy: 0.6
After fine-tuning accuracy: 0.8
Improvement: 0.2

I det här experimentet ökade modellens noggrannhet från 0,60 till 0,80.

Det motsvarar en förbättring med 20 procentenheter, där modellen svarade rätt på 40 av 50 frågor efter finjusteringen, jämfört med 30 av 50 före träning.

15. Spara och ladda upp den finjusterade adaptern

Spara den tränade LoRA-adaptern och processorfilerna till den utmatningskatalog som definierades tidigare.

model.save_pretrained(OUTPUT_DIR)
processor.save_pretrained(OUTPUT_DIR)

print(f"Saved LoRA adapter to: {OUTPUT_DIR}")

Du bör se:

Saved LoRA adapter to: diffusiongemma_pubmedqa_lora

Detta sparar endast den lätta LoRA-adaptern i stället för ytterligare en komplett kopia av basmodellen med 26 miljarder parametrar.

Ladda sedan upp adaptern och processorfilerna till Hugging Face Hub:

REPO_ID = "kingabzpro/diffusiongemma_pubmedqa"

model.push_to_hub(REPO_ID)
processor.push_to_hub(REPO_ID)

Eftersom miljövariabeln HF_TOKEN lades till under konfigurationen av RunPod-poden bör Hugging Face autentisera automatiskt. Du behöver bara köra notebook_login() när token inte redan är konfigurerad:

from huggingface_hub import notebook_login

notebook_login()

När uppladdningen är klar kommer lagringsplatsen att innehålla LoRA-adaptern och processorkonfigurationen som krävs för att ladda den finjusterade modellen senare.

Finetuned lora on Hugging face: diffusiongemma_pubmedqa

Källa: kingabzpro/diffusiongemma_pubmedqa · Hugging Face 

Avslutande tankar

Att finjustera DiffusionGemma med Unsloth var förvånansvärt enkelt. Det mest tidskrävande var att installera rätt beroenden och förstå hur den diffusionsspecifika träningsprocessen fungerar. När miljön väl var på plats gick det mycket smidigt att ladda modellen, träna LoRA-adaptern, utvärdera den och ladda upp resultaten till Hugging Face.

Jag tyckte DiffusionGemma var särskilt intressant eftersom den inte genererar text en token i taget som en traditionell språkmodell. I stället arbetar den med en fast duk och förfinar brusiga token genom avbrusningssteg. Att förstå denna annorlunda generationsprocess och finjustera den för en biomedicinsk frågesvarsuppgift gjorde experimentet särskilt lärorikt för mig.

Även med en liten uppsättning steg ökade noggrannheten på urvalet med 50 exempel från 0,60 till 0,80—även om felmarginalen är stor på en sådan storlek, och en "alltid yes"-baslinje når redan cirka 55 % på detta utvärderingsset. 

Det är också värt att notera att den artificiella träningsdelmängden innehåller nästan inga "maybe"-etiketter, så modellen har liten chans att lära sig den klassen även om den förekommer i utvärderingsdatan. Se detta som ett snabbt experiment i hur finjusteringsprocessen fungerar, inte som bevis på att modellen är redo för verklig medicinsk användning.

Redo att gå bortom en enskild finjusteringskörning? Vår Developing Large Language Models-väg spänner från grunderna i PyTorch och transformers till att bygga och driftsätta egna LLM:er.

Ämnen

Toppkurser om LLM

track

Utveckla stora språkmodeller

16 timmar
Lär dig utveckla stora språkmodeller (LLM:er) med PyTorch och Hugging Face, med hjälp av de senaste teknikerna inom djupinlärning och NLP.
Se detaljerRight Arrow
Starta Kursen
Se merRight Arrow