Выбарка выключаная або seed зафіксаваны, значыць на той жа промпт прыйдзе той жа адказ. Так я да гэтага ставіўся ўвесь год і ні разу не правяраў. Калі лакальная генерацыя не супадала з тым, што я захаваў раней, я казаў сабе “выбарка” і запускаў яшчэ раз, не пытаючыся, ці не змянілася асяроддзе мадэлі. А асяроддзе ў мяне мяняецца раз на некалькі тыдняў, бо я раблю pip install -U кожны раз, калі ў новым рэлізе transformers ёсць патрэбная мне праўка. Аднаго канкрэтнага зламанага вываду, які прымусіў бы мяне палезці ўнутр, не было. Проста канчаўся год і мне хацелася зразумець, што зрабіў з вывадам год такіх абнаўленняў.
Студзеньскія вывады я не захаваў, таму сабраў студзеньскі venv нанова з версій, што былі свежымі тады. Гэта torch 1.10.1 ад 15 снежня 2021 года і transformers 4.15.0 ад 22 снежня 2021 года. У снежаньскім venv стаяць torch 1.13.1 з мінулага чацвярга і transformers 4.25.1. Абодва працуюць на Python 3.9, толькі на CPU, у float32, на 4 vCPU віртуальнай машыны з AMD EPYC 7713. Вагі ўзятыя з GPT-2 small на 124 млн параметраў, адзін і той жа файл pytorch_model.bin ва ўсіх прагонах.
Тэкст не зрушыўся
Я ўзяў 8 промптаў, ад “The capital of France is” да “Q: What is 17 times 23?”, па 60 новых токенаў на кожны. Кожны промпт я прагнаў у трох рэжымах: прагна ў сваім цыкле з argmax, прагна праз generate() і з выбаркай (top_k 50, torch.manual_seed(1234) перад кожным промптам). Я чакаў, што першай зламаецца выбарка: яна залежыць ад усяго размеркавання, а не толькі ад таго, які токен на першым месцы.
Нічога не зламалася. Студзень і снежань далі тыя ж токены на ўсіх 8 промптах ва ўсіх рэжымах, уключна з выбаркай. На арыфметычнае пытанне GPT-2 адказвае ” 17 times 23.” і задае пытанне зноў, па крузе, у абодвух venv.
Лагіты пры гэтым змяніліся. Паўторны запуск у тым жа venv даў тыя ж біты. Снежань разыходзіцца са студзенем у 6 промптах з 8, на першым кроку не больш чым на 0,000107.
Тады я стаў мяняць па адным пакеце. transformers з 4.15.0 на 4.25.1 пры тым жа torch не змяніў ніводнага біта. Код увагі GPT-2 за гэты час у ім мяняўся, значэнне маскі і дзяленне на маштаб, але ніводная з гэтых правак тут біты не зрушыла. torch 1.11.0 дае роўна тыя ж біты, што 1.10.1, а 1.12.1 роўна тыя ж, што 1.13.1. Версію 1.12.0 я не спрабаваў, так што ўся розніца за год хаваецца недзе паміж 1.11.0 і 1.12.1.
Дзе прыступка
2 промпты захавалі свае біты нават пасля гэтай прыступкі і я не разумеў чаму. Спачатку я патраціў час на MKL. З MKL_VERBOSE=1 torch паказвае выклікі SGEMM з MKL 2020.0, гэта значыць матрыцы сапраўды перамнажае MKL. Але з MKL_ENABLE_INSTRUCTIONS=SSE4_2 не змянілася наогул нічога, ніводнага біта ні ў адным venv. Чаму, я дагэтуль не ведаю.
Потым я паспрабаваў ATEN_CPU_CAPABILITY=default. З ім torch бярэ свае ўніверсальныя ядры замест ядраў AVX2, як на старым працэсары. Лагіты зрушыліся максімум на 0,000122 супраць звычайнага снежаньскага прагону. Студзень і снежань сталі пабітна аднолькавымі. Значыць, змена сядзела ў адным з ядраў AVX2.
Я выгрузіў выхад кожнага слоя на промпце з найбольшым разыходжаннем. Гэта арыфметычнае пытанне, у ім 11 токенаў. Эмбедынгі і першы LayerNorm супадаюць. Першае разыходжанне, 0,0000019, з’яўляецца на выхадзе ўвагі ў блоку 0 і да апошняга блока дарастае да 0,00032. Потым я прагнаў кожную аперацыю асобна на фіксаваным выпадковым уваходзе. Адрозніваецца толькі softmax.
У 2 промптаў, якія захавалі біты, 5 і 7 токенаў. Ва ўсіх астатніх 8 і больш. Я прагнаў softmax на радках усіх даўжынь ад 1 да 64. Даўжыні з 1 па 7 даюць тыя ж біты ў абедзвюх версіях. Пачынаючы з 8 у кожнай даўжыні ёсць радкі, якія разыходзяцца, акрамя 23. Думаю, гэта выпадковасць.
8 лікаў float гэта адзін рэгістр AVX2. Астатняе відаць у дыфе aten/src/ATen/cpu/vec/functional_base.h паміж 2 тэгамі. Softmax сумуе экспаненты праз reduce_all, а той трымае 8 частковых сум у палосах рэгістра і напрыканцы зводзіць іх у адзін лік. У 1.11.0 гэтая згортка ідзе паласа за паласой: да паласы 0 дадаецца паласа 1, потым паласа 2 і так да паласы 7. У 1.12.1 для AVX2 дадалі хутчэйшую згортку:
// 128-bit shuffle
Vec v1 = _mm256_permute2f128_ps(v, v, 0x1);
v = vec_fun(v, v1);
// 64-bit shuffle
v1 = _mm256_shuffle_ps(v, v, 0x4E);
v = vec_fun(v, v1);
// 32-bit shuffle
v1 = _mm256_shuffle_ps(v, v, 0xB1);
v = vec_fun(v, v1);
Яна складае палосы праз 4, потым праз 2, потым суседнія. Лікі тыя ж, парадак складання іншы, а ў float ад гэтага можа змяніцца апошні біт. Радкі, карацейшыя за адзін рэгістр, да новага кода не даходзяць. Ва ўніверсальных ядрах яго няма зусім. Няма яго і ў зборках кампілятарам MSVC: новая згортка закрытая праверкай на GCC, так што на Windows гэтай прыступкі, хутчэй за ўсё, няма.
Чаму токены выжылі
Зрух на 0,0001 мае значэнне толькі тады, калі зазор паміж першым і другім токенам меншы за яго. На 8 промптах пра гэта нічога не скажаш, таму я ўзяў 200 пачаткаў сказаў з “Алісы ў Краіне цудаў” і згенераваў прагна па 100 токенаў на кожны, 20 000 крокаў на прагон.
Прагонаў было 6, але розных набораў зазораў сярод іх толькі 3. Паўторны снежаньскі прагон і прагон на 1 патоку супадаюць да біта. Абодва прагоны на ўніверсальных ядрах таксама супадаюць, а студзень гэта трэці набор. Усе 200 працягаў аказаліся аднолькавымі ва ўсіх 6 прагонах.
Найменшы зазор паміж першым і другім токенам быў 0,000366, на кроку 14 сказа, які пачынаецца з “So she set to work”. Ён супаў да апошняга біта ва ўсіх прагонах. Ніводная ўмова не зрушыла ніводны зазор больш чым на 0,000172, так што ў самага цеснага кроку быў прыкладна двайны запас.
Прагная GPT-2 хутка зацыкліваецца і ў цыкле яна вельмі ўпэўненая ў сабе. Медыянны зазор на першых 10 кроках роўны 0,70, а на апошніх 10 ужо 6,27. У мадэлі, якая вагаецца часцей, крокаў каля нуля было б больш. Тады і той жа зрух на 0,0001 меў бы больш шанцаў перавярнуць выбар токена.
1 паток замест 4 таксама зрушыў лагіты першага кроку, не больш чым на 0,0000229 у абодвух venv. Ніводнага токена ён не змяніў. Чаму, я не разбіраўся.
Чаго я не правяраў
- GPU і float16, дзе парадак складання залежыць ад іншых ядраў, якія я не чытаў.
- Батчынг на серверы, з якім маюць справу карыстальнікі воблачных API. У мяне батч 1 і машына нічым больш не занятая.
- Іншы сапраўдны працэсар. У AVX-512 у тым жа файле свая згортка. Стары працэсар я толькі імітаваў зменнай асяроддзя.
- Сам каміт. Я чытаў дыф паміж тэгамі v1.11.0 і v1.12.1, а не сам pull request.
- Большыя мадэлі і даўжэйшыя вывады. 20 000 крокаў зацыкленай мадэлі на 124 млн параметраў гэта не старонка тэксту ад большай мадэлі.
На гэтай машыне я магу перастаць спісваць змененыя вывады на выбарку, прынамсі з GPT-2. Але ў самага цеснага кроку запас быў толькі двайны і для мадэлі, якая зацыкліваецца радзей, я б гэтага не абяцаў.