Смешанная точность — FP32, BF16, FP8
Как устроены числа с плавающей точкой, почему LLM обучают в BF16 и FP8 и как DeepSeek-V3 заставила FP8 работать на масштабе 671B с помощью поблочного масштабирования.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Каждое число в нейросети где-то хранится и с чем-то перемножается. Модель на 7 миллиардов параметров в обычном формате FP32 (4 байта на число) весит 28 ГБ, в BF16 — 14 ГБ, в FP8 — 7 ГБ. Скорость тоже зависит от формата: тензорные ядра GPU Hopper (H100/H800) — специальные блоки для умножения матриц — умножают их в FP8 примерно вдвое быстрее, чем в BF16, а в BF16 — во много раз быстрее, чем в FP32. Меньше бит — меньше памяти, меньше трафика, больше операций в секунду.
Платить приходится точностью. Слишком грубые числа превращают маленькие градиенты в ноль, а большие активации — в бесконечность, и обучение разваливается. смешанная точностьсмешанная точностьEN mixed precision — набор приёмов, который позволяет считать в низкой точности там, где можно, и держать высокую там, где нужно. Это первый рычаг эффективности, о котором вы читали в уроке china/02. DeepSeek-V3 первой из открытых моделей такого масштаба провела почти все матричные умножения в 8-битном формате. Как именно — главный сюжет урока.
Как устроено число с плавающей точкой
Заголовок раздела «Как устроено число с плавающей точкой»число с плавающей точкойчисло с плавающей точкой (float)EN floating-point numberСпособ хранить дробные числа как «мантисса × 2 в степени». Форматы: FP32 (4 байта), FP16 и BF16 (2 байта), FP8 (1 байт). хранится в двоичной «научной записи», как , только по основанию 2:
Что здесь написано:
- — знак, один бит;
- — порядок (exponent), бит. Он отвечает за масштаб: сколько «нулей» у числа. Больше бит порядка — шире диапазон;
- — мантисса, бит. Она отвечает за точность: сколько значащих цифр. Между и помещается ровно ступенек;
- bias — сдвиг, чтобы порядок мог быть отрицательным.
Пример: 0.3 в FP8 E4M3 (4 бита порядка, 3 бита мантиссы). Запишем . Мантисса из 3 бит даёт 8 ступенек между 1 и 2 с шагом : 1, 1.125, 1.25, … Ближайшая к 1.2 — это . Получаем : ошибка 4%. В BF16 (7 бит мантиссы, шаг ) то же число станет 0.30078, ошибка 0.26%.
Главное правило: относительная точность одинакова на любом масштабе (в пределах так называемых нормальных чисел, о самых маленьких — ниже). Шаг между соседними числами растёт вместе с числом. Величину (расстояние от 1 до следующего числа) называют машинным эпсилон.
| Формат | Биты: знак / порядок / мантисса | Максимум | (точность) | Наименьшее нормальное | Где используется |
|---|---|---|---|---|---|
| FP32 | 1 / 8 / 23 | мастер-веса, оптимизатор, накопление сумм | |||
| FP16 | 1 / 5 / 10 | 65 504 | обучение до 2020 года, инференс | ||
| BF16 | 1 / 8 / 7 | стандарт обучения LLM | |||
| FP8 E4M3 | 1 / 4 / 3 | 448 | 0.125 | 0.0156 | веса и активации (у DeepSeek-V3 — и градиенты) |
| FP8 E5M2 | 1 / 5 / 2 | 57 344 | 0.25 | градиенты в классическом FP8-рецепте | |
| FP4 E2M1 | 1 / 2 / 1 | 6 | 0.5 | 1 | веса экспертов, инференс (2025–2026) |
Самое интересное в таблице — пара FP16 и BF16. Оба занимают 2 байта, но делят биты по-разному. FP16 точнее, но диапазон
у него узкий: всё, что больше 65 504, становится inf. BF16BF16 (bfloat16)EN bfloat16 (brain floating point)16-битный формат: 1 бит знака, 8 бит порядка (как у FP32), 7 бит мантиссы. Диапазон как у FP32, точность — 2–3 значащие цифры. Стандарт обучения LLM. (brain floating point, формат Google Brain) взял у FP32 все 8 бит порядка
и пожертвовал мантиссой. Он грубее, зато «видит» те же числа от до , что и FP32.
Проверьте всё это сами (PyTorch 2.1+, работает на CPU):
import torch
# 1. Паспорт форматов: сколько бит, максимум, точность около единицы (eps)for name, dt in [("FP32", torch.float32), ("FP16", torch.float16), ("BF16", torch.bfloat16), ("FP8 E4M3", torch.float8_e4m3fn), ("FP8 E5M2", torch.float8_e5m2)]: fi = torch.finfo(dt) print(f"{name:9s} бит={fi.bits:2d} max={fi.max:<12.4g} eps={fi.eps:<10.3g} min_normal={fi.smallest_normal:.3g}")
# 2. Одно и то же число в разных форматахx = torch.tensor(0.3)for dt in [torch.float16, torch.bfloat16, torch.float8_e4m3fn]: print(dt, x.to(dt).float().item())
# 3. Три беды низкой точностиprint(torch.tensor(70000.0).to(torch.float16)) # переполнение FP16print(torch.tensor(1e-8).to(torch.float16)) # исчезновение (underflow) в FP16print(torch.tensor(1e-8).to(torch.bfloat16)) # BF16 держитone = torch.tensor(1.0, dtype=torch.bfloat16)print(one + 0.001, one + 0.003) # поглощение: малое прибавили к большому — и пропалоFP32 бит=32 max=3.403e+38 eps=1.19e-07 min_normal=1.18e-38FP16 бит=16 max=6.55e+04 eps=0.000977 min_normal=6.1e-05BF16 бит=16 max=3.39e+38 eps=0.00781 min_normal=1.18e-38FP8 E4M3 бит= 8 max=448 eps=0.125 min_normal=0.0156FP8 E5M2 бит= 8 max=5.734e+04 eps=0.25 min_normal=6.1e-05torch.float16 0.300048828125torch.bfloat16 0.30078125torch.float8_e4m3fn 0.3125tensor(inf, dtype=torch.float16)tensor(0., dtype=torch.float16)tensor(1.0012e-08, dtype=torch.bfloat16)tensor(1., dtype=torch.bfloat16) tensor(1., dtype=torch.bfloat16)Три беды из последнего блока — переполнение, исчезновение и поглощение — и есть всё, от чего защищает смешанная точность.
Рецепт смешанной точности
Заголовок раздела «Рецепт смешанной точности»Классическая работа Micikevicius и соавторов (NVIDIA и Baidu, 2017) сформулировала рецепт, который с небольшими изменениями используют до сих пор. В нём три правила.
Правило 1. Мастер-веса в FP32. Шаг оптимизатора — это , и обычно в тысячи раз меньше . Вы только что видели: в BF16 . Если хранить веса в BF16, маленькие обновления просто исчезнут, и модель перестанет учиться. Поэтому настоящие веса — мастер-весамастер-весаEN master weightsКопия весов в FP32, к которой применяется шаг оптимизатора. Для вычислений с неё делают копию в BF16/FP8. Без мастер-весов маленькие обновления «поглощаются» при округлении. — живут в FP32, а для вычислений с них делают BF16-копию.
Правило 2. Накопление сумм в FP32. Матричное умножение — это суммы из тысяч произведений. Входы могут быть грубыми, но сумма должна быть точной. Иначе случится вот что:
import torch
vals = torch.full((10_000,), 0.01).to(torch.bfloat16) # 10 000 слагаемых по 0.01, ответ ≈ 100acc16 = torch.tensor(0.0, dtype=torch.bfloat16)acc32 = torch.tensor(0.0)for v in vals: acc16 = acc16 + v # аккумулятор в BF16 acc32 = acc32 + v.float() # те же слагаемые, аккумулятор в FP32print(f"BF16-аккумулятор: {acc16.item():.3f} FP32-аккумулятор: {acc32.item():.3f}")BF16-аккумулятор: 4.000 FP32-аккумулятор: 100.098Сумма в BF16 застряла на 4. Около четвёрки шаг BF16 равен , половина шага больше 0.01, и каждое новое слагаемое округляется обратно к 4. Поэтому тензорные ядра принимают входы в BF16, а складывают в FP32. (Ответ 100.098, а не 100, потому что само 0.01 в BF16 хранится как 0.010009…)
Правило 3. Loss scaling для FP16. Градиенты глубоких сетей бывают порядка и меньше. Наименьшее нормальное число FP16 — (см. таблицу). Ниже него формат ещё держит так называемые субнормальные числа, у которых меньше значащих бит, но самое маленькое из них — , и всё, что меньше его половины (около ), округляется в ноль. Решение — масштабирование lossмасштабирование loss (loss scaling)EN loss scalingLoss умножают на большое число S перед обратным проходом, чтобы маленькие градиенты не обнулились в FP16, а перед шагом оптимизатора делят на S в FP32. Для BF16 не нужно.: умножить loss на большое число перед обратным проходом. По цепному правилу все градиенты тоже умножатся на и поднимутся в «видимую» зону FP16. Перед шагом оптимизатора их делят на уже в FP32:
import torch
torch.manual_seed(0)grads = 10 ** (torch.rand(100_000) * 8 - 10) # «градиенты» от 1e-10 до 1e-2
def through_fp16(g, scale): """Умножаем на scale, храним в FP16, делим обратно уже в FP32 (как делает GradScaler).""" return (g * scale).to(torch.float16).float() / scale
print("масштаб обнулилось ошибка>1% inf")for k in [0, 8, 16, 24]: g16 = through_fp16(grads, 2.0**k) zero = (g16 == 0).float().mean().item() bad = ((g16 - grads).abs() / grads > 0.01).float().mean().item() inf = torch.isinf(g16).float().mean().item() print(f"2^{k:<2d} {zero:8.1%} {bad:8.1%} {inf:5.1%}")масштаб обнулилось ошибка>1% inf2^0 30.8% 50.5% 0.0%2^8 0.9% 20.3% 0.0%2^16 0.0% 0.0% 0.0%2^24 0.0% 5.1% 5.1%Без масштаба треть градиентов исчезает. При всё в порядке, а при большие градиенты уже переполняются.
Подбирать руками неудобно, поэтому используют динамический loss scaling (torch.amp.GradScaler): начинают с ,
при появлении inf пропускают шаг и делят пополам, после 2000 удачных шагов подряд — удваивают.
flowchart LR M["Мастер-веса FP32"] -->|"копия в BF16"| W["Веса BF16"] W --> F["Прямой проход:<br/>умножения BF16,<br/>суммы FP32"] F --> L["Loss в FP32<br/>(для FP16: × S)"] L --> B["Обратный проход<br/>в BF16"] B --> G["Градиенты → FP32<br/>(для FP16: ÷ S)"] G --> O["Шаг AdamW<br/>в FP32"] O --> M
Как это выглядит в коде
Заголовок раздела «Как это выглядит в коде»В PyTorch всё это делает torch.autocast. Внутри блока матричные умножения автоматически идут в BF16, а чувствительные
операции (softmax, нормализации, функции потерь) PyTorch сам держит в FP32:
import torchimport torch.nn as nnimport torch.nn.functional as F
torch.manual_seed(0)model = nn.Sequential(nn.Linear(64, 256), nn.GELU(), nn.Linear(256, 10)) # веса в FP32 — это и есть мастер-весаopt = torch.optim.AdamW(model.parameters(), lr=1e-3)x = torch.randn(512, 64)y = x[:, :10].argmax(dim=1) # игрушечная задача: номер максимальной из первых 10 координат
for step in range(201): with torch.autocast(device_type="cpu", dtype=torch.bfloat16): # на GPU: device_type="cuda" logits = model(x) # матричные умножения идут в BF16 loss = F.cross_entropy(logits.float(), y) # вероятности и loss — в FP32 opt.zero_grad() loss.backward() # градиенты приходят к FP32-весам opt.step() if step % 50 == 0: print(f"шаг {step:3d}: loss = {loss.item():.3f}")
print("logits:", logits.dtype, "| веса:", model[0].weight.dtype, "| градиент:", model[0].weight.grad.dtype)шаг 0: loss = 2.324шаг 50: loss = 0.832шаг 100: loss = 0.126шаг 150: loss = 0.037шаг 200: loss = 0.019logits: torch.bfloat16 | веса: torch.float32 | градиент: torch.float32Сколько это стоит в памяти
Заголовок раздела «Сколько это стоит в памяти»Посчитаем байты на один параметр при обучении с AdamW (оптимизатор из урока 1.4.2):
| Что хранится | Формат | Байт на параметр |
|---|---|---|
| Рабочая копия весов | BF16 | 2 |
| Градиенты | BF16 | 2 |
| Мастер-веса | FP32 | 4 |
| Первый момент Adam | FP32 | 4 |
| Второй момент Adam | FP32 | 4 |
| Итого | 16 |
Модель на 7B требует ГБ ещё до активаций (полный расчёт для ваших 4×RTX 4080, с активациями и FSDP, — задача 1 в уроке 11.3.3: так это спрашивают на собеседованиях). Ни на одну видеокарту это не помещается, и именно отсюда растёт распределённое обучение и ZeRO из урока 5.4.3. Заметьте: веса для вычислений занимают лишь 2 байта из 16. Основную память съедает состояние оптимизатора. DeepSeek-V3 хранит моменты AdamW в BF16 и экономит на этом 4 байта на параметр.
FP8: восемь бит на всё
Заголовок раздела «FP8: восемь бит на всё»У FP8FP8 (8-битное число с плавающей точкой)EN FP8 (8-bit floating point)Два варианта: E4M3 (точнее, максимум 448) и E5M2 (грубее, максимум 57 344). Требует масштабирования тензоров. DeepSeek-V3 обучалась в FP8 с поблочными масштабами. два варианта. E4M3 точнее (3 бита мантиссы), но его максимум всего 448. E5M2 грубее, зато доходит до 57 344. Классический рецепт NVIDIA (библиотека Transformer Engine): веса и активации в E4M3, градиенты в E5M2, потому что разброс градиентов больше.
Диапазон FP8 настолько узок, что тензор нельзя просто привести к FP8. Сначала его делят на масштаб (scale), чтобы самый большой по модулю элемент стал ровно 448, а после умножения результат умножают обратно:
Пример: в тензоре максимум по модулю 896, значит . Элемент 0.6 превращается в , округляется до и восстанавливается как .
Проблема: выбросы
Заголовок раздела «Проблема: выбросы»В активациях LLM встречаются выбросы (outliers): отдельные элементы в тысячи раз больше остальных. Если масштаб один на весь тензор, его задаёт выброс. Обычные числа делятся на огромное , проваливаются в самую грубую область FP8 или в ноль.
Числа (из задания ниже): у активаций с одним выбросом относительная ошибка умножения на матрицу весов для остальных токенов такая:
| Выброс | Один масштаб на тензор | Тайлы + блоки |
|---|---|---|
| 3.7% | 3.7% | |
| 3.8% | 3.7% | |
| 12.8% | 3.7% | |
| 95.2% | 3.7% |
Порядок из 4 бит спасает от умеренных выбросов, но не от огромных. Поблочный масштаб держит ошибку на уровне погрешности самого формата при любом выбросе.
Как DeepSeek-V3 обучалась в FP8
Заголовок раздела «Как DeepSeek-V3 обучалась в FP8»В техотчёте V3 FP8 стоит на четырёх решениях. Все три основных матричных умножения линейного слоя (прямой проход, градиент по входу, градиент по весам) идут в FP8.
1. Мелкозернистое масштабирование (мелкозернистое (поблочное) квантованиемелкозернистое (поблочное) квантованиеEN fine-grained quantizationСвой масштаб у каждой маленькой группы чисел, а не у всего тензора. В DeepSeek-V3: тайлы 1×128 для активаций и блоки 128×128 для весов. Выброс портит только свою группу.). Масштаб считается не на тензор, а на маленькую группу:
- активации — тайлами : один токен, 128 каналов подряд. Выброс одного токена портит только свой тайл;
- веса — блоками . Квадратный блок остаётся блоком и после транспонирования, а в обратном проходе веса используются транспонированными. Поэтому одни и те же квантованные веса годятся и для прямого, и для обратного прохода.
Масштаб считается «на лету» по текущему тензору, без истории прошлых шагов. Цена — лишние числа в памяти: один FP32-масштаб на 128 байт тайла добавляет 3%, на блок — 0.02%.
2. Один формат E4M3 для всего, включая градиенты. Когда масштаб свой у каждой группы из 128 чисел, разброс внутри группы невелик. Хватает диапазона E4M3, а лишний бит мантиссы даёт точность.
3. Точное накопление. Авторы обнаружили, что тензорные ядра H800 накапливают FP8-произведения примерно с 14 битами точности, а не с 32. На сумме длины максимальная относительная ошибка достигает почти 2%. Решение: каждые элементов частичная сумма переносится в FP32-регистры на обычных CUDA-ядрах (универсальных вычислительных блоках GPU) и складывается там. Вот грубая модель этого эффекта (numpy):
import numpy as np
def keep_bits(x, bits=14): """Грубая модель «узкого» сумматора: оставляем bits значащих битов, остальное отбрасываем.""" if x == 0: return 0.0 step = 2.0 ** (np.floor(np.log2(abs(x))) - bits + 1) return float(np.trunc(x / step) * step)
def dot(a, b, promote_every=None): total, acc = 0.0, 0.0 # total — FP32 на CUDA-ядрах, acc — аккумулятор тензорного ядра for i, p in enumerate(a * b): acc = keep_bits(acc + p) if promote_every and (i + 1) % promote_every == 0: total, acc = total + acc, 0.0 # перенос частичной суммы в высокую точность return total + acc
rng = np.random.default_rng(0)print(" K без переноса перенос каждые 128")for K in [256, 1024, 4096, 16384]: a, b = rng.uniform(0, 1, K), rng.uniform(0, 1, K) # положительные слагаемые — худший случай exact = float(a @ b) e1 = abs(dot(a, b) - exact) / exact e2 = abs(dot(a, b, 128) - exact) / exact print(f"{K:5d} {e1:12.3%} {e2:16.3%}") K без переноса перенос каждые 128 256 0.602% 0.300% 1024 2.003% 0.258% 4096 7.811% 0.274%16384 25.908% 0.271%Модель грубее настоящего железа, поэтому числа хуже, чем у DeepSeek. Но характер тот же: без переноса ошибка растёт с длиной суммы, с переносом — остаётся постоянной.
4. Высокая точность там, где FP8 опасен. В BF16 или FP32 остались эмбеддинги, выходной слой, роутер MoE, нормализации и само внимание. На них приходится малая доля вычислений, а ошибки там дороже всего: вспомните «Китайский угол» урока 1.3.2 про logits и softmax. Мастер-веса и градиенты весов — в FP32, моменты AdamW — в BF16.
Итог по отчёту: относительное отклонение loss FP8-обучения от BF16-обучения — меньше 0.25%, то есть в пределах случайного шума между запусками.
| Классический FP8 (Transformer Engine) | DeepSeek-V3 | |
|---|---|---|
| Гранулярность масштаба | тензор | тайл / блок |
| Как выбирают масштаб | по истории max за прошлые шаги | по текущему тензору |
| Форматы | E4M3 вперёд, E5M2 для градиентов | E4M3 везде |
| Накопление | в тензорных ядрах | перенос в FP32 каждые 128 элементов |
Тренд: FP4
Заголовок раздела «Тренд: FP4»Следующая ступень — 4 бита. В формате E2M1 всего 15 различных чисел: . С таким набором масштаб на тензор бессмыслен, поэтому FP4 существует только вместе с очень мелкими блоками:
- MXFP4 (открытый стандарт OCP Microscaling): блок из 32 чисел и общий масштаб в виде степени двойки (формат E8M0);
- NVFP4 (NVIDIA): блок из 16 чисел, масштаб в FP8 E4M3 плюс общий FP32-масштаб тензора.
FP4 на тензорных ядрах появился в GPU поколения Blackwell. Обучать целиком в 4 битах пока не умеют. Типичный приём — обучение с учётом квантования (обучение с учётом квантованияобучение с учётом квантования (QAT)EN quantization-aware training (QAT)В прямом проходе модель видит уже округлённые (например, 4-битные) веса и учится быть к этому устойчивой. Так получены INT4-веса Kimi K2 Thinking.): в прямом проходе модель видит веса, округлённые до 4 бит, и учится быть к этому устойчивой. Так Moonshot выпустила Kimi K2 Thinking с весами в INT4 (4-битные целые числа, урок 7.2.1; ноябрь 2025). По техотчётам 2026 года, DeepSeek-V4 обучает экспертов в FP4 через QAT: мастер-веса в FP32 квантуются в FP4 и без потерь деквантуются в FP8 для вычислений. Kimi K3 с этапа SFT учится с весами экспертов в MXFP4 и активациями в MXFP8. Подробно о квантовании для инференса — в уроке 7.2.1.
Направление движения видно по блоку масштаба: тензор → → → 32 → 16. Чем меньше бит на число, тем мельче группа, в которой у чисел общий масштаб.
Практика
Заголовок раздела «Практика»Реализуйте округление в FP8 E4M3 на numpy (в браузере нет torch.float8), затем три способа масштабирования: на тензор,
тайлами и блоками . Кнопка «Запустить» выведет таблицу ошибок с выбросами разной силы.
import numpy as np
E4M3_MAX = 448.0 # самое большое число в FP8 E4M3
E4M3_MIN_NORMAL = 2.0**-6 # самое маленькое «нормальное» число E4M3
def fp8_e4m3(x):
"""Округляет каждый элемент x к ближайшему числу формата FP8 E4M3 и возвращает float-массив.
Правила формата (1 бит знака, 4 бита порядка, 3 бита мантиссы):
* модуль больше 448 «насыщается» до 448 (знак сохраняется);
* для |x| >= 2^-6 число лежит в «октаве» [2^e, 2^(e+1)), e = floor(log2|x|),
и в каждой октаве ровно 8 ступенек: шаг = 2^(e - 3);
* для |x| < 2^-6 (субнормальные числа) шаг постоянный: 2^(-6 - 3) = 2^-9;
* округление — к ближайшей ступеньке (np.round: при равенстве — к чётному).
Пример: fp8_e4m3([0.3, 500.0]) -> [0.3125, 448.0]
"""
x = np.asarray(x, dtype=np.float64)
# TODO: ваш код
...
def quantize_per_tensor(x):
"""Один масштаб на весь тензор.
s = max|x| / 448 (если max|x| == 0, то s = 1); x_hat = fp8_e4m3(x / s) * s.
Возвращает (x_hat, s), где s — число (np.ndarray формы () или float).
"""
# TODO: ваш код
...
def quantize_tiles(x, tile=128):
"""Активации (M, K): у каждого куска x[i, j*tile:(j+1)*tile] свой масштаб max|кусок| / 448.
Возвращает (x_hat той же формы (M, K), scales формы (M, K // tile)).
Подсказка: x.reshape(M, K // tile, tile).
"""
# TODO: ваш код
...
def quantize_blocks(w, block=128):
"""Веса (K, N): у каждого квадратного блока block×block свой масштаб max|блок| / 448.
Возвращает (w_hat формы (K, N), scales формы (K // block, N // block)).
Подсказка: w.reshape(K // block, block, N // block, block) и max по осям (1, 3).
"""
# TODO: ваш код
...
def rel_error(approx, exact):
"""Относительная ошибка по норме Фробениуса: ||approx - exact|| / ||exact||."""
# TODO: ваш код
...
def fp8_matmul(x, w, mode):
"""Симуляция FP8 GEMM: квантуем оба множителя и перемножаем (накопление в float64).
mode='tensor' — quantize_per_tensor для x и для w;
mode='fine' — quantize_tiles(x) и quantize_blocks(w), как в DeepSeek-V3.
"""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»:
print(fp8_e4m3([0.3, 1.0, 500.0, 0.001])) # ожидается [0.3125, 1.0, 448.0, 0.001953125]
rng = np.random.default_rng(0)
x = rng.normal(size=(64, 512)) # активации: 64 токена × 512 каналов
w = rng.normal(size=(512, 256)) * 0.05 # веса слоя
print("выброс общий масштаб тайлы 1×128 + блоки 128×128")
for outlier in [1e2, 1e3, 1e4, 1e5, 1e6]:
xo = x.copy()
xo[3, 7] = outlier # один «массивный» элемент активаций
ex = xo @ w
keep = np.ones(64, dtype=bool)
keep[3] = False # смотрим на ошибку в ДРУГИХ токенах
yt, yf = fp8_matmul(xo, w, "tensor"), fp8_matmul(xo, w, "fine")
if yt is None or yf is None:
print("fp8_matmul пока ничего не возвращает")
break
print(f"{outlier:7.0e} {rel_error(yt[keep], ex[keep]):13.4f} {rel_error(yf[keep], ex[keep]):10.4f}")
После решения поэкспериментируйте: замените tile=128 на 32 и 512. Как меняется ошибка в строке с выбросом ?
Почему тайл не делают из 4 чисел? Подсказка: посчитайте долю памяти, которая уйдёт на масштабы.
Проверь себя
Заголовок раздела «Проверь себя»- Число с плавающей точкой = знак × мантисса × . Порядок задаёт диапазон, мантисса — точность.
- Смешанная точность: мастер-веса в FP32, умножения в BF16 (или FP8), суммы в FP32. Для FP16 нужен loss scaling, для BF16 — нет.
- Обучение с AdamW стоит ~16 байт на параметр, и большая часть — состояние оптимизатора.
- DeepSeek-V3 сделала FP8 рабочим на масштабе 671B: тайлы и блоки , E4M3 везде, перенос накопления в FP32, чувствительные слои в высокой точности. Отклонение loss от BF16 — меньше 0.25%.
- Дальше — FP4 с блоками по 16–32 числа и QAT.
Дальше: Multi-Token Prediction — как DeepSeek-V3 выжимает из каждого обучающего токена больше сигнала и заодно получает черновик для ускорения генерации.