Дэбаггінг малога GPT у PyTorch: тэсты, якія ізольуюць кожную прычыну аблучэння.
Этапна схема роботы для дэбаггінгу GPT на рывень симвалаў у PyTorch – ад ідэнтаў токенав і змены цялевых значэнняў да градыентаў, значэнняў NaN і пунктав перапачатку.
Паракаменты ацэнкі, такія як пазырок, перплексітэт і частка павтарэння, парадкуюць, што маленькая модель GPT працуе некоректна, але рэдкасцю парадкуюць чаму. Пастойны спосаб — змяніць шчыльнасць навучэння, дадаць ўтварэнне і спакуяцца. Шырокі падход заменяе гаданні на повторнае выконвальнае раштрырэнне для кампактной моделі GPT на рэвэле характераў (Mini-GPT), навучанай на WikiText-2: прыяжджае кожны сімптам да верагодных прычын, адбываецца цялевая пераканальванне на кожны ўрадок процесу і выправляюцься проблемы па той порядку, як яны выяўляюцца. У канцы вы отрымваеце набор тэзаў і адну скрыпту дыягностикі, якая трэба запускаць перад кожным дорогім процесам навучэння.
Чаму пракцес GPT можа быць некоректны без збою
Навучэнне моделі мовы аб’едначае багато трансфармацый, кожная з якіх выкарыстоўвае выход пярэдней:
Raw dataset
→ cleaned text
→ tokenizer
→ token IDs
→ training batches
→ embeddings
→ Transformer blocks
→ vocabulary logits
→ cross-entropy loss
→ gradients
→ optimizer
→ checkpoints
→ generation
Дыфект у будзь-ям месцы заражае ўсё, што знаходзіцца далей. Падазроўваю, што цялевыя даны не адстаюць ад вхідных на адну пазыцыю:
Input: The cat
Target: The cat
Тэлерыце зараз адзначаюцца за тое, што воспамінаюць токен, які ўжо бачылі, замест таго, каб працавалі над прыказваннем наступнага. Нічога не выклікае памылак, і збыткі можа ўсё ж зменшыцца, адтакуль копіюванне є простым. Модель проста оптымаізуе некалькіць правільную мэту. Гэта і є ключовая разліка з адлагоджаннем веб-сэрвіса, дзе некалькіць правільная значэнне зворотнага выходу зазвычай спрачынае памылку тэста або сторанкі:
Код, який выконваецца да канца, все рава можа трэнаваць несправную модель.
Работайце ад пачатку каналу обработкі дадзеных да канца
Пераканаўцеся ў стадіях у тым порядку, як дадзеныя праходзяць чераз іх:
1. Environment
2. Files
3. Tokenizer
4. Token IDs
5. Training batches
6. Model shapes
7. Initial loss
8. Gradients
9. Optimizer
10. Validation behavior
11. Checkpoints
12. Generation
Адгукі пра якасць генеравання да таго часу, калі канал обработкі дадзеных не будзе паўнаста падтверджаны, — губіце час, адтакуль паўзлы прыклад можа выйсці з будзь-якай з адэльнадцяти пакульшых стадій. На кожной стадіі задайце адну конкрэтную запытанне і адпаведзіце на яго за дапамою пераканання, якое можа прыйсці або не прыйсці.
Чатыры групы памылак
Практычна кожны проблема з такім модэллю належыць да аднойчых чатар, і ведаўка пра групу скурчвае пошук.
- Прычыны, якія вырабляюць некоректнасць: код ў логічным сэнсе некоректны. Целі не перасуваюцца, маска прычыннасці дазволяе пазіцыям бачыць пазнейшыя токены, функцыя збытку викорыстоўвае тэнзары з некоректным форматам, а ID-ы токенайзера не састаюцца з вокабуляром, для якога быў створаны модэль.
- Нумерычныя проблемы: матэматычныя вырахунакі становяцца нестабільнымі. Збытак стае
NaN, градыянты взрываюцца, лоджітытас переплывае ў безканечнасць, а функцыя softmax прымеяе рядок без правільных значэнняў. - Прычыны, якія вырабляюць проблемы з оптымізацыяй: рэалізацыя правильная, але процес навчання неэфектыўны, таму што шчыльнасць навчання занадта вялікая або занадто мала, модэль занадта маленькая, а час роботы занадто корачы.
Пачніце з маленькай настройкі для дыбагавання
Дыбагаванне пад час цэлага запуску ператварае кожную гіпотэзу на довгае чаканне. Задайце маленькі модэль, який можа запам’ятаваць калькі прыкладоў за калікс секунд:
debug_config = MiniGPTConfig(
vocab_size=tokenizer.vocab_size,
block_size=32,
embedding_dim=64,
num_heads=4,
num_layers=2,
expansion_factor=4,
dropout=0.0,
)
Спаруйце яго з маленькай партіяю дадзеных:
debug_batch_size = 8
і короткім запускам:
debug_steps = 200
Функцыя Dropout ўмышлена выключаная:
dropout = 0.0
Функцыя Dropout ставяе на ноль адбудовы актывацый, таму ідэнтычныя запускі становяцца разнымі. Яе адключэнне (разам з фіксаваным сэдам) робіць кожны тест воспроізнавальным. Верніце стандартныя настройкі, калі процес завершыцца успешна.
Паказваце среду запуску
Перш чым прыкладвацца да модэлю, выведзіце версіі Python і PyTorch, а таксама чы робіцца викорыстоўванне CUDA або фонавай супорты Apple’s Metal (MPS):
import platform
import torch
print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())
if hasattr(torch.backends, "mps"):
print(
"MPS available:",
torch.backends.mps.is_available(),
)
Памочны прыемак выбірае найкращы прыстрой: спачатку CUDA, потым MPS, а пасля CPU. Функцыя hasattr дапамагае яму працаваць у старэйшых версіях без фонавай супорты MPS:
def get_device():
if torch.cuda.is_available():
return torch.device("cuda")
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_available()
):
return torch.device("mps")
return torch.device("cpu")
Выклікайце яго адназначы і запісуйце рэзультат:
device = get_device()
print("Selected device:", device)
Калі процес навчання неясна сповольнюецца, часта прычыной ёст тая лінія: програма спакоўна чакала GPU, але змушана была перайсці на CPU чераз проблемы з драйверамі або інсталяцыяй.
Убедзіцеся, што кожны файл вхідных дадзеных ўсё ж таки існуе
Пераканайцеся, што існуюць визначэння токенізатора і закодаваныя версіі дадзеных для навчання, пераканання і тэставання; у працоўным режыме трэба як можно раней паказаць чыстую памятку пра FileNotFoundError, а не заплутаную памятку глыбока ў цікле навчання:
from pathlib import Path
required_paths = [
Path("tokenizer/char_tokenizer.json"),
Path("data/encoded/train_ids.pt"),
Path("data/encoded/val_ids.pt"),
Path("data/encoded/test_ids.pt"),
]
for path in required_paths:
if not path.exists():
raise FileNotFoundError(
f"Required file not found: {path}"
)
print("Found:", path)
Таксама выведзіце размеры:
for path in required_paths:
print(
path,
path.stat().st_size,
"bytes",
)
Пусты файл або файл незвычайна маленькага розмеру зазвычай означае, што задача прадзействавання была перарвана і заставіла створыць непূরны файл.
Апробава токенізатора адсакрэджана
Заванесіце токенізатор симвалаў:
tokenizer = CharTokenizer.from_file(
"tokenizer/char_tokenizer.json"
)
Пераканайцеся ў розмерзе слоўніку і ў обох канцах спісу симвалаў, чы не ёсць чаго-небудзь зрабленага не так або спачатанага:
print("Vocabulary size:", tokenizer.vocab_size)
print("First tokens:", tokenizer.chars[:20])
print("Last tokens:", tokenizer.chars[-20:])
Атрыбут двухэтапнага перадавання
Токенізатор без втыратаў вяртае точна той жа вхідны даны пасля кодавання і декодавання. Выведчыце ўвесь тэкст за дапамогою repr, ўжо каб пазначыць невізуальныя симвалы, такія як праспекты:
sample = "The history of"
encoded = tokenizer.encode(sample)
decoded = tokenizer.decode(encoded)
print("Encoded:", encoded)
print("Decoded:", repr(decoded))
assert decoded == sample
Атрыбут, які пераканваецца:
decode(encode(text)) = text
Няспэх означае, што прынеймна адны симвал не можа быть правдзіва практычна представлен, зазвычай гэта сімвал, якога няма ў слоўніку. Метод encode у цэлым скрыпце ў такім случае выклекчвае KeyError, а не проста ігнаруе яго, што ўсё-такі є бажаным.
Захаванне ад несувярэннасцей токенізатора і чекпоінта
Равныя розмеры слоўнікаў є неабходнымі, але недастатнімі. Два слоўнікі могу аба разам мець 100 симвалоў і пры тым надаваць розныя ID:
Tokenizer A: "a" → 10
Tokenizer B: "a" → 24
Модель, натрэнаваная з аднай схемы карэткавання, але выкарыстоўваная з іншай, выдае бессэнсавыя даны, хоць форматы тэнзораў выглядаюць правільна. Зберагачы слоўнік разам з чекпоінтом, або прынеймна його «відблеск». Падойдзе хеш SHA-256 серыяванага списку симвалоў; налагоджэнне параметраў separators і ensure_ascii гарантуе, што той самы список завжды будзе серыявацца у тыя ж байты:
import hashlib
import json
def tokenizer_fingerprint(chars):
payload = json.dumps(
chars,
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(
payload
).hexdigest()
Вычысціце яго для запрашанага токенайзера:
fingerprint = tokenizer_fingerprint(
tokenizer.chars
)
print("Tokenizer fingerprint:", fingerprint)
Зберагуйце яго ў слоўніку пастановак пры зберэнні:
checkpoint[
"tokenizer_fingerprint"
] = fingerprint
Пры запуску параболічнае і адмовіце ў продажы, якщо выйшла няпадобнасць. Умова is not None яшчэ дазволяе запуск старых пастановак без відбытка:
saved_fingerprint = checkpoint.get(
"tokenizer_fingerprint"
)
if (
saved_fingerprint is not None
and saved_fingerprint != fingerprint
):
raise ValueError(
"Checkpoint and tokenizer do not match"
)
Аналізаваце кодаваныя ідэнтыфікаторы токенав:
Запрашайце частку навчання на CPU як 64-бітныя цалыя числы, каб падтрымаліся типавыя эмбеддынгі і крос-ентропія:
train_ids = torch.load(
"data/encoded/train_ids.pt",
map_location="cpu",
).long()
Адразуце форму, тип дадзейнаў і дыапазон:
print("Shape:", train_ids.shape)
print("Dtype:", train_ids.dtype)
print("Minimum ID:", train_ids.min().item())
print("Maximum ID:", train_ids.max().item())
Кожны ідэнтыфікатор павінен належаць да слоўніка:
0 ≤ token ID < vocabulary size
Як асерцыі:
assert train_ids.min().item() >= 0
assert (
train_ids.max().item()
< tokenizer.vocab_size
)
ID, які выходзяць за межы дыапазона, спаказваюць процес пошуку інфармацыі, а на GPU гэта якраз можа выражацца ў незрозумелых пазначэннях з боку прыстрою, якія не адпавядаюць справжней прычыне. Частыя прычыны — некоректны файл токенізатора, паскладжаныя зашифраваныя файлы, слоўнік, перзбудаваны пасля зашыфравання, або нэўсастойчывае оброблэнне спецыяльных токенав.
Чытайце зберажаныя даныя як текст
Нават числы, якія належаць да дыапазона, могу зашыфраваць некоректны текст, таму расшифруйце калькі соткаў ID і прачытайце іх:
sample_ids = train_ids[:500]
sample_text = tokenizer.decode(
sample_ids.tolist()
)
print(sample_text)
Вы должны бачыць чытальны WikiText у звычнай для яго форматаванні, з перапісамі лінакоў, загаловкамі і знакамі падзея, без серыёў павтараючыхся або паскладжаных симвалаў. Якщо прыклад выглядае некоректна, зупініцеся: жадныя змены ў модэлі не можу компенсаваць некоректны токенізатор або набор даных.
Пераканайцеся, што між вхіднымі і выходнымі даннымі є адзін токен
Самі створыце адзін прыклад, калі окно выходных данных пачынаецца на адну пазіцыю пасля:
block_size = 32
start = 100
inputs = train_ids[
start:
start + block_size
]
targets = train_ids[
start + 1:
start + block_size + 1
]
Дэкодавайце оба, каб іх можна было парыраваць:
input_text = tokenizer.decode(
inputs.tolist()
)
target_text = tokenizer.decode(
targets.tolist()
)
print("Input: ", repr(input_text))
print("Target:", repr(target_text))
Цэльвы выхідны дадзеныя павинны адпаваць выхідным дадзеныям, пры чым павінен быць выдален першы символ, а дадзены ўсё новы символ. Пасля чаго трэба пераканацца ў адносе тэнзораў:
assert torch.equal(
inputs[1:],
targets[:-1],
)
Некалькі пераканаў у проекте дапамагаюць выявіць болей серйозныя бяги. Інваріант:
inputs[1:] == targets[:-1]
Кожна пазицыя цэльвых дадзеннях храніць токен, які следуе за адпаведной пазыцыяй выхідных дадзенняў, што якраз і патрэбна для прыценкі наступнага токена.
Бяг ідэнтычных фрагментаў
Класычная памылка заключаецца ў вжыванні адных і тых жа меж для обох фрагментаў:
inputs = data[
start:
start + block_size
]
targets = data[
start:
start + block_size
]
Пасля чаго модель выучае сабе ідэнтычную праекцію:
Current token → current token
Пачатак фрагмента цэльвых дадзенняў на адны токен пазней выправляе гэтую проблему:
targets = data[
start + 1:
start + block_size + 1
]
і вярна выконвае пачатковую задачу:
Current context → next token
Яскравы знак гэтага бягу — странна быстрая падышка значэння лоссу з самага пачатку.
Перакантролюйце формы пакетаў, типы дадзеных і прыстроі
Аберыце прыклад рэальнага пакета:
inputs, targets = get_batch(
data=train_ids,
batch_size=8,
block_size=32,
device=device,
)
Адразу выведзіце все, што можа быць некорактна:
print("Input shape:", inputs.shape)
print("Target shape:", targets.shape)
print("Input dtype:", inputs.dtype)
print("Target dtype:", targets.dtype)
print("Input device:", inputs.device)
print("Target device:", targets.device)
Для размеру пакета 8 і размеру блока 32 чакайце:
Input shape: [8, 32]
Target shape: [8, 32]
Dtype: torch.int64
Device: same as model
Зробіце ці спадзяванні стаўкамі:
assert inputs.shape == targets.shape
assert inputs.dtype == torch.long
assert targets.dtype == torch.long
assert inputs.device == device
assert targets.device == device
Вылечыце несувязы прыстроў
Гэтае выкліканне стаўкаеся пастойна пад час работы з PyTorch:
Expected all tensors to be on the same device
Адна аперацыя праОбрала тэнзоры на разных прыстроях, напрыклад CPU і GPU. Выведзіце, дзе знаходзяцца параметры і пакет:
model_device = next(
model.parameters()
).device
print("Model device:", model_device)
print("Input device:", inputs.device)
Явна пераместіце модэль і кожны пакет:
model = model.to(device)
inputs = inputs.to(device)
targets = targets.to(device)
Іншы выкліканне схована ўнутрь модэль: torch.arange за замовчаннем выкарыстоўвае CPU, таму вярніце прыстрой з прыходзячых токэнаў:
positions = torch.arange(
sequence_length,
device=token_ids.device,
)
Інакша ситуацыя — аддаванне позіцыйных эмбеддингаў да токэн-эмбеддингаў на CUDA або MPS не вяршыцца. Прыўязка працоўнага апарату да вхідных дадзеных таксама спакойвае переноснасць модэлю.
Паўерыяць процес вычыслення
Запустіце адну пакетную обработку з меткамі, каб модэль вярнула лоджіты і пазыраную вартасць:
logits, loss = model(
inputs,
targets,
)
Перагляньце іх:
print("Logits shape:", logits.shape)
print("Loss shape:", loss.shape)
print("Loss value:", loss.item())
Для лоджітав трэба адна вартасць на кожную позыцыю і для кожнага элемента слоўніка, а пазыраная вартасць должна быць скалярным значэнням:
assert logits.shape == (
inputs.size(0),
inputs.size(1),
tokenizer.vocab_size,
)
assert loss.ndim == 0
Лоджіты з форматам [B, V, T] означаюць, што практыка транспозыціі аб перэкройвання размераў прывела да неправільнага порядку выразоў. Паколькі функцыя F.cross_entropy прымеўка класовыя вартасці ў другым выразе, тэнзор з неправільным порядкам іноды можа прайсці да вычыслення пазыранай вартасці без адной бяды і даць нерозумны результат.
Параўняйце пачатковую пазыраную вартасць з адной разовай базовай
Модель, яка была няўжо ініцыявана з малымі вагамі, прыпускае практычна аднородны распад, а крос-ентропія па аднородным распадзе на V класоў становіць log(V):
import math
expected_loss = math.log(
tokenizer.vocab_size
)
print("Expected loss:", expected_loss)
print("Actual loss:", loss.item())
Невялікі адхыленне ўскладненне; вялікі адхыленне — гэта сігнал. Пачатковая вартасць збытку, значна вышэй за базовую, можа свідчыць пра экстремальныя лоджіты, нестабільную ініцыявацыю, некоректныя ID токэнаў, цілі, якія не падходзяць да слоўніка, або некоректную форму выходных даных. Пачатковая вартасць збытку, значна нижэй за базовую, чаго модель, якая нічога не ведае, не можа чыста досягнуць, можа свідчыць пра вытэк дадзеных, падключэнне некоректных ваг, цілі, роўныя інпутам, видныя будучыя токэны, або ненарочнае продакцэюванне чекпоінта.
Даказваць, што каузальная маска працюе
GPT павінен працаваць толькі на адміністрацыях, які ўзьняты з ранейшых токенав. Створыце два сэквэнсы з адным прыфіксам і разнымі суфіксамі; якщо модель каузальная, логіты прыфікса павінны збігацца. У режыме ацэнкі выключаецца функцыя dropout, ўжо таму што варантнасць не павінна вплываць:
model.eval()
prefix_length = 8
sequence_length = 16
sequence_a = torch.randint(
0,
tokenizer.vocab_size,
(1, sequence_length),
device=device,
)
sequence_b = sequence_a.clone()
sequence_b[
:,
prefix_length:
] = torch.randint(
0,
tokenizer.vocab_size,
(
1,
sequence_length - prefix_length,
),
device=device,
)
Запускайце оба без вычыслення градыянтав:
with torch.no_grad():
logits_a, _ = model(sequence_a)
logits_b, _ = model(sequence_b)
Зважыце максимальную разніцу прыфіксаў:
prefix_difference = (
logits_a[:, :prefix_length, :]
- logits_b[:, :prefix_length, :]
).abs().max().item()
print(
"Maximum prefix difference:",
prefix_difference,
)
Пераканайцеся ў роўнасці за дапамогою маленькага допуску, який падхапляе шумы з флотацыйных чисоў:
assert torch.allclose(
logits_a[:, :prefix_length, :],
logits_b[:, :prefix_length, :],
atol=1e-5,
)
Працязка означае, што інформацыя з пазнейшых позыцый праточыцца да ранейшых, зазвычай таму, што маска неяўна, застосавана да неправильнай высоты або створаная з неправільнага трэхкутніка. Теставанне поведзення ад пачатку да канца є эфектывнейшым спосабам, чым толькі перагляд тэнзора маскі.
Переадаптаванне да аднаго пакета
Якщо вы выберазеце адну з тэхнікаў з гіда, выберыце самэ гэтую:
Модель з достатнім прыемлемым розмахам должна магчымаць запамятаванне аднаго невялікага пакета дадзеных.
У яе адночасна праця з дадзенымі, самай модэллю, показнікам губернацыі, процесам зворачання і оптымайзерам. Неабяжна паспрацаваць з адним пакетам, які вжываецца ў кожным кроку:
fixed_inputs, fixed_targets = get_batch(
data=train_ids,
batch_size=8,
block_size=32,
device=device,
)
Створыце невялікую модэль без функцыі dropout:
debug_config = MiniGPTConfig(
vocab_size=tokenizer.vocab_size,
block_size=32,
embedding_dim=64,
num_heads=4,
num_layers=2,
expansion_factor=4,
dropout=0.0,
)
debug_model = MiniGPT(
debug_config
).to(device)
Трэнуйце яе на гэтым пакете аднарозова, фіксуючы рэзультаты кожных 50 крокаў:
optimizer = torch.optim.AdamW(
debug_model.parameters(),
lr=1e-3,
)
for step in range(500):
optimizer.zero_grad(
set_to_none=True
)
_, debug_loss = debug_model(
fixed_inputs,
fixed_targets,
)
debug_loss.backward()
optimizer.step()
if step % 50 == 0:
print(
step,
debug_loss.item(),
)
Показнік губернацыі должен быць значна нижэй базовага значэння. Якщо гэтага не выходзіць, можна падазрываць на несправны показнік губернацыі, градыенты, якія ніколі не доходзяць да дзеяных параметраў, цэлі без перасуну, модэль, якая занадта мала нават для гэтага, паслаба выбраны коефіцыент навучэння, несправны каузальны маск чы оптымайзер, який нічога не апдэюе. Теставанне служыць крокам перад пачатком або завершэнням цэлага процесу.
Паказванне таго, чы градыенты доходзяць да кожнага параметра
Адрабатайце адна паўза ў напрамку і адной у зворачным напрамку:
optimizer.zero_grad(
set_to_none=True
)
_, loss = model(
inputs,
targets,
)
loss.backward()
Звярніце адказ працэйкаваных параметраў, якіх .grad яшчэ ёсць None, а таксама пра норму для рэшты:
for name, parameter in (
model.named_parameters()
):
if not parameter.requires_grad:
continue
if parameter.grad is None:
print(
"NO GRADIENT:",
name,
)
else:
print(
name,
parameter.grad.norm().item(),
)
Відсутнае градыянт зазвычай означае шар, заданы ў __init__, але не выкарыстоўваны ў forward, случайны вызов .detach(), галоўная ветка, якая праходзіць праз компоненту, значэнне збытку, вырачана з несвязанага тэнзора, або настройка requires_grad=False.
Следзіце за глобальной нормай градыянта
Нормы па кожным параметру дапамагаюць выявіць «мертвыя» шары; адна сумарная цифра фіксуе стабільнасць з часам. Штая функция сумавае ўсі нормы градыянта L2, тые ж самыя значэння, якія выкарыстоўваюцца для кліпання:
def calculate_gradient_norm(model):
squared_norm = 0.0
for parameter in model.parameters():
if parameter.grad is None:
continue
parameter_norm = (
parameter.grad
.detach()
.norm(2)
.item()
)
squared_norm += (
parameter_norm ** 2
)
return squared_norm ** 0.5
Запісуйце яго пасля кожнага празыходу назад:
gradient_norm = (
calculate_gradient_norm(model)
)
print("Gradient norm:", gradient_norm)
Стараўцеся звернуць увагу на нормы, якія ўзмаўчана равныя нулю, вельмі великія або рэзкая зменшаюцыяся, NaN або inf.
Раннее выяўленне NaN і безконечнасці
Памочны прыгун запускаецца як толькі тэнзор мае несконечную значэнне, і называе гэты тэнзор:
def assert_finite_tensor(
tensor,
name,
):
if not torch.isfinite(
tensor
).all():
raise FloatingPointError(
f"{name} contains NaN or infinity"
)
Застосавайце яго да лоджітов і пазыркі:
assert_finite_tensor(
logits,
"logits",
)
assert_finite_tensor(
loss,
"loss",
)
а таксама да кожнага градыянта пасля backward():
for name, parameter in (
model.named_parameters()
):
if parameter.grad is not None:
assert_finite_tensor(
parameter.grad,
f"gradient for {name}",
)
Перакантраванне калькольніка па калькольных тыпах паказвае месца, дзе спачатку з’яўляюць некалькольныя числы, што ў дзесяткі разоў корыстней, чым выявленне NaN пазыркі за сотні крокаў пасля таго.
Чаму пазыркі стае NaN
Частыя прычыны: занадта высокі коефіцыент навчання, выбухаючыя градыянты, рядок увагі, дзе зашчытаны ўсе пазыцыі, некалькольны вхід для softmax, переплыв у сумешанай прыцэльнасі, вяроўныя ўжо зіпсаваныя параметры, дзеленне на ноль, логарым з нуля або адзячай значэння, і несконечныя лоджіты. Калі гэта выйшла:
- Зупініце запуск.
- Знайдзіце пасляпэўны крок з сконечным пазыркам.
- Знізьце коефіцыент навчання.
- Увялікніце обмежэнне градыянтаў.
Ніколі не продавайце процес апдэйта, калі параметры маюць NaN; кожны апдэйт распрасцеляе парадкі, таму кращэ прыступіце да работы з последнім стабільным чекпоінтом.
Існуючы кліппінг градыента як захоўнічны механізм
Кліппінг пераскладоўвае градыенты, чыя сумарная норма перасілляе певны порог, таму ён выконваецца між backward() і optimizer.step():
loss.backward()
gradient_norm = (
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0,
)
)
optimizer.step()
clip_grad_norm_ вяртае норму, вымераную да кліппінга, што таксама служыць для манітарынгу:
print(
"Gradient norm before clipping:",
float(gradient_norm),
)
Якщо порог перасілляе практычна ў кожны крок, кліппінг маскіруе проблемы, такія як занадта велика шчыльнасць навучэння аб цыфровая нестабільнасць. Ён захоўвае ад эпізодычных проблем з даступным даннем; ён не заменяе разумную шчыльнасць навучэння.
Пераканацеце, чы ўтэмізатор зменяе вагі
Зробіце копію аднаго параметра прычыму до апдэйта. Функцыя .clone() мае важнае значэнне: без яе параметр before дзеліць памяць з параметрам і таксама зменяецца:
parameter_name, parameter = next(
model.named_parameters()
)
before = parameter.detach().clone()
Адрыхніце адны крок навчання:
optimizer.zero_grad(
set_to_none=True
)
_, loss = model(
inputs,
targets,
)
loss.backward()
optimizer.step()
Змерыце змену:
after = parameter.detach()
maximum_change = (
after - before
).abs().max().item()
print(
"Maximum parameter change:",
maximum_change,
)
і трэба, каб была змена:
assert maximum_change > 0
Незмененыя вагі можаць свядчыць пра нулевую шчыльнасць навчання, утэмізатор, створанный без параметраў моделі (напрыклад, прычыму до замены моделі), відсутнае значэнне градыентаў, відсутняя функцыя optimizer.step() аб заморожаныя параметры.
Настроюйце шчыльнасць навчання ў обох напрамках
Шчыльнасць, якая занадта велика, выражаецца ў быстрым адныянні значэння патраса збытка аб яго стрэльчастых колываннях, вельмі большых нормах градыентаў, значэннях патраса збытка NaN і прыкладах, якія ніколі не павышаюць своих показнікаў. Першы спосаб прыладзення — знижэнне гэтага піку, напрыклад да:
max_learning_rate = 1e-4
Заместо тага:
max_learning_rate = 1e-3
Запісваць коефіцыент навчання разам з пазуром. Пры вармапе нестабільнасць частаючы з’яўляецца самэ пры піку, што неактуальна для графіка пазуру. Коефіцыент, які занадта низькі, дае іншыя наследкі: пазур спадае дужа медленна, хоць існую градыенты, параметры ледве рухаюцца, а нават тэст з аднай пакетам дадзеных трывае многа крокаў. Тады яго трэба падняць, напрыклад да:
max_learning_rate = 3e-4
Заместо тага:
max_learning_rate = 1e-5
Няма ўніверсальна правильнага значэння; яно залежыць ад розмеру модэлю і пакета дадзеных, оптымізатора і самага набору дадзеных. Адрабатайце короткія експерыменты, у яых змінюецца толькі коефіцыент навчання.
Спіс пераконтальных пунктав для пазуру, які не спадае
Расглядзіце гэтыя пытанні па порядку. Дадзеныя:
Are targets shifted by one token?
Are token IDs within range?
Does decoded input look correct?
Модэль:
Are logits shaped [B, T, V]?
Is the causal mask valid?
Are positions on the correct device?
Пазур:
Does cross-entropy receive raw logits?
Are logits and targets flattened correctly?
Падзець у тым, што верасціяты softmax падаюць у функцыю cross_entropy, ўсё таму, што сама гэта функцыя выконвае лог-softmax. Градіенты:
Do all important parameters receive gradients?
Are gradient norms finite and nonzero?
Оптымізатор:
Is the learning rate positive?
Does optimizer.step() run?
Do parameters change?
Ёмкасць:
Can the model overfit one batch?
This order avoids random trial and error.
Гэты порядак усувае адну категорію прычын за раз, замест таго, каб паслужыцца методам проб і памылак.
Адзначэнне переадаптавання і недастатковай адаптавання
Переадаптаванне выражаецца ў розходжэннях крывых:
Training loss: continues decreasing
Validation loss: stops decreasing or increases
Канкрэтызаваць разлік:
generalization_gap = (
validation_loss
- training_loss
)
Способы выправлення включаюць зберагчы найкращы кэшпоінт валідацыі, падняць параметры dropout або weight decay, зменшыць размер модэлю, дадаць больш або разнарадзіваныя даны, і зупініцца раней. Выберыце тачку зупінкі на адзее валідацыі; выкарыстоўванне адзеі тэставання прыводзіць да вытэкання інфармацыі і завышэння канечнага рэзультата.
Недастатковая адаптавання выражаецца ў двух крывых, якія застаюцца высокімі разам:
Training loss: remains high
Validation loss: remains similarly high
Ўзледзейныя прычыны — гэта недастатковая ўместнасць, занадта короткі запуск, низкая шчыльнасць навучэння, маленькі прастор контексту, даныя, якія занадта складныя для архітектуры, або токенізацыя, якая витрачае контекст. Серед варыянтаў — больш пасоў, большая размера вкладання, больш слоёў Transformer, дыявалейшы прастор контексту, кодаванне па парах байт (BPE) заместо симвалів, а таксама перенастройка шчыльнасці навучэння. Спачатку запустіце тест з адной пакетам: якщо модель не можа запам’ятаваць адну пакет, проблема крыўчыця ў правільнасці або оптымізацыі, а не ў ўместнасці.
Дыагназа павтаральнай генерэціі
Павтаральнасць выглядае так:
the the the the
альбо, з абзначэннямі загалоўкаў у WikiText:
= = = = = = =
Прычыны включаюць жаданую декодаванню, чырваўскі тэмпературны параметр, мінімальнае значэнне top-k, недастаткова або перенавучаную модель, павтаральныя структуры ў данных і маленькі прастор контексту. Прабуйце выкарыстоўваць балансаванэе выбіранне прыкладоў:
temperature = 0.8
top_k = 20
top_p = 0.9
Штраф за павтарэнне можа быць корыстным, якщо ён будзе лёгкі:
repetition_penalty = 1.05
У моделях з символамі сильны штраф пераканае не вжываць зноў тыя ж літеры і швыдка паспрабоўвае зіпсаваць артыкуляцыю. Якщо кожна настройка декодавання все равно выконвае циклы, то слабым местам ёстся сама модель, а не прыстрой сэмпліраўання. Як гэтыя настройкі взаіднае дзейнуюць, пазнайоміцеся з нашым кялікам пра тэмпературу, top-k і top-p.
Діагназа хаотычнага генеравання
Абваротныя проблемы выражаюцца ў наяўнасці непатрэбных симвалоў, зламаных слоў, чрэзмернай колькасці знакоў падчыркування, раптовых пераходах між тэмамі і неразбіраемых строках. Оерагодныя прычыны — высока тэмпература, абсенцыя фільтраў top-k чысто top-p, несувязаны токенайзер, некоректны чэкпоінт, недастаткова навучаная модель з высокай вартасцю валідацыі чысто вагі, якія так і не былі завантажаны. Спробуйце болей строгае сэмпліраўанне:
temperature = 0.6
top_k = 10
top_p = 0.9
Паказваць, чы рэальна вага апранулася з пункта контролю:
model.load_state_dict(
checkpoint["model_state_dict"]
)
і чы функцыя dropout выключана пад час выбору прыкладоў:
model.eval()
Калі выходы ігнаруюць запит:
Запіт можа быть дужа короткім або не супадаць з данымі трэнавання; модель може быть маленькай, недастаткова трэнаваная, слабой у рашыранні залежнасцяў на дальняя вялічыну, або маць обмежаны контекст; кроме таго, токены знакоў ускладняюць вывучэнне семантычных патэранаў. Апробаваць дыявоольскія запіты у стыле WikiText. Пораўняць мінімальны запіт:
"The "
з болей насыценым запітом:
"The history of the city began"
Другі варыянт дае модэлі значна больш паказанняў для адгукнення. Якщо продажы все ж такі змінююцяся, пераглядзець паказанні бэкпраўкі і реалізацыю механізма адгукнення.
Пункты контролю, якія адмовляюцца чытаць:
Адрасы памялі з багамі ўжо вядомы:
Missing key(s) in state_dict
Unexpected key(s) in state_dict
Size mismatch
Это означае, што модэль, якую вы створылі, не тая, яку зберагалі: зменілася ўстановка, колькасць шароў, размернасць эмбеддынга, розмер слоўніку чы сумы коефіцяентаў, класы чы атрыбуты былі перайменаваны, або чытаецца стан оптымізатора з іншай архітэктуры. Перагляньце зберажаную установку:
print(
checkpoint["config"]
)
Створыце модэль на адной з гэтых установак, а не на текущых стандартных:
config = MiniGPTConfig(
**checkpoint["config"]
)
model = MiniGPT(config)
Потым запчытайце словнік стану. Стварэнне модэль на новай установцы і спадзяванне, што старыя коефіцыенты падойдуць, ўсё чащэ вызывае гэтыя адны.
Спіс нехваткаючых і неканальных ключоў
Толькі для дыагназы: запчытайце іх не строга і выведзіце розніцу:
load_result = model.load_state_dict(
checkpoint["model_state_dict"],
strict=False,
)
print(
"Missing keys:",
load_result.missing_keys,
)
print(
"Unexpected keys:",
load_result.unexpected_keys,
)
Співвідношэння элементаў спісаў зазвычай паказвае прычыну, напрыклад, перэіменаваны падзял. Для выважвання або продактываўкі навчання неабходна строга старацца пры завантажэнні, каб несумесны чекпоінт выклікаў явныя проблемы, а не залишаў шары з касматычнымі пачатковымі значэннямі.
Пераместіце стан оптымайзера на правы прыстрой
Пасля вярнення оптымайзера яго внутраніе тэнзоры (на прыклад, оценкі моменту для AdamW) можаць знаходзіцца на іншым прыстрое чым сама модэль. Штатны калектар перамешчае кожны тэнзор у стане:
def move_optimizer_to_device(
optimizer,
device,
):
for state in optimizer.state.values():
for key, value in state.items():
if torch.is_tensor(value):
state[key] = value.to(
device
)
Выклікайце яго адразу пасля завантажэння:
optimizer.load_state_dict(
checkpoint[
"optimizer_state_dict"
]
)
move_optimizer_to_device(
optimizer,
device,
)
Гэта мае вялікое значэнне, калі зберагаеце даны на адной машыне, а продактываеце навчання на іншай, напрыклад, з CUDA на MPS або CPU.
Аб’еднайце ключовыя пераканання ў адной функцыі стану
Зберагаюце найважлівэйшыя пераканання ў адной функцыі: тип і дыапазон токэнаў, зсув цялевых значэнняў, форма лоджітов, скончаная вартасць збытку і паўстанавленне у адносе да касматычнага базовага значэння:
def run_model_health_checks(
model,
tokenizer,
train_ids,
device,
):
model.eval()
assert train_ids.dtype == torch.long
assert train_ids.min().item() >= 0
assert (
train_ids.max().item()
< tokenizer.vocab_size
)
batch_size = 4
block_size = min(
32,
model.config.block_size,
)
inputs, targets = get_batch(
data=train_ids,
batch_size=batch_size,
block_size=block_size,
device=device,
)
assert inputs.shape == targets.shape
assert torch.equal(
inputs[:, 1:],
targets[:, :-1],
)
with torch.no_grad():
logits, loss = model(
inputs,
targets,
)
assert logits.shape == (
batch_size,
block_size,
tokenizer.vocab_size,
)
assert torch.isfinite(loss)
expected_loss = math.log(
tokenizer.vocab_size
)
print("Current loss:", loss.item())
print(
"Random baseline:",
expected_loss,
)
print("Model health checks passed.")
Для адаптаванага чэкпоінта падышкі выкарыстоўвання ресурса должны ясна быць нижэй за базовы значэнне; інакш вагі не былі завантажаны або токенайзер не падходзіць.
Аднаходжэнне числовых проблемаў за дапамою хуків
Калі гдзе-небудзь з’яўляецца NaN, хукі перагледваюць выход кожнага модуля пад час адпаведнага процеса. Штырэнне караецца іменем класу модуля, калі пад час перагледу з’яўляецца першая несконечная значэнне, і прычына бярэцца ў выкарыстоўванне:
def finite_output_hook(
module,
inputs,
output,
):
tensors = []
if torch.is_tensor(output):
tensors = [output]
elif isinstance(output, tuple):
tensors = [
item
for item in output
if torch.is_tensor(item)
]
for tensor in tensors:
if not torch.isfinite(
tensor
).all():
raise FloatingPointError(
"Nonfinite output detected in "
f"{module.__class__.__name__}"
)
Прыкрепіце яго да кожнага лінейнага, layer-norm і embedding модуля, заставаючы сабе ўсія хэндлы:
hooks = []
for module in model.modules():
if isinstance(
module,
(
torch.nn.Linear,
torch.nn.LayerNorm,
torch.nn.Embedding,
),
):
hooks.append(
module.register_forward_hook(
finite_output_hook
)
)
Адрабатайце адну пасэж працы; так какяк шары выконваюцца па порядку, першая асаблівасць паказвае назывку першага шару, які не працавае. Пасля чаго адключыце хукі:
for hook in hooks:
hook.remove()
Хукі выконваюцца праз кожны вызов і спамляюць працэю моделі, таму выкорыстоўваеце іх толькі пад час пошуку бага. Чыбаўце точны пацёк модуля, запісваюце імены з named_modules() пад час рэўістрацыі.
Полны діагностычны скрыпт
Усе перакананні аб’еднаныя ў адны інструмент у командной лініи. Зберагаюце яго як:
debug_mini_gpt.py
У скрыпцы заданы мінімальны CharTokenizer, выбор прыстрою, «відблеск» дадзеных, памагальны прысобнік і числовыя засобы. Ёй будуецца модель на адпрацоўкі сэнтрыфюжу самай яго настройкі, адхоўваецца токенайзер з іншым размерам слоўніку, пасля чаго адбываецца восьма тэставанняў з номерамі: транзакцыя токенайзера, дыапазон токенаў, перасуванне пакетаў, праходжэння даных наперад, каузальная незалежнасць, градыенты, апдэйт оптымізатора і, за выборам, перенасыцэнне адной пакетаў на новай модэлі для дыбагу, усё гэта пад фіксаваным седлам. Варта звернуць увагу на два моменты: тэст токенайзера декодуе зберажаныя ID і перекодуе іх, пераканаючыся ў правядзібнасці рэальнага набора дадзеных, а тэст оптымізатора выкарыстоўвае новы экземпляр AdamW, каб стары стан не могаў васклічваць.
import argparse
import hashlib
import json
import math
from pathlib import Path
import torch
from mini_gpt import MiniGPT
from mini_gpt import MiniGPTConfig
class CharTokenizer:
def __init__(self, chars):
self.chars = chars
self.vocab_size = len(chars)
self.stoi = {
char: index
for index, char in enumerate(chars)
}
self.itos = {
index: char
for index, char in enumerate(chars)
}
@classmethod
def from_file(cls, path):
with open(
path,
"r",
encoding="utf-8",
) as file:
data = json.load(file)
return cls(data["chars"])
def encode(self, text):
return [
self.stoi[char]
for char in text
]
def decode(self, token_ids):
return "".join(
self.itos[int(token_id)]
for token_id in token_ids
)
def get_device():
if torch.cuda.is_available():
return torch.device("cuda")
if (
hasattr(torch.backends, "mps")
and torch.backends.mps.is_available()
):
return torch.device("mps")
return torch.device("cpu")
def tokenizer_fingerprint(chars):
payload = json.dumps(
chars,
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(
payload
).hexdigest()
def get_batch(
data,
batch_size,
block_size,
device,
):
start_positions = torch.randint(
low=0,
high=len(data) - block_size,
size=(batch_size,),
)
inputs = torch.stack([
data[
position:
position + block_size
]
for position in start_positions
])
targets = torch.stack([
data[
position + 1:
position + block_size + 1
]
for position in start_positions
])
return (
inputs.to(device),
targets.to(device),
)
def assert_finite_tensor(
tensor,
name,
):
if not torch.isfinite(
tensor
).all():
raise FloatingPointError(
f"{name} contains NaN or infinity"
)
def calculate_gradient_norm(model):
squared_norm = 0.0
for parameter in model.parameters():
if parameter.grad is None:
continue
norm = (
parameter.grad
.detach()
.norm(2)
.item()
)
squared_norm += norm ** 2
return squared_norm ** 0.5
def load_model(
checkpoint_path,
device,
):
checkpoint = torch.load(
checkpoint_path,
map_location=device,
)
config = MiniGPTConfig(
**checkpoint["config"]
)
model = MiniGPT(config)
model.load_state_dict(
checkpoint["model_state_dict"]
)
model = model.to(device)
return model, checkpoint
def test_tokenizer(
tokenizer,
train_ids,
):
print("\n1. Testing tokenizer")
print(
"Vocabulary size:",
tokenizer.vocab_size,
)
print(
"Tokenizer fingerprint:",
tokenizer_fingerprint(
tokenizer.chars
),
)
sample_ids = train_ids[:300]
sample_text = tokenizer.decode(
sample_ids.tolist()
)
round_trip_ids = tokenizer.encode(
sample_text
)
assert round_trip_ids == (
sample_ids.tolist()
)
print("Decoded sample:")
print(repr(sample_text))
print(
"Tokenizer round-trip test passed."
)
def test_token_ids(
tokenizer,
train_ids,
):
print("\n2. Testing token IDs")
print("Shape:", train_ids.shape)
print("Dtype:", train_ids.dtype)
minimum_id = train_ids.min().item()
maximum_id = train_ids.max().item()
print("Minimum ID:", minimum_id)
print("Maximum ID:", maximum_id)
assert minimum_id >= 0
assert maximum_id < (
tokenizer.vocab_size
)
print("Token ID test passed.")
def test_batch(
tokenizer,
train_ids,
block_size,
device,
):
print("\n3. Testing batches")
inputs, targets = get_batch(
data=train_ids,
batch_size=4,
block_size=block_size,
device=device,
)
print("Input shape:", inputs.shape)
print("Target shape:", targets.shape)
assert inputs.shape == targets.shape
assert inputs.dtype == torch.long
assert targets.dtype == torch.long
assert torch.equal(
inputs[:, 1:],
targets[:, :-1],
)
input_text = tokenizer.decode(
inputs[0].cpu().tolist()
)
target_text = tokenizer.decode(
targets[0].cpu().tolist()
)
print("Input sample:")
print(repr(input_text))
print("Target sample:")
print(repr(target_text))
print("Batch shift test passed.")
return inputs, targets
def test_forward_pass(
model,
tokenizer,
inputs,
targets,
):
print("\n4. Testing forward pass")
model.eval()
with torch.no_grad():
logits, loss = model(
inputs,
targets,
)
print("Logits shape:", logits.shape)
print("Loss:", loss.item())
assert logits.shape == (
inputs.size(0),
inputs.size(1),
tokenizer.vocab_size,
)
assert_finite_tensor(
logits,
"logits",
)
assert_finite_tensor(
loss,
"loss",
)
print(
"Random baseline:",
math.log(tokenizer.vocab_size),
)
print("Forward-pass test passed.")
def test_future_independence(
model,
tokenizer,
device,
):
print(
"\n5. Testing causal independence"
)
model.eval()
sequence_length = min(
16,
model.config.block_size,
)
prefix_length = (
sequence_length // 2
)
sequence_a = torch.randint(
0,
tokenizer.vocab_size,
(1, sequence_length),
device=device,
)
sequence_b = sequence_a.clone()
sequence_b[
:,
prefix_length:
] = torch.randint(
0,
tokenizer.vocab_size,
(
1,
sequence_length
- prefix_length,
),
device=device,
)
with torch.no_grad():
logits_a, _ = model(sequence_a)
logits_b, _ = model(sequence_b)
difference = (
logits_a[
:,
:prefix_length,
:,
]
- logits_b[
:,
:prefix_length,
:,
]
).abs().max().item()
print(
"Maximum shared-prefix difference:",
difference,
)
assert torch.allclose(
logits_a[
:,
:prefix_length,
:,
],
logits_b[
:,
:prefix_length,
:,
],
atol=1e-5,
)
print(
"Causal independence test passed."
)
def test_gradients(
model,
inputs,
targets,
):
print("\n6. Testing gradients")
model.train()
model.zero_grad(
set_to_none=True
)
_, loss = model(
inputs,
targets,
)
loss.backward()
missing_gradients = []
nonfinite_gradients = []
for name, parameter in (
model.named_parameters()
):
if not parameter.requires_grad:
continue
if parameter.grad is None:
missing_gradients.append(name)
continue
if not torch.isfinite(
parameter.grad
).all():
nonfinite_gradients.append(
name
)
print(
"Gradient norm:",
calculate_gradient_norm(model),
)
if missing_gradients:
print(
"Missing gradients:",
missing_gradients,
)
if nonfinite_gradients:
print(
"Nonfinite gradients:",
nonfinite_gradients,
)
assert not missing_gradients
assert not nonfinite_gradients
print("Gradient test passed.")
def test_optimizer_update(
model,
inputs,
targets,
):
print("\n7. Testing optimizer update")
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-3,
)
name, parameter = next(
model.named_parameters()
)
before = parameter.detach().clone()
optimizer.zero_grad(
set_to_none=True
)
_, loss = model(
inputs,
targets,
)
loss.backward()
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0,
)
optimizer.step()
maximum_change = (
parameter.detach() - before
).abs().max().item()
print("Tracked parameter:", name)
print(
"Maximum parameter change:",
maximum_change,
)
assert maximum_change > 0
print(
"Optimizer update test passed."
)
def run_single_batch_overfit(
tokenizer,
train_ids,
device,
steps,
):
print(
"\n8. Running single-batch "
"overfitting test"
)
config = MiniGPTConfig(
vocab_size=tokenizer.vocab_size,
block_size=32,
embedding_dim=64,
num_heads=4,
num_layers=2,
expansion_factor=4,
dropout=0.0,
)
model = MiniGPT(config).to(device)
inputs, targets = get_batch(
data=train_ids,
batch_size=8,
block_size=config.block_size,
device=device,
)
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-3,
)
initial_loss = None
final_loss = None
for step in range(steps):
optimizer.zero_grad(
set_to_none=True
)
_, loss = model(
inputs,
targets,
)
if initial_loss is None:
initial_loss = loss.item()
assert_finite_tensor(
loss,
"single-batch loss",
)
loss.backward()
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0,
)
optimizer.step()
final_loss = loss.item()
if (
step % 50 == 0
or step == steps - 1
):
print(
f"Step {step:4d}: "
f"loss {final_loss:.4f}"
)
print(
"Initial loss:",
initial_loss,
)
print(
"Final loss:",
final_loss,
)
assert final_loss < initial_loss
print(
"Single-batch overfitting "
"test passed."
)
def parse_args():
parser = argparse.ArgumentParser(
description=(
"Run Mini-GPT diagnostic tests"
)
)
parser.add_argument(
"--checkpoint",
type=str,
default=(
"checkpoints/mini_gpt_best.pt"
),
)
parser.add_argument(
"--tokenizer",
type=str,
default=(
"tokenizer/char_tokenizer.json"
),
)
parser.add_argument(
"--train-data",
type=str,
default=(
"data/encoded/train_ids.pt"
),
)
parser.add_argument(
"--overfit-steps",
type=int,
default=300,
)
parser.add_argument(
"--skip-overfit",
action="store_true",
)
return parser.parse_args()
def main():
args = parse_args()
torch.manual_seed(42)
device = get_device()
print("Using device:", device)
tokenizer = CharTokenizer.from_file(
args.tokenizer
)
train_ids = torch.load(
args.train_data,
map_location="cpu",
).long()
checkpoint_path = Path(
args.checkpoint
)
if not checkpoint_path.exists():
raise FileNotFoundError(
f"Checkpoint not found: "
f"{checkpoint_path}"
)
model, checkpoint = load_model(
checkpoint_path=checkpoint_path,
device=device,
)
if (
tokenizer.vocab_size
!= model.config.vocab_size
):
raise ValueError(
"Tokenizer and model vocabulary "
"sizes do not match"
)
print(
"Checkpoint step:",
checkpoint.get("step"),
)
test_tokenizer(
tokenizer,
train_ids,
)
test_token_ids(
tokenizer,
train_ids,
)
test_block_size = min(
32,
model.config.block_size,
)
inputs, targets = test_batch(
tokenizer=tokenizer,
train_ids=train_ids,
block_size=test_block_size,
device=device,
)
test_forward_pass(
model=model,
tokenizer=tokenizer,
inputs=inputs,
targets=targets,
)
test_future_independence(
model=model,
tokenizer=tokenizer,
device=device,
)
test_gradients(
model=model,
inputs=inputs,
targets=targets,
)
test_optimizer_update(
model=model,
inputs=inputs,
targets=targets,
)
if not args.skip_overfit:
run_single_batch_overfit(
tokenizer=tokenizer,
train_ids=train_ids,
device=device,
steps=args.overfit_steps,
)
print(
"\nAll requested diagnostics passed."
)
if __name__ == "__main__":
main()
Няўзнікшлі выданні PyTorch зменілі стандартную працэю функцыі torch.load на завантажэнне толькі ваг, таму, залежна ад вашай версіі і содержання чекпоінта, можа знадобіцца явна настройка параметра weights_only; пераканаўцеся з ныякімі дасведчэннямі.
Адклеканне дыягностычных даных
Запускайце весь набор з стандартнымі шляхамі:
python debug_mini_gpt.py
Ёсць можласць прыскочыць зарады швыраког пераканаўця чераз этап перэзапоўнення. Для гэтага викорыстоўваецца флаг --skip-overfit з двума пераднімі мінусамі:
python debug_mini_gpt.py - skip-overfit
Викорыстоўваеце іншы чекпоінт:
python debug_mini_gpt.py \
--checkpoint checkpoints/mini_gpt_latest.pt
Дадзіце тэсту на перэзапоўнення больш крокаў:
python debug_mini_gpt.py \
--overfit-steps 500
Заканчоўны слэш продовжвае команду ў шэлы працэсаў у стылі Unix; якщо ваш шэл не падтрымвае гэта, запісуйце команду на адной лініі.
Практычны порядак дэбагавання
Калі модель працуе некоректна, выконвайце этыя крокі без прыскакання да наступных.
Крокі 1 да 5: даны і формы
Чытайце дэкодаваныя даны:
Does the tokenized dataset decode correctly?
Пераканаўцеся ў схылэнні цяльвы:
Does inputs[:, 1:] equal targets[:, :-1]?
Пераканаўцеся ў дыапазоне токэнаў:
Are all IDs between 0 and vocab_size - 1?
Пераканаўцеся ў формах тэнзораў:
Inputs: [B, T]
Targets: [B, T]
Logits: [B, T, V]
Пераканаўцеся ў пачатковай збыткавасці:
Is it near log(vocab_size) for a new model?
Крокі 6 да 10: паведанне модэлю і трэніраванне
Пераканаўцеся ў прычынна-наследный незалежнасці:
Can changing the future affect prefix logits?
Едынны прыемлівы адказ — «нет». Пераканаўцеся ў градыянтах:
Are gradients present, finite, and nonzero?
Пераканаўцеся ў апдэйтах параметраў:
Does optimizer.step() change weights?
Запамятаце адну пакетацыю:
Can the model memorize a tiny fixed batch?
Толькі тады запусціце полное трэніраванне:
Only after all earlier tests pass should you invest in a long training run.
Спісы пераканаўкі для кожнай фазы
Да трэніравання:
□ Dataset files exist
□ Tokenizer round-trip works
□ Token IDs are within vocabulary range
□ Decoded data looks correct
□ Inputs and targets are shifted by one
□ Batch tensors use torch.long
□ Model and batch use the same device
□ Logits have shape [B, T, V]
□ Initial loss is near log(V)
□ Future-independence test passes
□ All important parameters receive gradients
□ Optimizer changes parameters
□ Model can overfit one batch
Пад час трэніравання:
□ Loss remains finite
□ Gradient norms remain finite
□ Learning rate follows the intended schedule
□ Training loss decreases
□ Validation loss is evaluated in eval mode
□ Best checkpoint updates when validation improves
□ Samples become more structured
Пад час генеравання:
□ Best checkpoint is loaded
□ Matching tokenizer is loaded
□ Model is in evaluation mode
□ Context is cropped to block size
□ Only final-position logits are sampled
□ Temperature is positive
□ Top-k does not exceed vocabulary size
□ Repetition is measured, not only observed
Звычкі, якія ускладнююць адлучэнне бяга
Змена многіх налашоўкаў адразу
Якщо адзін эксперымент зменяе ўсе гэтыя параметры разам, нельга прызначыць рэзультат жаднаму з іх:
Learning rate
Batch size
Dropout
Model size
Context length
Зменяйце адну значныю зменную за кожны эксперымент.
Адгукі працою толькі на прыкладах
Няпрацэйны текст можа выйсці ад недастатковыя падготовкі, слабага декодавання, неправильнага чэкпоінта або токенайзера, переадаптавання або недастатковага адаптавання, і прыклады не можу разлічыць гэтыя ситуацыі. Спачатку разглядзіце метрыкі і тэсты практычнага апарату.
Закрыць паведамленні пра проблемы
Паведамленні, якія згадваюць перакаштаванне тэнзораў, пераход на іншы прыстрой, значэнні NaN або безканечныя, або непасуеючыя ключы чэкпоінта, часта су глыбокімі багамі; з’ясавіце іх перш чым іх закрыць.
Праскакванне тэстаў на працоособленасць
Перад дзейсненням довгага запуску пачніце з малага:
Tiny model
Tiny batch
Short context
Few training steps
Потым поступова расшырюйце масштаб.
Выведзенне кліпання як спосабу рашэння проблемы
Кліпінг можа працаваць з адзінам перадмерным апдэйтом, але сталякі кліпінг значыць, што трэба звернуць увагу на ўсё глыбэйшае: шчыльнасць навучэння, ініцыялізацыю, масштабаванне пазыркі, числовую точнасць або анамаліі ў дадзенах.
Вядомыя: спецыяльна нарабляйце кантрольныя ситуацыі
Пасля таго, як вы бачыце, як тэст не працуе, вы больш даверяеце яму, таму кожны вядомы прыклад містить вядомы баг.
Адзёрвайце змяну цэлі
Зробіце вхідныя даны і цэлі ідэнтычнымі, паказайце, што тэст пакета не працуе, а потым вярніце пачатковую змяну.
Увядзіце токен, які выходзіць за межы
Значэнне ID адного токена задаецца як:
tokenizer.vocab_size
Пераканаленне межы павінна не працаваць, таму што высокаяя дзееспроможная ID ёст:
vocab_size - 1
Абыякаваце каузальным маскам
Тымчасова адзёрвайце маску і запускайце тэст на незалежнасць ад будучыні; змена толькі суфікса тепер павінна зменіць логіты прыфакса.
Іспользуйце абсурдную шчыльнасць навучэння
Задаецца:
learning_rate = 0.1
Стежыце за падэнням якосці, нормай градыянта, значэнням параметраў і перакананняамі ў наявнасці несконечных значэнняў, а таксама старацеся, каб тэсты праходзілі за мяркавы час.
Заморажваеце модэль
Застосавіце наступныя крокі і паглядзіце, як тэсты градыянта і оптымізатора аддаюць інфармацыю пра ўсё гэта:
for parameter in model.parameters():
parameter.requires_grad = False
Завантажэнне ў неправильную архітектуру
Завантажыце пункт контролю ў модэль, які разлічаецца па аднам з эга, а потым пераканайцеся ў наявнасці памилак з прычынай «нехвацяючая кантрольная пара», «некальківаная кантрольная пара» і «неспакоўнае размера»:
Vocabulary size
Embedding dimension
Number of layers
Порэшанне налашоўкаў dropout
Адрабатайце тэст з аднам пакетам дадзеных з обомі значэннямі і порэшанце, як шыбка кожна з налашоўкаў запам’ятаецца:
dropout = 0.0
dropout = 0.2
Запісваеце адчынны звярнення
Зберагуце гэтыя рынуткі у формате JSON, каб было можна порэшанце тэстаў і воспамінаваць аб памылках:
Tokenizer fingerprint
Vocabulary size
Token range
Batch shape
Initial loss
Expected baseline
Gradient norm
Missing gradients
Parameter update size
Single-batch final loss
Галоўныя выводы
- Заданне, якое завершылася без памылак, можа ўсё равно выучыць неправільную задачу.
[B, T, V], а чыстая модель павінна запусквацца блізка значэнню log(vocab_size).Полны трык:
Environment
→ files
→ tokenizer
→ token IDs
→ batch shifting
→ shapes
→ initial loss
→ causal independence
→ gradients
→ optimizer updates
→ one-batch overfitting
→ full training
→ evaluation
→ generation
За всім гэтым стоіць адна правіла: не дыбагаваць на інтуўіцыі; напісаць тэст, які ізалюе адну падазанне, параболіць яе і працаваць далей.
Наступны логічны крок — кращая представлення дадзэнняў. Такія элементы, як симвалы, ствараюць дужа дзяўныя последованасці, а словнікі вырастаюць до вельмі вялікага розмеру; кодаванне па парах байтоў выучае способы злічэння частых символьных последованасцей, скорачаючы іх, тады ў однам окне контексту можа размістіцца больш тексту. Адмаўленне гэтага падходу значыць неабходнасць трэніравання способаў злічэння, перекодавання дадзэнняў WikiText-2 і змяны розмеру словніка модэлю, пры чым усе пераказаны тут правіла застаюцься без змян:
Character tokens
→ learned subword merges
→ shorter sequences
→ better use of the context window
Спадневана літэратура
- The Next-Token Loop: A Mental Model of LLMs Before You Touch Agents — Дазвольце дазнацца, як працуюць токены, акна контэксту, процес выбору дадзеных і цыкл гэнеравання, за дапамою невялікіх прыкладаў на Python, якія адказваюць на пытанне, чаму існуюць RAG, ReAct і LangGraph.
- From a One-Node Chatbot to an MCP-Backed Agent in LangGraph — Створыце прыклад аплікацыі на LangGraph шаг за шагам: стан і функціі змены стану, рэшткі спрыяння, циклы выкарыстоўвання інструментаў, задпісаныя потокі, тры режыму стрімавання дадзеных і інструменты, якія працуюць через MCP.