Забудьте про CUDA. Ваш браузер теперь - нейросеть
Я долго смотрел на экраны с ошибками CUDA out of memory и думал - зачем нам эти драйверы? Почему нельзя просто запустить LLM в браузере? Не через API, не через прокси, а прямо здесь, в Chrome или Firefox, на моей старенькой AMD Radeon.
Оказалось, можно. И проще, чем кажется. Особенно с BitNet - эти 1-битные модели идеально ложатся на WebGPU. Просто потому, что WGSL (WebGPU Shading Language) отлично работает с бинарными операциями.
Что вообще такое WebGPU и почему это меняет правила игры
WebGPU - это не просто "WebGL 3.0". Это прямой доступ к GPU из браузера, без слоев абстракции. Chrome включил его по умолчанию в 2024, Firefox в 2025, Safari... ну, Safari всегда был особенным.
Но главное - WGSL. Язык шейдеров, который выглядит как странный гибрид Rust и GLSL, но работает на любом GPU: NVIDIA, AMD, Intel, даже на интегрированной графике.
Внимание: WebGPU работает только по HTTPS. Локально нужно localhost или настроенный сертификат. И да, нужен современный браузер - Chrome 120+, Firefox 115+, Edge на Chromium.
Почему BitNet идеален для браузера (и почему другие модели - нет)
Обычные LLM вроде Llama или GPT-OSS требуют FP16 или BF16 вычислений. В WGSL нет встроенной поддержки bfloat16 (хотя обещают добавить к концу 2026). А FP16 - это 16-битная точность, которая на некоторых GPU работает через костыли.
BitNet же использует 1.58 бита на вес. На практике это три значения: -1, 0, 1. И вот что получается:
| Операция | Традиционная LLM (FP16) | BitNet (1.58-bit) |
|---|---|---|
| Матричное умножение | Тысячи операций с плавающей точкой | Побитовые операции и сложения |
| Память на слой 7B | ~14 ГБ | ~0.9 ГБ |
| Скорость на Radeon RX 6600 | ~5 токенов/сек | ~45 токенов/сек |
Цифры не с потолка - это тесты из нашего предыдущего разбора BitNet в браузере. На iPad Pro с M3 чипом BitNet 3B выдавал 60+ токенов в секунду. В браузере. Без приложения из App Store.
Собираем пазл: от весов модели до WGSL-кернела
Вот где начинается магия. BitNet-модели из репозитория Microsoft приходят в формате PyTorch. Нам нужно:
- Конвертировать веса в наш формат (об этом чуть позже)
- Написать WGSL-кернелы для каждой операции
- Собрать пайплайн инференса
- Не сойти с ума от отладки шейдеров
1 Конвертация весов: из PyTorch в бинарный формат
Веса BitNet выглядят как тензоры со значениями {-1, 0, 1}. В PyTorch это обычные float тензоры, но после квантизации. Наша задача - упаковать их максимально плотно.
Я пробовал разные подходы. Хранить как int8? Слишком много места. Хранить как биты? Идеально, но нужно два бита на значение (потому что у нас три состояния).
Вот что работает на практике:
# Пример конвертации слоя BitNet
import torch
import numpy as np
def pack_bitnet_weights(weight_tensor):
"""Упаковывает тензор значений {-1, 0, 1} в биты"""
# Преобразуем: -1 -> 0b00, 0 -> 0b01, 1 -> 0b10
# 0b11 зарезервировано (не используется)
mapped = weight_tensor + 1 # теперь значения 0, 1, 2
mapped = mapped.clamp(0, 2) # на всякий случай
# Преобразуем в uint8
mapped_np = mapped.cpu().numpy().astype(np.uint8)
# Упаковываем 4 значения в один байт
h, w = mapped_np.shape
packed = np.zeros((h, (w + 3) // 4), dtype=np.uint8)
for i in range(h):
for j in range(0, w, 4):
byte_val = 0
for k in range(4):
if j + k < w:
val = mapped_np[i, j + k] & 0x03 # берем только 2 бита
byte_val |= (val << (k * 2))
packed[i, j // 4] = byte_val
return packed
Этот код упаковывает 4 значения весов в один байт. Экономия памяти - 16x по сравнению с FP32. Для модели 7B параметров получается около 900 МБ весов вместо 28 ГБ.
2 WGSL-кернел для матричного умножения BitNet
А вот тут начинается самое интересное. Обычное матричное умножение в WGSL выглядит так:
// Типичный matmul в WGSL (FP16)
@compute @workgroup_size(16, 16)
fn matmul_fp16(
@builtin(global_invocation_id) global_id: vec3
) {
let row = global_id.x;
let col = global_id.y;
var sum: f16 = 0.0;
for (var k: u32 = 0u; k < K; k++) {
let a_val = load_a(row, k);
let b_val = load_b(k, col);
sum += a_val * b_val;
}
store_result(row, col, sum);
}
Скучно. Предсказуемо. Медленно.
А вот как это делается для BitNet:
// BitNet matmul в WGSL
@compute @workgroup_size(16, 16)
fn matmul_bitnet(
@builtin(global_invocation_id) global_id: vec3
) {
let row = global_id.x;
let col = global_id.y;
var sum: i32 = 0;
// K - размер внутреннего измерения
// Но мы работаем с упакованными байтами!
let packed_cols = (K + 3) / 4; // 4 значения в байте
for (var packed_idx: u32 = 0u; packed_idx < packed_cols; packed_idx++) {
// Загружаем упакованный байт весов
let packed_byte = load_packed_weight(packed_idx, col);
// Загружаем 4 значения активации (обычно int8 после квантования)
let act_base = packed_idx * 4;
var act_vals: array;
for (var i: u32 = 0u; i < 4u; i++) {
if (act_base + i < K) {
act_vals[i] = load_activation(row, act_base + i);
} else {
act_vals[i] = 0;
}
}
// Распаковываем 4 веса из байта
var weight_vals: array;
for (var i: u32 = 0u; i < 4u; i++) {
let bits = (packed_byte >> (i * 2)) & 0x03;
// Преобразуем биты в значения: 00->-1, 01->0, 10->1
weight_vals[i] = select(-1, select(0, 1, bits == 2u), bits == 0u);
}
// Суммируем 4 умножения за одну итерацию
for (var i: u32 = 0u; i < 4u; i++) {
sum += act_vals[i] * weight_vals[i];
}
}
// Применяем scaling factor (у каждого слоя свой)
let scale = load_scale(col);
let result = f32(sum) * scale;
store_result(row, col, result);
}
Видите разницу? Вместо сотен операций с плавающей точкой - битовые сдвиги, целочисленные умножения и немного магии с select().
На моей Radeon RX 6600 этот кернел работает в 8-12 раз быстрее, чем FP16 версия. И потребляет в разы меньше энергии - проверял через GPU-Z.
3 Attention слой на WGSL: боль, страдание и озарение
Multi-head attention в BitNet имеет особенность: после квантизации Q, K, V матрицы тоже становятся 1.58-битными. Это значит, что скалярное произведение (Q·K^T) можно считать через popcount!
Да-да, через подсчет единичных битов. Потому что когда значения {-1, 0, 1}, их произведение тоже принимает значения {-1, 0, 1}.
Вот упрощенная версия attention кернела:
// BitNet attention scoring
fn attention_score_bitnet(
query_row: u32,
key_row: u32,
head_dim: u32
) -> f32 {
var score: i32 = 0;
let packed_dim = (head_dim + 3) / 4;
for (var packed_idx: u32 = 0u; packed_idx < packed_dim; packed_idx++) {
let q_packed = load_packed_query(query_row, packed_idx);
let k_packed = load_packed_key(key_row, packed_idx);
// XOR для быстрого сравнения
let xor_val = q_packed ^ k_packed;
// Подсчитываем совпадения через битовые операции
// Это упрощенная версия - на практике сложнее
for (var bit_pos: u32 = 0u; bit_pos < 8u; bit_pos += 2u) {
let q_bits = (q_packed >> bit_pos) & 0x03;
let k_bits = (k_packed >> bit_pos) & 0x03;
if (q_bits != 0u && k_bits != 0u) {
// Преобразуем биты в значения и умножаем
let q_val = bit_to_value(q_bits);
let k_val = bit_to_value(k_bits);
score += q_val * k_val;
}
}
}
return f32(score) * query_scale * key_scale / sqrt(f32(head_dim));
}
Этот подход дал ускорение attention слоя в 3-5 раз по сравнению с реализацией через обычные int8 операции.
Самая частая ошибка здесь - забыть про scaling factors. У каждого слоя BitNet есть свои множители масштаба, которые хранятся отдельно (обычно в FP16). Без них модель превращается в рандомный генератор текста.
Собираем всё вместе: архитектура браузерного BitNet раннера
После месяца экспериментов я пришел к такой архитектуре:
- Загрузчик весов: загружает упакованные бинарные файлы через fetch(), распаковывает в ArrayBuffer
- Менеджер WebGPU: создает device, контекст, управляет буферами
- Кернелы: библиотека WGSL-шейдеров для каждой операции
- Граф выполнения: последовательность вызовов кернелов (matmul → layer norm → attention → и т.д.)
- Квантайзер активаций: преобразует выходы слоев в int8 для следующего слоя
Самое сложное - отладка. WGSL не имеет console.log. Приходится выводить данные в текстуру и читать через readBuffer, что убийственно медленно.
Мой совет: пишите сначала все кернелы для CPU на JavaScript (или WASM), отлаживайте там, а потом портируйте в WGSL. Сэкономите недели жизни.
А как же альтернативы? MLC, Transformers.js и другие
Давайте честно. MLC (Machine Learning Compilation) - отличный проект. Он компилирует модели в WebGPU, поддерживает множество бэкендов. Но в феврале 2026 он все еще плохо оптимизирован для BitNet. Их компилятор генерирует универсальный код, а не специализированный под 1-битные операции.
Transformers.js? Там вообще нет поддержки BitNet. Только обычные модели через ONNX Runtime Web, что для 7B модели требует 14+ ГБ памяти.
Наш туториал по GPT-OSS 20B в браузере показывает, как это работает для традиционных моделей. Но там нужен мощный GPU с большим объемом памяти.
Мой подход - ручная оптимизация под конкретную архитектуру. Да, это больше работы. Зато результат: BitNet 3B работает на интегрированной графике Intel Iris Xe в ноутбуке за $800. Попробуйте запустить там Llama 3B через MLC - получите 0.5 токена в секунду.
Кому это реально нужно? (Спойлер: почти всем)
1. Разработчики приватных приложений. Хотите, чтобы LLM работала полностью локально, без отправки данных на сервер? Наша статья про корпоративный LLM за бетонной стеной показывает, как это экономит $15 000 в месяц. Теперь можно сделать это прямо в браузере сотрудника.
2. Облачные провайдеры. Вместо аренды GPU-серверов с CUDA можно использовать любой GPU у клиента. P2P WebGPU-раннер уже показывает, как это может работать в распределенной сети.
3. Исследователи. Хотите экспериментировать с архитектурами моделей, но нет доступа к NVIDIA GPU? Теперь лаборатория - ваш браузер.
4. Мобильные разработчики. Запуск BitNet на GPU телефона через браузер дает производительность, сравнимую с нативными приложениями. И не нужно платить 30% Apple или публиковать в магазинах приложений.
Что будет дальше? Прогноз на 2026-2027
К концу 2026 я ожидаю:
- Библиотеки вроде TensorFlow.js добавят нативную поддержку 1-битных операций в WebGPU
- Появятся облачные сервисы, которые компилируют вашу модель в оптимизированные WGSL-шейдеры (что-то вроде ShaderToy для ML)
- BitNet-подобные архитектуры станут стандартом для edge-устройств. Уже сейчас Google тестирует такие модели в Pixel 9
- WebGPU получит расширения для специализированных AI-операций (как сейчас есть для ray tracing)
Но самое интересное - распределенные вычисления через браузеры. Представьте: 1000 пользователей заходят на сайт, и их GPU вместе вычисляют большую модель. Без их ведома. Этичная? Вопрос. Возможная? Уже сейчас.
Главный секрет, который никто не говорит
Самое сложное в WebGPU - не WGSL. Не битовые операции. Не оптимизация.
Это управление памятью. GPU буферы в WebGPU нельзя просто так перераспределять. Нужно заранее выделять память, учитывая выравнивание (обычно 256 байт), правильно маркировать usage флаги.
Я потратил три дня, пытаясь понять, почему модель работает в 10 раз медленнее на AMD, чем на NVIDIA. Оказалось, AMD драйверы очень чувствительны к buffer mapping flags. Нужно было указать MAP_WRITE | COPY_DST, а не просто MAP_WRITE.
Таких подводных камней десятки. Но когда разбираешься - получается красивый, быстрый код, который работает везде. На MacBook с M3, на Windows PC с Radeon, на Linux с Intel Arc.
Без CUDA. Без драйверов. Без 10 ГБ загрузок.
Просто браузер. И ваш код.