Track
DiffusionGemma — экспериментальная языковая модель Google DeepMind, которая генерирует текст иначе, чем традиционные большие языковые модели. Вместо предсказания по одному токену слева направо она начинает с фиксированного холста из зашумлённых токенов и постепенно уточняет их через несколько шагов денойзинга. Это позволяет модели параллельно обновлять сразу несколько позиций токенов и пересматривать части ответа в процессе генерации.
В этом руководстве мы будем дообучать diffusiongemma-26B-A4B-it на наборе данных PubMedQA с использованием NVIDIA H100 GPU. Модель будет получать биомедицинский вопрос и поддерживающий контекст, а затем предсказывать yes, no или maybe. Мы подготовим данные, обучим адаптер LoRA, оценим модель до и после дообучения и загрузим финальный адаптер на Hugging Face.
Я также опубликовал полноценный ноутбук, чтобы вы могли просмотреть исходный код, повторить шаги и запустить эксперимент самостоятельно.
Примечание: Этот проект предназначен исключительно для обучения и экспериментов и не должен использоваться для реальных медицинских решений.
1. Откройте Jupyter Notebook на RunPod
Создайте новый pod на RunPod с GPU NVIDIA H100 и выберите шаблон PyTorch/Jupyter. Настройте как минимум 100 ГБ постоянного хранилища, чтобы файлы модели и результаты обучения не потерялись при остановке pod.
Добавьте ваш токен доступа Hugging Face в качестве переменной окружения:
HF_TOKEN=your_hugging_face_token

Это позволит быстрее загружать модели и датасеты и даст возможность загрузить сохранённый адаптер LoRA на Hugging Face без ручного входа из ноутбука.
Сконфигурированный pod должен стоить приблизительно $3 в час, однако финальная цена может варьироваться в зависимости от доступности GPU и выбранного типа pod.

Когда pod запустится, откройте JupyterLab или Jupyter Notebook из интерфейса RunPod и создайте новый ноутбук с именем diffusiongemma_pubmedqa.ipynb.
2. Установите необходимые пакеты
Выполните следующие команды в первой ячейке ноутбука, чтобы установить Unsloth и библиотеки, необходимые для загрузки, дообучения и сохранения 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"
Команда %%capture скрывает длинный вывод установки. Версии пакетов зафиксированы, чтобы избежать проблем совместимости между DiffusionGemma, Transformers, Unsloth и библиотеками обучения.
После завершения установки перезапустите ядро ноутбука, прежде чем продолжить.
3. Импортируйте библиотеки
Импортируйте библиотеки, необходимые для подготовки датасета, загрузки модели, обучения и оценки.
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 включает более быстрые загрузки из Hugging Face Hub, а увеличение лимита перекомпиляций в Dynamo помогает избежать прерываний при работе с моделью.
При успешном выполнении ячейки Unsloth пропатчит среду обучения, и в выводе будет подтверждение, что CUDA доступна и обнаружен GPU H100.
🦥 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. Задайте конфигурацию
Определите модель, датасет, параметры обучения, настройки оценки и выходной каталог в одном месте.
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"
Мы будем использовать 3000 искусственных примеров для обучения и 200 вручную размеченных примеров для оценки. Чтобы эксперимент прошёл быстро, модель будет обучаться 60 шагов и оцениваться на 50 примерах с 16 шагами денойзинга.
Как работает DiffusionGemma
Прежде чем загружать модель, стоит представить, как DiffusionGemma фактически формирует ответ. Вместо записи токенов по одному она начинает с холста фиксированной длины и уточняет его за несколько шагов денойзинга, обновляя множество позиций сразу, пока текст не сложится в связный ответ.
Код ниже сообщает длину холста 256 — размер одного блока. Для наших коротких ответов yes/no/maybe одного холста более чем достаточно, а более длинные ответы генерируются последовательным объединением холстов блок за блоком. Диаграмма ниже на высоком уровне показывает этот процесс уточнения:

