
Первый в мире детектор конфликтов (hazard checker) для NVIDIA Blackwell (sm_120) с ассемблером и планировщиком, сверенными байт в байт с собственным компилятором NVIDIA. Та проверка, которую они так и не выпустили.
Проблема · Какие GPU · Как это работает · Быстрый старт · Измерено, а не предположено · Находки · API · Метод · Дорожная карта · Чистая комната
Инструкция NVIDIA GPU состоит из 128 бит, и 21 из них — это вовсе не инструкция. Это управляющее слово планировщика, от stall до reuse: сколько тактов ждать перед выдачей следующей инструкции, какие скорборды сигнализировать, какие ожидать и какие операнды могут быть обслужены из кэша повторного использования.
Аппаратура не проверяет ничего из этого. В sm_120 нет блокировки (interlock) для инструкций с фиксированной задержкой. Кремний доверяет тому, что создало управляющее слово. Если количество тактов ожидания меньше задержки значения, которое потребляет следующая инструкция, не возникает ни сбоя, ни ожидания, ни предупреждения. Инструкция читает ещё не записанный регистр и каждый раз вычисляет на устаревших данных, на полной скорости.
Это странный вид ошибки. Она не приводит к падению. Она не видна в отладчике. Она даёт числа, которые просто неверны, а в умножении матриц или в kernel внимания это означает модель, которая обучается слегка неправильно, а не модель, которая наглядно ломается.
Три самые дешёвые кодировки — сломанные, и нигде об этом не сообщается. Обратите также внимание на левый столбец: stall ноль — это не ноль тактов, а отдельная безопасная кодировка, которая ожидает незавершённые результаты и стоит примерно в девять раз дороже запланированной инструкции. Проверщик, прочитавший её как ноль, назвал бы корректные программы сломанными.
Инструменты, генерирующие машинный код для этой архитектуры, присваивают эти управляющие биты на основе модели задержек. basalt — это то, что проверяет ответ.
NVIDIA даёт вам компилятор, который записывает эти 21 бит. Она не даёт вам ничего, что прочитало бы их обратно и сказало, что они безопасны, — и никто другой тоже не даёт.
Ассемблеры для NVIDIA GPU существуют уже десятилетие, кодировка Blackwell была реверс-инженерингована ранее, есть опубликованные характеристики sm_120 на уровне тактов, и один публичный ассемблер для этой архитектуры уже сам назначает управляющие биты планировщика и запускает свои собственные kernel'ы на карте, чтобы убедиться, что ответы получаются правильными. Всё это правда, и ни одно из этих утверждений не является главным:
Никакому другому инструменту нельзя передать cubin, который он не создавал, и попросить сказать, безопасны ли его управляющие биты планировщика.
Ваш компилятор создал этот cubin, или его поставила библиотека, или кто-то написал его вручную — и до сих пор не было способа спросить. В архитектуре без аппаратной блокировки это разница между «он запустился» и «он корректен», и эта разница невидима: stall на один такт короче читает устаревший регистр и возвращает неверное число на полной скорости, без сбоя и без предупреждения, каждый раз.
Всё остальное здесь существует для того, чтобы сделать это утверждение проверяемым. Ассемблер — это то, что собирает программу с одним намеренно укороченным stall. Планировщик — это то, что заставляет модель зафиксировать ответ, а не оценивать чужой. А аудит — это то, где утверждение перестаёт быть отсутствием и становится измерением: basalt направили на 2,473 cubin'а sm_120, которые NVIDIA поставляет в cuBLAS, cuSOLVER, cuSPARSE, NPP и остальных.
Почему этого не существовало, словами самого сообщества. Самый используемый SASS-ассемблер в своей документации говорит, что «проверка строгой корректности всей программы … [далеко] не возможна без официальной поддержки. Поэтому гарантировать корректность программы предоставлено пользователю, при весьма ограниченной помощи со стороны ассемблера». SIP, занимаясь автонастройкой SASS-расписаний, утверждает, что «валидация невозможна для нативных ассемблерных кодов GPU, поскольку формальная семантика sass закрыта (closed-source)».
Оба утверждения касаются семантической корректности: вычисляет ли kernel то, что должен. basalt на это не отвечает, и ничто здесь на это не претендует. Он отвечает на строго более узкий вопрос, и суть в том, что этот более узкий вопрос разрешим без семантики:
Покрывают ли управляющие биты этой программы её собственные зависимости по данным?
Для этого нужна структура зависимостей, которую раскрывает кодировка, и модель задержек, которую раскрывает кремний при измерении. Ни то, ни другое не требует знать, что вычисляет какая-либо инструкция. Kernel может пройти эту проверку и всё равно быть неверным алгоритмом; чего он не может — так это прочитать регистр до того, как значение приземлится.
Во-вторых, ничто другое не сверяется с собственными байтами вендора. Эталон basalt — вывод ptxas, поэтому расхождение — это баг basalt, пока не доказано обратное. Его ассемблер должен воспроизводить точные 128 бит компилятора. Его планировщик должен отбросить все управляющие биты, выбранные компилятором, вычислить новые и заставить GPU вычислить тот же ответ.
Один стандарт для всего этого: точно соглашаться с вендором — или объяснить, почему нет.
| Компонент | Что делает | Как проверяется | Результат |
|---|---|---|---|
| Ассемблер | Текст SASS в 128-битное слово | Повторно ассемблировать каждую инструкцию, выпущенную ptxas, и сравнить байты, на корпусе и на 5.2M инструкций поставляемого библиотечного кода, который он никогда не видел | 59,693 из 59,760 инструкций корпуса и 4,585,336 из 5,237,448 поставляемых — точные, остальные отклонены по имени, 0 неверных в обоих случаях |
| Проверщик | Читает расписание, сообщает о рисках | Собственный вывод вендора должен проходить проверку начисто, а намеренно укороченный stall должен быть пойман | 0 ошибок на 1,323 парах «kernel вендора и уровень оптимизации», 0 пропущено на 233 сломанных |
| Аудит | Тот же проверщик, на поставляемых библиотеках | Запустить его на производственных sm_120 kernel'ах, исключённых из всех таблиц, которые он читает | 0 ошибок на 2,762 kernel'ах и 10,218,030 зависимостях, все 2,762 полностью проанализированы |
| Планировщик | Назначает каждый управляющий бит с нуля | Отбросить вендорские, вычислить новые, запустить обе версии на GPU на восьми входных наборах, сравнить выходные байты | 439 из 439 сравнимых kernel'ов байт-идентичны на всех трёх уровнях оптимизации |
И та часть, о которой планировщик обычно умалчивает: во что обходится корректность. Расписания basalt тратят 1.05x вендорских тактов выдачи: медленнее на 111 из 1,323 пар «kernel и уровень оптимизации» и дешевле на 842, при этом каждый сравнимый kernel остаётся байт-идентичным на GPU.
Третья строка — та, что изменила остальные три. Проверщик, откалиброванный на корпусе, не может ошибиться на этом корпусе: самый плотный зазор, который компилятор оставлял, является нижней границей — по построению, ровно для того кода, на котором он измерялся. В первый раз, когда этот проверщик увидел код откуда-то ещё, он сообщил о двадцати шести рисках на kernel в JPEG-декодере, который никогда не возвращал неверный пиксель, и все 6,593 были ошибками basalt. Их исправление привело его к нулю, но ноль на одной библиотеке тоже не был доказательством: расширение исключённого набора до трёх библиотек и 5.2 миллиона инструкций вернуло его прямо к 940 и обнаружило ещё пять ошибок модели в дополнение к первым восьми. Тринадцать исправлений, ни одно из которых не принадлежит NVIDIA, и требование, заново извлечённое из 24,311 поставляемых kernel'ов, установило предохраняющий предикат на 13 тактов на 229,567 наблюдениях — это число, которое инъекция отказов измерила на этой карте, намеренно ломая программу. См. находку 32.
Быть дешевле вендора — не повод для самодовольства. basalt планирует каждую зависимость с самым плотным зазором, который ptxas когда-либо оставлял для этой конкретной пары, а ptxas балансирует давление на регистры и память вместе с задержкой выдачи, тогда как этот инструмент оптимизирует одно число. Ему также не поверили сразу: когда коэффициент впервые опустился ниже 1.0, аппаратная круговая проверка сломалась, и число устояло только после того, как обнаруженный им баг был исправлен. Именно поэтому коэффициент закреплён с обеих сторон в тестовом наборе.
Средний столбец — в этом суть. Проверщик и планировщик, использующие одну модель задержек, согласуются друг с другом, оставаясь при этом оба неверными, поэтому ни один не является доказательством для другого; лишь кремний не имеет интереса в этом споре. Запуск планировщика на семи написанных вручную kernel'ах долгое время проходил семь из семи. Запуск на трёхстах выявил сорок один неверный, и каждое исправление в находках появилось в результате наблюдения за тем, как движется это число.
То же самое относится и к входным данным. Устаревшее чтение меняет ответ только тогда, когда устаревшее значение и свежее различаются, поэтому один набор байтов — это одна возможность заметить проблему, и прогон каждого kernel'а против второго, третьего и четвёртого набора сразу выявил предикат переноса, который модель операндов с самого начала читала как источник. Он пережил все проверки вплоть до этого момента, включая сам круговую проверку.
Та же дисциплина определяет, что ассемблеру разрешено делать, и стоит разделить два числа, которыми он располагает.
Покрытие составляет 99.9% корпуса и 87.5% поставляемого библиотечного кода. Корректность — 100%, и именно это число закреплено тестом. Разрыв между ними — это инструкции, которые basalt отклоняет, каждая с указанием поля, которое он не смог разместить, потому что инструмент, который угадывал бы, достиг бы полного покрытия, порождая слова, которые дизассемблируются в правильный текст, но вычисляют что-то другое. Он никогда не выпустил ни одной такой инструкции — на 59,760 инструкциях корпуса и 5,237,448 поставляемых.
Он пришёл к этому только после восьми отдельных раундов уверенных ошибок:
Каждая из них порождала слово, которое ассемблируется, дизассемблируется обратно ровно в тот текст, из которого пришло, и вычисляет что-то другое. Это тот же сбой, для выявления которого существует остальная часть этого репозитория; поэтому все восемь теперь отклоняются с указанием причины, называющей, что поле действительно хранит, и поэтому число инструкций, ассемблирующихся в неверные байты, — это тест, закреплённый на нуле, а не число в таблице.
Девятая ошибка всплыла в первый раз, когда ассемблеру дали машинный код, который он не создавал, и она была другого рода. c[0x0][UR4] индексирует своё смещение регистром там, где записанная форма хранит число, и кодировщик поднял исключение, а не отказал. Падение на чужом входе хуже неверного вердикта, потому что вызывающая сторона не получает ни того, ни другого.
sm_120 — это не номер модели. Это вычислительная способность (compute capability), общая для всей потребительской линейки Blackwell, поэтому кодировка инструкций, база данных, ассемблер и проверщик применимы к каждой карте в ней:
| Карта | Compute capability | Охват |
|---|---|---|
| GeForce RTX 5090, 5090D | 12.0 (sm_120) | да |
| GeForce RTX 5080, 5070 Ti, 5070 | 12.0 (sm_120) | да |
| GeForce RTX 5060 Ti, 5060, 5050 | 12.0 (sm_120) | да |
| Мобильные GeForce RTX 50 серии | 12.0 (sm_120) | да |
| Рабочие станции RTX PRO Blackwell | 12.0 (sm_120) | да |
| Датцентровые Blackwell (B100, B200, GB200) | 10.0 (sm_100) | нет, другая кодировка |
ptxas также нацеливается на sm_121 — другой чип в том же семействе. basalt никогда не запускался на нём и не заявляет о его поддержке. Что он может сказать — измерено: компилятор испускает байт-идентичный код, включая управляющие слова, для всех шести целей, которые он здесь предлагает, поэтому расписание, необходимое kernel'у, — это свойство архитектуры, а не конкретной детали
(находка 28). Если бы это было не так, собственный компилятор NVIDIA испускал бы небезопасное расписание для одной из них.
Каждое число, измеренное на кремнии, получено с одной физической карты, названной точно, потому что «какой-то 5070 Ti» недостаточно для воспроизведения запуска:
| Карта | Что именно это такое |
|---|---|
| Плата | Gigabyte GeForce RTX 5070 Ti EAGLE OC |
| Сообщено драйвером | NVIDIA GeForce RTX 5070 Ti |
| Compute capability | 12.0 |
| Потоковые мультипроцессоры | 70 |
| Буст-частота | 2542 МГц |
| Инструментарий | CUDA 13.3.1, ptxas V13.3.73 |
Большей части basalt GPU вообще не нужен. Оба оракула, база данных инструкций,
ассемблер и проверщик рисков работают с ptxas и nvdisasm как обычные подпроцессы,
поэтому они выполняются в CI на машине без видеокарты. 237 из 252 тестов входят в эту
группу, а 200 не требуют ни карты, ни бинарников NVIDIA.
GPU нужен ровно для трёх вещей, и это те три вещи, которые превращают правдоподобный инструмент в заслуживающий доверия:
| Требуется карта | Зачем |
|---|---|
measure, probe-stalls | Замер времени инструкции и выяснение того, что действительно требует зависимость, путём её разрушения |
scripts/roundtrip_corpus.py | Перепланирование каждого kernel'а корпуса и запуск обеих версий для сравнения выходных байтов |
scripts/agreement_sweep.py | Укорачивание одной зависимости на kernel и вопрос к кремнию, был ли basalt прав |
Фабричный разгон не влияет на измерения. Каждая задержка здесь указана в тактах — это свойство конвейера, а не частоты, а значение буста записывается рядом только для того, чтобы оставалась возможность сравнения по реальному времени. На что плата действительно влияет — так это на воспроизводимость, поэтому basalt measure --board записывает её.
Всё, что здесь измерено, было измерено на одной карте, и basalt записывает SKU рядом с каждым измерением, а не преподносит их как универсальные. В 5090 более чем вдвое больше SM и собственное поведение частоты; кодировка будет идентичной, а задержки следует перемерить, а не предполагать:```bash python -m basalt.cli measure -o my-card.json python -m basalt.cli verify kernel.cubin --latencies my-card.json
Это не скромность. Модель задержек, общая для проверяющего и планировщика, — это именно то место,
где прячется ошибочное число, поэтому вторая карта — самое полезное, что кто-либо может внести.
</details>
## Как это работает
Всё опирается на два оракула, оба из которых — штатные бинарники NVIDIA, запускаемые как внешние процессы. Никакие исходники, заголовки или библиотеки NVIDIA не используются и не распространяются.
| Оракул | Вызов | Что даёт |
| :--- | :--- | :--- |
| **Эталон** | `ptxas` → cubin → `nvdisasm -c -hex` | Кодировки, которые компилятор вендора реально выдаёт. Семантика вне сомнений. |
| **Зонд** | `nvdisasm -b SM120a` по сырым байтам | Декодирует слова, которые `ptxas` никогда не выдаст, превращая пространство кодировок в нечто, что можно искать, а не угадывать. |
Именно зонд-оракул имеет значение. Инструмент, ограниченный выводом компилятора, может лишь заново открыть то, что компилятор уже делает. Подача синтезированных 128-битных слов напрямую в декодер означает, что набор инструкций можно *измерить*.
Ни одному из оракулов не нужен GPU, поэтому вся база данных инструкций пересобирается в CI на любой машине.
### Выведение кодировки путём её изменения
basalt не читает таблицу опкодов ни из какого источника. Он берёт кодировку, которая собралась, переворачивает один бит, декодирует результат и записывает, что изменилось. Бит, который меняет регистр назначения, — это бит назначения; бит, который меняет мнемонику, — селектор; бит, который не меняет ничего наблюдаемого, инертен.
При запуске с `IADD R5, R5, 0x2a` измерение выглядит так:```
operand[0] bits 16:23 flip 16 -> R4, flip 17 -> R7 destination register
operand[1] bits 24:31 plus bit 72, which negates it source register
operand[2] bits 32:63 flip 32 -> 0x2b, flip 33 -> 0x28 32-bit immediate
opcode bits 2, 4, 12:15
inert 36 bits no observable effect
invalid 11 bits the decoder rejects the mutation
Восьмибитные поля регистров и 32-битное непосредственное значение, полученные экспериментальным путём, а не по предположению.
Раскладка подтверждает свою правильность при первом же применении. В тривиальном ядре S2R устанавливает write_barrier=0, а IMAD, потребляющий его результат, несёт wait_mask=0x01; LDCU.64 устанавливает write_barrier=1, а зависимый STG.E несёт wait_mask=0x02. Каждая пара «производитель-потребитель» совпадает, и инструкции, которые nvdisasm помечает .reuse, имеют установленным соответствующий бит reuse.
Никакой установки CUDA и никакого GPU. Скрипт тулчейна загружает закреплённые распространяемые пакеты, примерно 45 МБ, без прав администратора, ничего не добавляется в ваш PATH.```bash git clone https://github.com/sunnypatell/basalt.git cd basalt python -m venv .venv && source .venv/bin/activate # Windows: ..venv\Scripts\Activate.ps1 pip install -e ".[dev]"
python scripts/fetch_toolchain.py # pinned ptxas + nvdisasm python -m basalt.cli doctor # verify both oracles end to end python scripts/verify_all.py # every control in this README, in order
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Использование</h3>
<!-- /wp:heading -->
**Интеграция со службами с ключами**
Библиотека включает интеграцию с функцией служб с ключами .NET. Это полезно в мультитенантных сценариях, когда один и тот же тип службы регистрируется несколько раз с разными ключами (например, для каждого тенанта). Когда служба зарегистрирована с ключом, контекст тенанта не будет использоваться для её разрешения. Вместо этого вы можете использовать ключ для разрешения конкретной регистрации.
- `WithKeyedServiceLifetimeScope` — метод расширения, который позволяет создать область службы на основе типа службы, зарегистрированного с ключом.
- `WithKeyedServiceResolution` — метод расширения, который позволяет разрешить службу с ключом, используя её ключ.
```csharp
services.AddSingleton<IService>(factory => ...);
// ...
using var scope = services.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IService>(); // resolved from the tenant context
Для служб, зарегистрированных с ключами, необходимо использовать WithKeyedServiceLifetimeScope.
services.AddKeyedSingleton<IService>("key", factory => ...);
// ...
using var scope = services.WithKeyedServiceLifetimeScope(typeof(IService), "key");
var service = scope.ServiceProvider.GetRequiredService<IService>();
AddKeyedSingleton регистрирует синглтон с ключом, а WithKeyedServiceLifetimeScope создаёт область с использованием внутреннего TenantContext, связанного с ключом.
Мультитенантный резолвер
Библиотека включает встроенный MultiTenantResolver, который использует текущий HttpContext для определения тенанта.
[!IMPORTANT] Этот резолвер предназначен для приложений, использующих разделение баз данных по тенантам, например, когда каждый тенант использует собственную базу данных.
using TenantCore;
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddMultiTenant<Tenant>()
.WithIdentityResolver<MultiTenantResolver>()
.WithDatabaseResolver<DatabaseResolver>();
MultiTenantResolver отвечает за определение тенанта на основе HttpContext при создании новой области (например, HTTP-запроса), а DatabaseResolver отвечает за разрешение строки подключения.
Если тенант найден, строка подключения разрешается на основе этого тенанта.
Регистрация и настройка
Чтобы использовать библиотеку, необходимо зарегистрировать службы с помощью метода AddMultiTenant и настроить необходимые компоненты.
using TenantCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMultiTenant<Tenant>()
.WithIdentityResolver<CustomTenantResolver>()
.WithDatabaseResolver<DatabaseResolver>();
var app = builder.Build();
AddMultiTenant регистрирует все необходимые службы: IHttpContextAccessor, ITenantContext, IMultiTenantConnection и другие.
[!NOTE] Более подробный пример см. в демонстрационном проекте.
Пользовательский резолвер тенанта
Вы можете создать пользовательский резолвер тенанта, реализовав интерфейс ITenantResolver.
public class CustomTenantResolver : ITenantResolver
{
public ValueTask<string> ResolveAsync(object? context)
{
if (context is HttpContext httpContext)
{
var tenant = httpContext.Request.Headers["X-Tenant-ID"].ToString();
return ValueTask.FromResult(tenant);
}
return ValueTask.FromResult(string.Empty);
}
}
Этот резолвер ищет заголовок X-Tenant-ID в HTTP-запросе и использует его значение в качестве идентификатора тенанта.
Фабрика мультитенантных подключений
Шаблон ConnectionFactory используется для создания подключения к базе данных для конкретного тенанта с учётом предоставленной строки подключения.
Библиотека включает MultiTenantConnectionFactory, которая автоматически используется при работе с IMultiTenantConnection и выполняет следующие задачи:```console
$ python -m basalt.cli doctor
ok toolchain V13.3.73 in third_party/cuda/13.3.1/bin
ok ptxas assembled sm_120a
ok cubin oracle 16 instructions with encodings
ok probe oracle 16/16 mnemonics round-tripped
both oracles healthy. no GPU required for anything above.
### Или как пакет
`pip install basalt-sass` устанавливает CLI, библиотеку и все три таблицы измерений,
без зависимостей времени выполнения. Используйте это, если на машине уже есть CUDA; описанный выше checkout
подойдёт, если её нет, или если вы хотите воспроизвести измерения.```bash
pip install basalt-sass
basalt doctor
basalt verify kernel.cubin
ptxas and nvdisasmbasalt запускает обе как внешние процессы и не распространяет ни одну из них, поэтому ему приходится находить их копию. Он использует первую найденную, и подойдёт любая установка CUDA 13: ничего не должно быть привязано к конкретному распространяемому пакету.
basalt doctor выводит, какой именно вариант он выбрал, и завершается с ненулевым кодом, если не может найти ни один, так что это работает как предусловие для шага сборки, а не просто как справочная информация.
Для запросов к базе данных инструкций вообще не нужен тулчейн, потому что база измеряется заранее и поставляется внутри пакета:```bash basalt isa --stats basalt isa --opcode QMMA
Пересоздайте базу данных инструкций с нуля или выполните запрос к закоммиченной:```bash
python -m basalt.cli build-isa # harvest, probe, write src/basalt/data/isa/sm_120a.json
python -m basalt.cli isa --stats
python -m basalt.cli isa IMAD.WIDE.U32 # one form, with its measured field layout
python -m basalt.cli isa --opcode QMMA # every form of one opcode
Это та часть, которую больше никто не делает, и для неё не нужны ни GPU, ни аргументы. Измеренная модель задержек и добытая таблица требований обе закоммичены, так что свежий клон можно направить прямо на cubin, чем бы он ни был создан:```bash python -m basalt.cli verify kernel.cubin
[No input text provided to translate.]```console
$ python -m basalt.cli verify nvjpeg.sm_120.cubin
25 kernels, 0 with an error
7984 instructions in 789 blocks, 11580 dependencies checked across blocks: 0 errors, 3 warnings
pair data: 3957 pairings from 25634 kernels, 427 producers with enough observations to use
latency model: measured on NVIDIA GeForce RTX 5070 Ti
Библиотечный ELF-файл содержит сотни ядер, и каждое проверяется по отдельности, поскольку смещения
начинаются с нуля, и ничто не переходит из одного ядра в следующее. Добавьте --strict, чтобы завершить работу
с ненулевым кодом при обнаружении опасности, — именно этого ожидает шаг сборки. Если у вас есть карта sm_120 и вы хотите,
чтобы модель была протестирована на вашем собственном кремнии, а не на том, что есть в этом репозитории:```bash
python -m basalt.cli measure -o my-card.json # needs a GPU, once
python -m basalt.cli verify kernel.cubin --latencies my-card.json
<details>
<summary><b>Каждая команда и для каких нужна карта</b></summary>
<br/>
| Команда | Что делает | Нужна GPU |
| :--- | :--- | :--- |
| `doctor` | Проверяет оба оракула по полному циклу | нет |
| `build-isa` | Собирает данные и зондирует, записывает базу инструкций | нет |
| `isa` | Запрашивает форму, опкод или покрытие | нет |
| `validate-isa` | Доказывает, что измеренные поля можно записывать насквозь | нет |
| `mine-stalls` | Изучает требования для каждой пары на основе того, что планирует компилятор | нет |
| `verify` | Проверяет управляющие биты cubin на наличие конфликтов данных | нет |
| `schedule` | Назначает управляющие биты cubin с нуля и проверяет результат | нет |
| `assemble` | Кодирует текст SASS или целый cubin и читает его обратно для проверки | нет |
| `measure` | Замеряет задержку инструкций на реальном кремнии | **да** |
| `probe-stalls` | Находит требуемый stall, намеренно ломая программы | **да** |
</details>
Всё, что умеет CLI, доступно для импорта, а библиотечный интерфейс с запускаемыми примерами
находится в [`docs/API.md`](https://github.com/sunnypatell/basalt/blob/main/docs/API.md).
И два элемента контроля, которые не дают остальному врать. Первому нужна карта; второму нужны
поставляемые библиотеки и вообще никакого оборудования:```bash
python scripts/roundtrip_corpus.py # reschedule all 441 corpus kernels, run both on the GPU
python scripts/fetch_toolchain.py --libs # ~1.2 GB, no admin, nothing on PATH
python scripts/audit_shipped.py --libs third_party/cuda/13.3.1/libs
The input chunk appears to be empty. No translatable content was provided after "INPUT:". Please supply the chunk text so I can translate it into Russian.```console $ python -m basalt.cli verify kernel.cubin --latencies src/basalt/data/latency/rtx-5070-ti.json kernel.cubin 32 instructions in 3 blocks, 23 dependencies checked: clean latency model: measured on NVIDIA GeForce RTX 5070 Ti
## Измерено, а не предположено
Числа здесь выводятся инструментарием и пересоздаются из чистого checkout. Команды выше — источник истины; эти таблицы — снимки.
**База данных инструкций.** Каждая запись содержит кодировку, которая действительно ассемблировалась, и сборку компилятора, которая её произвела.
| База данных инструкций | Количество |
| :--- | ---: |
| Формы инструкций | 345 |
| Различные опкоды | 90 |
| Формы с полной картой операндов | 339 |
| Формы тензорных ядер | 46 |
| Собрано с помощью | `ptxas` V13.3.73 |
Покрытие тензорных инструкций — там, где живёт низкоточное железо: `HMMA` и `IMMA`, `QMMA` для типов FP8, FP6 и FP4, включая асимметричные пары операндов, формы с масштабным коэффициентом `QMMA.SF` и `OMMA.SF`, несущие экспоненту на блок, разрежённые `IMMA.SP`, а также инструкции перемещения матриц `LDSM`, `STSM` и `MOVM` во всех формах, включая транспонирующие варианты.
**Задержка на RTX 5070 Ti.** 70 SM, каждая аппроксимация R² ≥ 0.9998. Измерено путём замера времени зависимых цепочек и вычисления наклона; длина цепочки считывается из скомпилированного SASS, а не предполагается.
| Инструкции | Циклы |
| :--- | ---: |
| `IMAD` `IADD3` `FFMA` `FADD` `FMUL` `LOP3` `SHF` | 4 |
| `POPC` | 18 |
| `I2FP` + `F2I` вместе | 24 |
| `MUFU` | 44 |
| `DADD` `DFMA` | 64 |
Три из них противоречат предполагаемой модели, поставлявшейся с basalt: `DADD` предполагался равным 48, `POPC` — 4, а каждая конверсия — 6, тогда как для полного цикла измерено 24.
<img src="https://raw.githubusercontent.com/sunnypatell/basalt/main/docs/assets/chart-latency.svg" alt="Три задержки, которые basalt предполагал до измерения их против того, что сообщил кремний: сложение fp64 предполагалось 48, а измерено 64, POPC предполагался 4, а измерен 18, и полный цикл конверсии I2FP плюс F2I предполагался 12, а измерен 24.">
Предполагаемая модель задержек — это не малое приближение измеренной; в этом весь аргумент в пользу измерения.
**И stall со значением ноль — это не ноль циклов.** Это отдельная безопасная кодировка, которая ожидает незавершённые результаты, и обходится примерно в 37 циклов там, где запланированная инструкция стоит 4. Вот почему `ptxas -O0` генерирует полностью обнулённое управляющее слово, и код при этом по-прежнему вычисляет корректно — примерно в девять раз медленнее.
| `stall` | циклов/инструкция | результат |
| ---: | ---: | :--- |
| **0** | **36.85** | **корректно** |
| 1 | 4.88 | неверно |
| 2 | 4.88 | неверно |
| 3 | 5.88 | неверно |
| 4 | 6.88 | корректно |
**Он согласуется с компилятором производителя по каждому ядру в корпусе.** Каждое ядро, которое `ptxas` собирает из корпуса, проверяется против его собственного планирования на каждом уровне оптимизации, который выполняет планирование: 30 421 зависимость, ноль ошибок. Этот прогон выполняется в CI при каждом пуше, и каждая ошибка моделирования, которую допустил этот проект, была поймана именно им, а не рассуждениями.
**Вердикты совпадают с кремнием.** Для каждого кодируемого stall на зависимом производителе статический ответ basalt и то, что реально вычисляет железо, согласуются, включая случай нуля. Это закреплено тестом, а не утверждается здесь. Полные доказательства, включая три независимых метода для требуемого stall и исправления, сделанные по ходу, — в [findings](https://github.com/sunnypatell/basalt/blob/main/docs/FINDINGS.md).
**И когда он говорит, что расписание небезопасно, кремний соглашается.** Возьмите собственное рабочее расписание производителя для 233 ядер, укоротите в каждом по одной реальной зависимости и сравните вердикт basalt с тем, что вычисляет GPU: 79, которые он назвал сломанными, действительно были сломаны, и **ничего из того, что он назвал безопасным, не вычислило неверный ответ**. Это число начиналось с 34 пропущенных, а не с нуля, и в [findings](https://github.com/sunnypatell/basalt/blob/main/docs/FINDINGS.md) сказано, в чём была причина и во что обошлось её исправление в ложных срабатываниях, потому что прогон, сообщающий только итоговую цифру, стоит меньше, чем тот, который сообщает свою первую.
## Он тоже умеет назначать управляющие биты
Верификатор отвечает на вопрос, безопасно ли расписание. Планировщик отвечает на вопрос, каким было бы безопасное расписание, исходя из тех же измерений: он отбрасывает каждый управляющий бит, созданный `ptxas`, вычисляет свои, возвращает результат верификатору, а затем запускает его на GPU рядом с версией того же ядра от производителя.
При прогоне по всему корпусу на карте, на каждом уровне оптимизации, который создаёт расписание, все 439 сопоставимых ядер дают побайтово идентичные результаты расписанию производителя, причём управляющие биты basalt вычислил сам. Те 2, что исключены, читают часы и идентификатор сетки, так что не согласуются даже сами с собой, и в [findings](https://github.com/sunnypatell/basalt/blob/main/docs/FINDINGS.md) об этом сказано прямо, а не вписано в проценты.
Этот контроль — причина, по которой можно доверять всему остальному. Проверяющий и планировщик читают одну и ту же модель задержек, поэтому ошибочная запись в ней удовлетворяет обоих одновременно, и они согласуются друг с другом, оставаясь оба неправыми. Только у кремния нет интереса в этом споре. Долгое время прогон планировщика по семи написанным вручную ядрам проходил семь из семи; прогон по трёмстам нашёл сорок одно неправильное ядро, и каждое последующее исправление модели родилось из наблюдения за тем, как движется это число.
Именно из этого цикла и появились настоящие баги. Stall, проведённый вне окна между производителем и его потребителем, ничего не стоит, и трата его там завершает поиск программой, которая всё ещё слишком коротка. Stall, привязанный к безопасной кодировке, перезаписывался более поздним проходом, подменяя гарантию маленьким числом. Операнды fp64 занимают пары регистров, и в мнемонике ничто об этом не говорит, поэтому половина каждой зависимости fp64 была невидима и для проверяющего, и для планировщика. Предикат, используемый как guard инструкции, требует тринадцать циклов там, где тот же предикат, читаемый как данные, требует пять, потому что guard должен быть разрешён до того, как инструкция вообще будет выдана. А ожидание на scoreboard не улаживает зависимость полностью: производитель всё ещё должен небольшой собственный stall — два цикла для сложения fp64, — и на один цикл меньше — это тихая ошибка. Ни одна из этих проблем не была найдена рассуждениями; каждая была найдена запуском вывода и получением неверного числа.
> [!NOTE]
> **1.0, и конкретно о том, что это значит.** Что сделано: оба оракула, база данных инструкций с доказанно записываемыми полями, проверяющий опасности поверх реального графа потока управления, задержка, измеренная на одном SKU тремя независимыми методами, планировщик, который прогоняет каждое сопоставимое ядро корпуса через железо туда и обратно с побайтовым совпадением, и аудит 2 762 поставляемых ядер, исключённых из всех таблиц, которые читает проверяющий. Что не сделано: 12 ядер корпуса, невыполнимых по построению, и 2, чей вывод производителя не детерминирован, — все перечислены в findings; десять опкодов по-прежнему несут предполагаемую задержку вместо измеренной, и ни один из них никогда не был производителем ни в одном из двух корпусов кода; и измерен только один GPU, а [finding 28](https://github.com/sunnypatell/basalt/blob/main/docs/FINDINGS.md) показывает, что это значит меньше, чем звучит. Где что-то выведено, а не измерено, инструментарий говорит об этом, а не округляет до факта. См. [roadmap](https://github.com/sunnypatell/basalt/blob/main/docs/ROADMAP.md) и [method](https://github.com/sunnypatell/basalt/blob/main/docs/METHOD.md).
## Структура репозитория
<details>
<summary><b>Где что находится</b></summary>
<br/>```
src/basalt/
toolchain.py Locating and driving ptxas / nvdisasm
encoding.py The 128-bit instruction word and its control fields
disasm.py Both oracles: cubin ground truth and raw-word probe
harvest/ PTX corpus generation and encoding extraction
probe/ Differential bit probing and field inference
isa/ The generated instruction database and its builder
asm/ The assembler, and the ELF reader that rewrites words in place
sched/ Assigning the control bits, and costing the result
verify/ Register def-use analysis, hazard model, latency checking
gpu/ Driver-API bindings and the latency measurement harness
src/basalt/data/ The measured tables, inside the package so an installed copy
has them: the ISA database, the latency model and the mined
stall requirement
docs/ Findings, method, the Python API, roadmap, artwork sources
scripts/ Toolchain fetch, asset rendering, drift check, and the two
hardware controls: the corpus round trip and the agreement sweep
tests/ Unit tests, plus toolchain- and GPU-marked suites
basalt — это независимая работа в стиле clean-room, созданная для обеспечения интероперабельности. Он не содержит исходного кода NVIDIA, заголовков, библиотек или документации и ничего не распространяет. Он наблюдает за поведением публично распространяемых исполняемых файлов и фиксирует его — именно на этом основании такого рода работы ведутся уже более десяти лет.
NVIDIA, CUDA и Blackwell являются товарными знаками NVIDIA Corporation. Этот проект не связан с NVIDIA, не одобрен ею и не спонсируется ею.
Лицензировано под Apache-2.0. Apache выбран намеренно, а не что-то ограничительное: инструмент корректности, на котором никому не разрешено строить, — это инструмент корректности, который никто не запускает, а патентная оговорка важна для работы, настолько близкой к аппаратному обеспечению.
Наиболее ценный вклад — это кодировка, которую basalt определяет неверно. См. CONTRIBUTING.md и шаблон ISA gap, который собирает достаточно информации для воспроизведения без вашей машины.
SUPPORT.md описывает, куда обращаться с вопросами, GOVERNANCE.md — что должно пройти изменение, RELEASING.md — как готовится и проверяется релиз, а SECURITY.md — как сообщить о проблеме конфиденциально.
Если basalt повлиял на статью, инструмент, модель или отчёт об ошибке, пожалуйста, процитируйте его. GitHub нативно читает
CITATION.cff, поэтому пункт Cite this repository на боковой панели выдаёт вам
APA и BibTeX без транскрибирования. Этот же файл анализируют Zenodo и менеджеры цитирования, и он является авторитетной записью об авторстве.
Ключ ниже — тот же, что генерирует GitHub, поэтому копирование отсюда и копирование с боковой панели дают одну и ту же запись, а не две, похожие на разные работы:```bibtex @software{Patel_basalt_a_hazard_2026, author = {Patel, Sunny}, license = {Apache-2.0}, month = aug, title = {{basalt: a hazard checker, assembler and scheduler for NVIDIA consumer Blackwell (sm_120)}}, doi = {10.5281/zenodo.22072811}, url = {https://github.com/sunnypatell/basalt}, version = {1.0.0}, year = {2026} }
Цитируйте **концептуальный DOI**, [`10.5281/zenodo.22072811`](https://doi.org/10.5281/zenodo.22072811), а не
версионный DOI или этот URL. Он ведёт на самый свежий релиз, поэтому остаётся корректным без
необходимости когда-либо редактировать его снова. `CITATION.cff` содержит его, так что он уже
есть в обеих формах выше.
Если вы используете измеренные таблицы (`src/basalt/data/`) или воспроизводите рисунок, цитируйте релиз,
из которого они взяты, а не `main`: числа перегенерируются скриптом `scripts/verify_all.py` в
конкретном коммите, и именно тег делает это воспроизводимым.
**Атрибуция — это условие лицензии, а не любезность.** Пункт 4 Apache-2.0 требует, чтобы
[`LICENSE`](https://github.com/sunnypatell/basalt/blob/main/LICENSE) и [`NOTICE`](https://github.com/sunnypatell/basalt/blob/main/NOTICE) сопровождали любое распространение или производную работу,
а `NOTICE` содержит сведения об авторстве и заявление о «чистой комнате». Форки, вендорные копии и
переупакованные wheel-пакеты сохраняют оба файла.
## Автор
**Sunny Patel** · [sunnypatel.net](https://www.sunnypatel.net) · [github.com/sunnypatell](https://github.com/sunnypatell)
| Поле | Биты | Значение |
|---|
stall | 108:105 | Тактов ожидания перед выдачей следующей инструкции |
yield | 109 | Подсказка, что планировщик варпов может переключить варпы |
write_barrier | 112:110 | Скорборд для сигнализации при записи результата (7 = нет) |
read_barrier | 115:113 | Скорборд для сигнализации при чтении операнда (7 = нет) |
wait_mask | 121:116 | Скорборды, которые должны быть очищены перед выдачей |
reuse | 125:122 | Флаги кэша повторного использования операндов, по одному на слот источника |
| Метод | Описание |
|---|
RegisterTenantContext | Регистрирует инфраструктуру контекста тенанта |
WithTenantContext | Связывает контекст тенанта с областью, схемой базы данных |
WithKeyedServiceLifetimeScope | Создаёт область с разрешённым типом службы с ключом |
WithKeyedServiceResolution | Разрешает службу с ключом, используя ключ |
GetTenantContext | Получает текущий контекст тенанта |
GetMultiTenantConnection | Получает мультитенантное подключение для текущего тенанта |
Reset | Сбрасывает состояние контекста тенанта |
| Order | Where |
|---|
| 1 | --cuda-bin, передаётся через командную строку |
| 2 | BASALT_CUDA_BIN, каталог, содержащий оба исполняемых файла |
| 3 | CUDA_PATH, CUDA_HOME или CUDA_ROOT, каждый плюс /bin |
| 4 | ptxas в вашем PATH |
| 5 | third_party/cuda/<version>/bin в рабочей копии, сначала новейшая |