FP32, FP16, BF16 и FP8 — разбираемся в основных типах чисел с плавающей запятой
Привет, Хабр! Сегодня давайте поговорим о том, как современные вычисления на GPU стали более гибкими и эффективными благодаря различным форматам чисел с плавающей запятой (FP64, FP32, FP16, BFLOAT16 и FP8). Эти форматы не просто числа — за каждым из них стоит конкретная область применения. В разных ситуациях мы сталкиваемся с задачами, где важны либо скорость, либо точность, и правильно выбранный тип floating point помогает оптимизировать ресурсы. Давайте разберём всё это на примерах и поймём, в каких задачах каждый из этих форматов будет наиболее полезен.
Введение
В предыдущей статье мы рассказали о истории появления чисел с плавающей запятой, но лишь частично рассказали о их применении на практике. Сегодня же мы хотим сконцентрировать ваше внимание на каждом из популярных форматов вычислений и рассказать, почему же существует такое количество разнообразных форматов вычислений. Для корректного формирования выборки типов вычислений, мы будем обращаться к спецификации топового ускорителя для обучения ИИ — Nvidia Tesla H100.
FP64: максимальная точность для научных расчётов
FP64, или 64-битные числа с плавающей запятой, применяются там, где малейшая ошибка может привести к неверным результатам. В таких областях, как космическая индустрия, моделирование траекторий спутников или расчёты по гидродинамике, отклонение даже на малую величину может иметь самые серьёзные последствия.
Задача: Рассмотрим задачу численного интегрирования для расчёта траектории космического объекта, движущегося под воздействием гравитации Земли. Для таких задач крайне важно использовать максимальную точность, чтобы не допустить отклонений в расчётах, которые могут привести к неправильному предсказанию орбиты.
Пример кода для численного интегрирования с использованием FP64:
import torch
# Положение и скорость спутника
initial_position = torch.tensor([7.0e6, 0.0, 0.0], dtype=torch.float64)
initial_velocity = torch.tensor([0.0, 7.12e3, 0.0], dtype=torch.float64)
mass_earth = 5.972e24
g = 6.67430e-11
# Функция для вычисления ускорения
def compute_acceleration(position, mass, g=6.67430e-11):
distance = torch.norm(position)
return -g * mass / distance**3 * position
# Метод Рунге-Кутты для численного интегрирования
def runge_kutta_step(position, velocity, mass, time_step):
k1_v = compute_acceleration(position, mass) * time_step
k1_x = velocity * time_step
k2_v = compute_acceleration(position + k1_x / 2, mass) * time_step
k2_x = (velocity + k1_v / 2) * time_step
return position + (k1_x + 2 * k2_x) / 3, velocity + (k1_v + 2 * k2_v) / 3
# Применение метода
time_step = 1.0 # шаг времени в секундах
position, velocity = runge_kutta_step(initial_position, initial_velocity, mass_earth, time_step)
print(f"Положение: {position}, Скорость: {velocity}")
Зачем здесь нужен FP64? В этой задаче важно, чтобы даже минимальные погрешности не привели к значительным отклонениям в расчётах, поскольку расчёт орбиты требует максимальной точности. FP64 позволяет минимизировать ошибки округления и обеспечивает высокую точность при выполнении численных методов, таких как метод Рунге-Кутты.
FP32: баланс между точностью и скоростью
FP32 — это стандартный 32-битный формат, который используется в большинстве повседневных задач, таких как рендеринг графики, обработка изображений и обучение нейросетей. Он обеспечивает достаточную точность при высокой производительности, что делает его оптимальным выбором для задач, где скорость важнее точности.
Задача: Умножение матриц для обработки изображений в реальном времени. В таких задачах требуется быстрое выполнение вычислений, а точность FP32 достаточна для получения качественных результатов.
Пример кода для умножения матриц с использованием FP32:
import torch
# Инициализация матриц в формате float32
matrix_a = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32)
matrix_b = torch.tensor([[5.0, 6.0], [7.0, 8.0]], dtype=torch.float32)
# Выполнение умножения матриц
result = torch.matmul(matrix_a, matrix_b)
print(f"Результат умножения матриц с FP32: \n{result}")
Зачем здесь нужен FP32? В ряде задач, связанных с рендерингом или обработкой графики, критически важно выполнять расчёты в реальном времени, например, при создании графических эффектов или обработке видео. FP32 обеспечивает хорошее соотношение между точностью и производительностью. Конечно, крайне маловероятно что кто-то будет использовать Python для задач требующих исполнения в реальном времени, но для однородности он тут выбран как и для остальных примеров, хотя C/C++/Rust/Zig подошли бы лучше.
FP16: ускорение обработки данных
FP16 — это 16-битный формат, который позволяет значительно ускорить вычисления за счёт уменьшения точности, но без существенного ущерба для качества результата. Этот формат активно используется в задачах машинного обучения и нейросетей, где важна высокая скорость обработки больших объёмов данных.
Задача: Обучение нейросети для классификации изображений. FP16 позволяет ускорить тренировку модели без значительной потери качества.
Пример кода для работы с FP16:
import torch
import torch.nn as nn
import torch.optim as optim
# Простая нейросеть для классификации
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
return self.fc2(x)
# Инициализация данных и модели
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleNet().to(device, dtype=torch.float16)
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Фиктивные данные для обучения
data = torch.randn(64, 784, dtype=torch.float16).to(device)
target = torch.randint(0, 10, (64,), dtype=torch.long).to(device)
# Прогон обучения
optimizer.zero_grad()
output = model(data)
loss = nn.CrossEntropyLoss()(output, target)
loss.backward()
optimizer.step()
print(f"Потери: {loss.item()}")
Зачем здесь нужен FP16? При обучении больших нейросетей на огромных датасетах время обработки играет решающую роль. FP16 позволяет в несколько раз ускорить процесс обучения на GPU, уменьшив объём занимаемой памяти и снизив потребление ресурсов. Это особенно полезно при обучении глубоких моделей с большими объёмами данных.
BFLOAT16: оптимизация для инференса
BFLOAT16 — это формат, который чаще всего используется для инференса, то есть для выполнения уже обученных моделей. Он позволяет существенно ускорить обработку данных без значительных потерь в точности, что особенно полезно в задачах, связанных с анализом данных в реальном времени.
Задача: Инференс модели для обработки изображений с использованием BFLOAT16 для ускорения вычислений.
Пример кода для инференса с использованием BFLOAT16:
import torch
# Инициализация данных с использованием BFLOAT16
input_data = torch.randn(64, 784, dtype=torch.bfloat16).cuda()
# Фиктивная модель для инференса
class SimpleInferenceNet(nn.Module):
def __init__(self):
super(SimpleInferenceNet, self).__init__()
self.fc = nn.Linear(784, 10)
def forward(self, x):
return self.fc(x)
# Выполнение инференса
model = SimpleInferenceNet().cuda().bfloat16()
output = model(input_data)
print(f"Результат инференса: {output}")
Зачем здесь нужен BFLOAT16? Этот формат используется для ускорения инференса, что особенно важно в задачах, связанных с реальным временем, таких как обработка потоков данных или систем искусственного интеллекта в автономных автомобилях. BFLOAT16 сохраняет точность экспоненциальной части числа, что позволяет работать с большими диапазонами значений, при этом экономя память и ресурсы.
FP8: максимальная производительность для инференса
FP8 — это новый формат, который используется для выполнения операций с максимальной скоростью при минимальных затратах ресурсов. Этот формат хорошо подходит для инференса, где точность не так важна, как скорость, например, в задачах, связанных с компьютерным зрением или распознаванием объектов в реальном времени.
Задача: Выполнение инференса с использованием FP8 для распознавания объектов на изображении.
Пример кода:
import torch
import torch.nn as nn # Import the torch.nn module
# Пример симуляции работы с FP8
def to_fp8(tensor):
return torch.clamp(tensor, min=-128, max=127).to(torch.float32)
# Инициализация данных
input_data = torch.randn(64, 784).cuda()
input_data_fp8 = to_fp8(input_data)
# Простейшая модель для инференса
class SimpleFP8Net(nn.Module):
def __init__(self):
super(SimpleFP8Net, self).__init__()
self.fc = nn.Linear(784, 10)
def forward(self, x):
return self.fc(x)
# Выполнение инференса
model = SimpleFP8Net().cuda()
output = model(input_data_fp8)
print(f"Результат с использованием FP8: {output}")
Зачем здесь нужен FP8? FP8 идеально подходит для задач, где точность может быть снижена в пользу максимальной скорости обработки данных. Это актуально в инференсе для систем искусственного интеллекта, где нужно мгновенно обрабатывать огромные объёмы данных с минимальными затратами ресурсов.
Заключение
Каждый тип float — будь то FP64, FP32, FP16, BFLOAT16 или FP8 — имеет своё применение и должен выбираться в зависимости от задачи. FP64 — для научных расчётов, FP32 — для баланса между производительностью и точностью, FP16 — для обучения нейросетей, а BFLOAT16 и FP8 — для инференса. Современные ускорители Nvidia Tesla, Radeon Instinct или Intel GPU Max поддерживают все эти форматы, что позволяет вам максимально эффективно использовать мощь GPU для каждой конкретной задачи.
А какой тип float используете вы в своих проектах и почему? Делитесь в комментариях, будет интересно обсудить!