5. Загрузите DiffusionGemma
Загрузите модель DiffusionGemma, дообученную на инструкциях, с точностью bfloat16. Мы не будем использовать 4-битную квантизацию, поскольку у H100 достаточно памяти для загрузки модели с более высокой точностью.
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)
Размер словаря используется при добавлении случайного шума во время обучения диффузии. Длина холста определяет максимальное число токенов, которые модель может уточнять в одном блоке генерации.
Вы должны увидеть вывод, похожий на:
Vocab size: 262144
Canvas length: 256
Model device: cuda:0
6. Добавьте адаптер LoRA
Добавьте адаптер LoRA, чтобы обучать лишь небольшой набор дополнительных параметров вместо обновления всей модели на 26 миллиардов параметров.
model = FastModel.get_peft_model(
model,
r=64,
lora_alpha=128,
use_gradient_checkpointing=False,
)
Это значительно снижает потребление памяти и вычислительные затраты на дообучение. Контрольные точки градиента отключены, поскольку у H100 достаточно памяти для этого эксперимента.
7. Загрузите PubMedQA
Загрузите искусственное подмножество PubMedQA для обучения и вручную размеченное подмножество для оценки.
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])
Обучающее подмножество содержит автоматически сгенерированные примеры, а в оценочном подмножестве — экспертом размеченные биомедицинские вопросы.
Печать первой строки позволяет изучить вопрос, контекст аннотации и финальное решение перед форматированием данных.
Вы должны увидеть:
Train size: 211269
Eval size: 1000
Каждый пример содержит биомедицинский вопрос, один или несколько поддерживающих фрагментов аннотаций и финальный ответ yes, no или maybe.

8. Преобразуйте датасет
Преобразуйте каждый пример PubMedQA в формат чата, содержащий подсказку пользователя и ответ ассистента.
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"])
Контекстные фрагменты объединяются в одну строку и ограничиваются 2500 символами, чтобы вход оставался управляемым. Каждый ответ приводится к нижнему регистру, а примеры с метками вне yes, no или maybe удаляются.
Печать первого преобразованного примера помогает убедиться, что контекст, вопрос и ответ корректно отформатированы перед обучением.

9. Постройте примеры для обучения диффузии
DiffusionGemma требует размещать целевой ответ внутри холста фиксированной длины. Эта функция токенизирует подсказку, конвертирует ответ в ID токенов, дополняет до длины холста модели и создаёт маску, показывающую, какие токены должны участвовать в расчёте функции потерь.
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)
Токен конца последовательности добавляется после каждого ответа, а оставшиеся позиции холста заполняются паддинг-токенами. Маска потерь гарантирует, что обучение фокусируется только на токенах ответа и конце последовательности, а не на заполнителях.
10. Создайте функции инференса и оценки
Далее определите функции для генерации ответов, очистки вывода модели и расчёта точности оценки.
Сгенерируйте ответ
Функция answer_question() форматирует подсказку, генерирует ответ через несколько шагов денойзинга и декодирует сгенерированные токены в текст.
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()
Извлеките предсказание
Хотя подсказка просит однословный ответ, иногда модель может сгенерировать дополнительный текст. Эта функция извлекает первое допустимое предсказание yes, no или 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"
Оцените точность
Функция оценки сравнивает каждое очищенное предсказание с правильным ответом, печатает результат для каждого примера и возвращает общую точность вместе с индивидуальными предсказаниями.
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. Оцените модель до дообучения
Запустите оценку до обучения, чтобы зафиксировать базовый уровень.
before_eval = evaluate_model(
eval_rows,
total=EVAL_TOTAL,
steps=EVAL_DENOISING_STEPS,
title="Before Fine-Tuning Evaluation",
)
Это оценивает 50 примеров с 16 шагами денойзинга на ответ. В этом эксперименте базовая модель правильно ответила на 30 из 50 вопросов.

