Галоўная / Артыкулы / Дэбаггінг малога GPT у PyTorch: тэсты, якія ізольуюць кожную прычыну аблучэння.

Дэбаггінг малога GPT у PyTorch: тэсты, якія ізольуюць кожную прычыну аблучэння.

Этапна схема роботы для дэбаггінгу GPT на рывень симвалаў у PyTorch – ад ідэнтаў токенав і змены цялевых значэнняў да градыентаў, значэнняў NaN і пунктав перапачатку.

6828 слоў

Паракаменты ацэнкі, такія як пазырок, перплексітэт і частка павтарэння, парадкуюць, што маленькая модель 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, переплыв у сумешанай прыцэльнасі, вяроўныя ўжо зіпсаваныя параметры, дзеленне на ноль, логарым з нуля або адзячай значэння, і несконечныя лоджіты. Калі гэта выйшла:

    1. Зупініце запуск.
    2. Знайдзіце пасляпэўны крок з сконечным пазыркам.
    3. Знізьце коефіцыент навчання.
    4. Увялікніце обмежэнне градыянтаў.
  • Парадзейце зноў маску прычыны-наследку.
  • Выключыце режым сумешанай точнасці.
  • Пераканаце параметры і градыенты на наявнасць несконечных значэнняў.
  • Ніколі не продавайце процес апдэйта, калі параметры маюць 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.
  • Стварэнне токенайзера з кодавання пар байтоў з нуля для малага GPT — Рэалізаваць токенайзер BPE на рывень симвалаў у Python, натрэнаваць яго на WikiText-2, запісаць і створыць його «пятэрку», а таксама перенатрэнаваць малага GPT на корачыя, болей ўжоўтвореныя последованасці токенаў.
  • З GPT-1 да модэляў для рассудкавання: што кожная генерацыя зменіла для разработчыкаў — Праследаваць эвалюцыю сямейства GPT ад прадзейнаўкі 2018 года да модэляў для рассудкавання, пазнакамліцца, якія ідэі дадала кожная генерацыя, а таксама вызваць функціі візуальнага розпазнаўання GPT-4o і структураваных выходаў з Python.