Позже этот базовый уровень будет сопоставлен с точностью модели после дообучения.
12. Настройте обучение
Переключите модель в режим обучения, создайте оптимизатор и шедулер скорости обучения и определите, как чистые токены ответа будут искажаться во время обучения диффузии.
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",
)
В оптимизатор передаются только параметры с requires_grad=True, что означает, что в процессе обучения обновляется адаптер LoRA, а не вся модель.
Далее создайте функцию искажения, которая заменяет случайную долю холста ответа случайными токенами.
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)
Количество шума меняется для каждого примера. Во время обучения модель учится восстанавливать исходный ответ из этих искажённых токенов холста.
13. Обучите модель
Следующий цикл обучает адаптер LoRA в течение 60 шагов с накоплением градиента.
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"
)
Для каждого обучающего примера модель получает биомедицинскую подсказку и искажённый холст ответа. Функция потерь кросс-энтропии вычисляется только для настоящих токенов ответа, выбранных маской потерь.
Накопление градиента объединяет четыре примера перед обновлением модели. Также применяется обрезка градиента для стабильности обучения.
В этом эксперименте обучение заняло примерно две минуты:
step 20/60 | loss 0.0019 | 43s
step 40/60 | loss 0.0003 | 85s
step 60/60 | loss 0.0001 | 126s
Поступательное снижение лосса указывает, что адаптер учится восстанавливать ожидаемые ответы из искажённого холста. Во время обучения вы также можете запустить nvidia-smi в терминале RunPod, чтобы мониторить использование и загрузку памяти GPU.

14. Оцените дообученную модель
Запустите ту же оценку после обучения, чтобы измерить, улучшилась ли производительность модели.
after_eval = evaluate_model(
eval_rows,
total=EVAL_TOTAL,
steps=EVAL_DENOISING_STEPS,
title="After Fine-Tuning Evaluation",
)
Дообученная модель оценивается на тех же 50 примерах и с теми же 16 шагами денойзинга, что и в базовой оценке.

Далее сравните точность до и после дообучения.
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
В этом эксперименте точность модели выросла с 0.60 до 0.80.
Это соответствует улучшению на 20 процентных пунктов: после дообучения модель правильно ответила на 40 из 50 вопросов, по сравнению с 30 из 50 до обучения.
15. Сохраните и загрузите дообученный адаптер
Сохраните обученный адаптер LoRA и файлы процессора в ранее определённый выходной каталог.
model.save_pretrained(OUTPUT_DIR)
processor.save_pretrained(OUTPUT_DIR)
print(f"Saved LoRA adapter to: {OUTPUT_DIR}")
Вы должны увидеть:
Saved LoRA adapter to: diffusiongemma_pubmedqa_lora
Сохраняется только лёгкий адаптер LoRA, а не ещё одна полная копия базовой модели на 26 миллиардов параметров.
Затем загрузите файлы адаптера и процессора в Hugging Face Hub:
REPO_ID = "kingabzpro/diffusiongemma_pubmedqa"
model.push_to_hub(REPO_ID)
processor.push_to_hub(REPO_ID)
Поскольку переменная окружения HF_TOKEN была добавлена при конфигурации pod на RunPod, аутентификация в Hugging Face должна пройти автоматически. Вам нужно вызывать notebook_login() только если токен ещё не был настроен:
from huggingface_hub import notebook_login
notebook_login()
После завершения загрузки репозиторий будет содержать адаптер LoRA и конфигурацию процессора, необходимые для последующей загрузки дообученной модели.

Источник: kingabzpro/diffusiongemma_pubmedqa · Hugging Face
Заключение
Дообучать DiffusionGemma с Unsloth оказалось на удивление просто. Больше всего времени заняла установка корректных зависимостей и понимание специфики диффузионного процесса обучения. Как только среда была настроена, загрузка модели, обучение адаптера LoRA, его оценка и загрузка результатов на Hugging Face прошли очень гладко.
DiffusionGemma показалась особенно интересной, поскольку она не генерирует текст по одному токену, как традиционная языковая модель. Вместо этого она работает с фиксированным холстом и постепенно уточняет зашумлённые токены через шаги денойзинга. Понимание этого иного процесса генерации и его дообучение на задаче биомедицинского вопросно-ответного взаимодействия сделали эксперимент особенно ценным для меня.
Даже в небольших настройках точность на выборке из 50 примеров выросла с 0.60 до 0.80 — хотя при таком размере выборки погрешность велика, и базовая стратегия «всегда yes» уже даёт около 55% на этом наборе для оценки.
Также стоит отметить, что искусственное обучающее подмножество почти не содержит меток «maybe», поэтому у модели мало шансов выучить этот класс, хотя он встречается в оценочных данных. Рассматривайте это как быстрый эксперимент по работе процесса дообучения, а не как подтверждение готовности модели к реальному медицинскому применению.
Готовы пойти дальше одного запуска дообучения? Наш трек Developing Large Language Models проведёт вас от основ PyTorch и transformers к созданию и развёртыванию собственных LLM.