Inference Scaling
Оригинал: Inference Scaling

Я бы рассматривал текущую предлагаемую реализацию «подцепи» как набросок. Существует много вариантов того, как это может работать, описанный вариант может быть первой итерацией, поскольку он относительно прост.

Это, безусловно, было бы большим улучшением. Но применение этого шаблона к более масштабируемому реестру BFT, такому как Obyte DAG, позволит масштабировать вещи на гораздо более высокие уровни и добавить богатый набор производных функций.

Пожалуйста, рассмотрите возможность присоединиться к обсуждению этого предложения и поделиться любыми идеями или предложениями. Первоначальная версия будет предоставлена автором предложения, а отзывы участников помогут усовершенствовать подход и прояснить следующие шаги.

Есть ли шанс, что тем, кто в настоящее время исключен, все еще будет разрешено присоединиться, но без права на вознаграждения? Это позволит провести дополнительное тестирование и позволит потенциальным хостам быть полностью готовыми, когда система будет готова разрешить им повторное включение. На данный момент похоже, что прошло слишком много времени, а исключенные хосты не взаимодействовали.

Предложение: масштабирование вывода
Проблема
Согласно выводам, следующие транзакции записываются в цепочке:
Мсгстартинференце
MsgFinishInference
MsgValidation (от 0 до N_hosts на вывод; для простоты рассмотрим случай, когда это 1 tx)
3 транзакции за вывод. Максимальная емкость на блок составляет ~5000 => 5000/3 = 1666 выводов на блок => 1666/6 = 277 выводов в секунду.
Рассмотрим 4xH100 с развернутым Qwen3-235B. Для 5000/1000 токенов ввода/вывода такая установка может обрабатывать 3,5-4 RPS (TODO: подтвердить) => 277 / 3,5 * 4 = 316 графических процессоров H100 для насыщения цепочки.
Запросы могут быть объединены в одну транзакцию, но вычисления и рост состояния на каждый запрос делают это не масштабируемым для сотен тысяч выводов.
Узкое место лучше проявляется при более длинных запросах (больше вычислений на передачу) и хуже у моделей меньшего размера (больше запросов в секунду на графический процессор, больше потоков на графический процессор).
Примечание: на практике основным ограничением является не количество транзакций, а стоимость вычислений на блок. Это становится проблемой при наличии нескольких сотен таких транзакций на блок. На основе профилирования его можно оптимизировать в 2–10 раз (или даже в 100 раз), но это ограничение сработает раньше ограничения количества передач. Нынешнее предложение по-прежнему пытается решить всю проблему.
Предложение
Это предложение описывает подход, который выводит всю коммуникацию для каждого вывода за пределы цепочки. Цепочка обрабатывает только две транзакции: одну для помещения монет на условное депонирование и назначения подгруппы хостов, другую для расчета в конце. Вся передача выводов и проверки происходят непосредственно внутри подгруппы в течение длительного сеанса (например, одной эпохи). Чтобы закрыть сеанс, пользователь отправляет окончательное состояние использования, подписанное подавляющим большинством хостов (порог: 2/3 взвешенных по слотам). У обеих сторон есть явный стимул к урегулированию: пользователь возвращает неиспользованный остаток условного депонирования, а подгруппа получает от него оплату.
Фактически, поскольку каждая подгруппа должна будет достичь консенсуса относительно окончательного состояния, архитектура будет состоять из:
основной блокчейн
множество подцепей/шардов с чрезвычайно легкой архитектурой
Субцепи смогут обрабатывать только транзакции, связанные с выводом, и их решение может повлиять только на условное депонирование, назначенное таким субцепям.
Примечание. «Подцепь» не обязательно означает настоящий блокчейн. Поскольку группа не несет никакого состояния за пределами назначенного ей пользователя, группы могут быть динамическими: формируются для каждого сеанса с большим перекрытием между ними. Единственное, что у них общее, — это условное депонирование в основной сети в качестве якоря.
Архитектура
+-----------+ +-------------------+ +----------------------------+ | Пользователь | | Основная сеть | | Подсеть (одна на сеанс) | +-----------+ +-------------------+ +----------------------------+ | | | | 1. MsgCreateEscrow | | | (100ГНК) | | | ------------------> | | | <- escrow_id, | | | группа=[h1..hN] | | | | | | 2. POST /chat (req1) --------------------------> | | 3. POST/chat (req2) --------------------------> | | 4. POST /chat (reqN) --------------------------> | | ... | | | | | | 5. MsgSettleEscrow | | | (finalState, | | | подписи, ..) | | | ------------------> | | | <- возврат средств пользователю + | | | хосты заплатили | | +-----------+ +-------------------+ +----------------------------+
Пользователь отправляет в основную сеть ровно 2 транзакции: MsgCreateEscrow для открытия сеанса и MsgSettleEscrow для его закрытия. Все запросы на вывод выполняются напрямую с назначенной группой подсети; Mainnet никогда не видит отдельные запросы.
Пользовательский поток
[mainchain]: пользователь создает MsgCreateEscrow(100GNK)
- [подцепь]: пользователь взаимодействует с хостами в подгруппе в заранее определенном порядке.
- [mainnet]: в конце сеанса пользователь создает MsgSettleEscrow(state_root, nonce, подписи, использование, host_stats, ...)
Вопрос 1: Кто решает, какие наказания будут наказывать хосты: субчейн или основная сеть?
Если субцепочка примет решение: ей необходимо агрегировать статистику по пользователям, что требует общего постоянного состояния для каждой группы, что требует фиксированных групп, а не динамических групп для каждого пользователя.
Текущий подход: решает основная сеть. Субцепочка записывает только необработанную статистику за сеанс (пропущенные/недействительные счетчики для каждого хоста) внутри MsgSettleEscrow. Mainnet объединяет данные по сеансам и применяет наказание. Можно пересмотреть.
Вопрос 2. Поддерживаются ли хосты для отдельных групп или для отдельных пользователей?
Если для каждой группы: те же последствия, что и для варианта A Q1, требуются фиксированные группы.
Текущий подход: по каждому пользователю, как следует из Q1. Каждый хост отслеживает только то, что происходит внутри каждого пользовательского сеанса. Нет общего состояния между пользователями в одной группе. Именно это делает возможными динамические группы для каждого сеанса. Можно пересмотреть.
Дальнейшее предложение следует этой архитектуре: «цепочка на пользователя».
Основной сетевой протокол
MsgCreateEscrow (сумма CreatorAddr, )
перевести деньги на условное депонирование через MsgCreateEscrow
- вернуть идентификатор выборке N(64?) хостов-слотов с использованием взвешенной случайной выборки (идею слота см. в предложениях/poc/optimize.md (../poc/optimize.md))
взаимодействовать в подцепи во время сеанса
произвести расчет внутри сети через MsgSettleEscrow
MsgSettleEscrow( escrow_id, # идентификатор сеанса state_root, # корень Merkle: hash(host_stats_hash || rest_hash) nonce, # последние подписи nonce, # map[slot_id -> sig] поверх (state_root, escrow_id, nonce) rest_hash, # родной брат Merkle: hash(balance || inferences_hash) host_stats, # map[slot_id -> HostStats] ) HostStats( пропущено, # промахов при выполнении (начато, но не завершено) неверно, # выводы признаны недействительными из-за стоимости голосования по вызову, # общая стоимость выполненных выводов require_validations, # выводов MustValidate, выбранных для этого хоста Completed_validations, # фактически отправленные txs MsgValidation )
Основная сеть пересчитывает host_stats_hash из отправленной host_stats, проверяет хэш (host_stats_hash || rest_hash) == state_root, затем проверяет 2/3+ взвешенных по слотам подписей (state_root, escrow_id, nonce). Для расчета не требуются отдельные записи выводов. Обязательный финальный раунд гарантирует, что все выводы будут решены, а соответствие валидации вычислено перед расчетом.
- При расчете условного депонирования основная сеть проверяет доказательство Меркла и 2/3+ подписей, взвешенных по слотам. После проверки он осуществляет условное депонирование для пользователя: каждый хост оплачивается из условного депонирования в соответствии с стоимостью хоста_stats[slot].cost, оставшийся баланс возвращается пользователю, статистика хоста записывается.
Протокол подсети
Подсеть представляет собой легкий сегмент, вес голосования которого предоставляется основной сетью. Когда сеанс заканчивается, он возвращается в основную сеть.
Цели проектирования: легкий, распараллеливаемый, обеспечить, чтобы пользователь использовал все хосты из группы.
Чего хочет пользователь? Отправляйте запросы REST, совместимые с OpenAPI ( /chat/completions , /embeddings и т. д.) и узнавайте как можно меньше о блокчейне.
Чего хочет сеть? Те же свойства, которые мы пытались достичь в основной сети:
- Знайте, когда начинается и заканчивается каждый запрос. Другие хосты измеряют производительность исполнителя в соответствии с ожидаемой пропускной способностью и наказывают за низкую производительность (пропущенную скорость).
- Узнайте хэш приглашения (подписанного пользователем) и хэш полезных данных ответа (подписанного исполнителем). Оперативная подпись разрешает оплату. Подпись полезной нагрузки обеспечивает проверку вероятностного вывода (недопустимая частота).
- Обеспечьте распределение запросов по исполнителям пропорционально их весу.
Цепочке нужны эти свойства, но она не хочет обрабатывать эти данные в основной сети.
Типы транзакций подсети (все вне цепочки, только внутри подсети):
- MsgStartInference (пользователь) — авторизовать вывод, зарезервировать стоимость.
MsgFinishInference (хост) — завершение записи, хэш ответа, количество токенов
MsgValidation (хост) — результат проверки; valid=false открывает голосование по вызову
MsgValidationVote (хост) — голосование во время окна вызова
MsgTimeoutInference (хост) — объявить, что время ожидания истекло
- MsgRequestPrompt (хост) — восстановление: запросить данные подсказки, которые пользователь скрыл.
Состояние каждого пользователя. Состояние сохраняется для каждого пользователя независимо. История каждого пользователя представляет собой цепочку различий. Каждый дифф по сути представляет собой блок. Поскольку межпользовательского состояния нет, оператор узла может сегментировать свою базу данных и ресурсы для каждого пользователя. Каждый узел может одновременно участвовать в любом количестве подсетей. Обработка подсети линейно масштабируется в зависимости от количества пользователей. Только создание условного депонирования и расчеты в основной сети этого не делают.
Распространение, управляемое пользователем. Пользователь несет ответственность за последовательность и распространение транзакций. Пользователь прикрепляет накопленные различия к каждому запросу вывода. Это параллельное распространение при обычном использовании API.
Циклический заказ хоста. Пользователь должен перебирать хосты в группе в заранее определенном порядке. Это естественным образом распределяет запросы по хостам (не реальный объем работы, а количество запросов). Каждая разница содержит одноразовый номер, который определяет ожидаемого получателя: slot_at_position(nonce % group_size) . Перед обработкой принимающий хост проверяет, что он является ожидаемым получателем nonce. Если это не так, запрос отклоняется. Это обеспечивает циклический перебор и предотвращает пропуски.
Поток подписания. Когда запрос /chat/completions отправляется на хост1, пользователь создает MsgStartInference(1). Если хост1 честен, он должен немедленно вернуть (состояние, подпись), не дожидаясь выполнения. После выполнения хост1 подписывает MsgFinishInference(1), и пользователь передает его в сеть в следующем раунде (или позже, в зависимости от производительности). Блокировки должны быть необходимы только для генерации новых одноразовых номеров и составления новых сообщений, а не для записи входящих данных. Пользователь не блокирует получение подписи хоста перед отправкой следующего запроса. Сигнатуры поступают асинхронно и включаются в последующие различия. Это позволяет ускорить отправку запросов за счет задержки подписей на один или несколько раундов.
Эскроу-учет. При каждом MsgStartInference подсеть отслеживает расходы по условному балансу пользователя. Та же идея, что и в основной сети: убедитесь, что у пользователя достаточно средств, прежде чем принять запрос. Минимальный баланс условного депонирования всегда должен быть как минимум размером subnet_size * max_inference_cost, обеспечивая достаточную сумму для покрытия наихудшего случая, когда каждый хост в группе обрабатывает одновременный запрос.
Недоступность хоста. Если хост недоступен, пользователь переходит к следующему хосту по порядку. Поскольку каждый запрос содержит ВСЕ накопленные различия за текущий раунд, он включает беззнаковые различия для недоступного хоста. Обнаружение и восстановление выполняются посредством распространения nonce (см. сценарии ниже).
Распространение одноразового номера. После обработки каждого пользовательского запроса принимающий хост сообщает группе текущий одноразовый номер. Небольшое сообщение с постоянными накладными расходами. Каждый хост отслеживает самый высокий увиденный nonce. Если хост_i видит, что nonce продвинулся за назначенную ему позицию, но с ним так и не связались, он обнаруживает пробел и может действовать упреждающе. Это единственный надежный механизм обнаружения: другие хосты не могут отличить «все еще вычисления» от «никогда не полученные данные», просматривая различия (время выполнения варьируется), а задержка подписи является нормальной (сигнатуры всегда отстают как минимум на один раунд).
Транзакции, предложенные хостом. Хосты создают транзакции (MsgFinishInference, триггеры аннулирования и т. д.), которые необходимо включить в состояние. Пользователь является секвенсором, но ему нельзя доверять его включение. Каналы распространения:
- Тело ответа: хост возвращает пользователю предложенные транзакции вместе с результатом вывода.
- Ленивые сплетни: хост отправляет предложенные транзакции другим хостам только в том случае, если пользователь не включил их после K раундов. Ноль накладных расходов на счастливом пути.
- Публичная конечная точка: каждый хост раскрывает свои неурегулированные транзакции за сеанс. Запасной вариант, если ленивые сплетни не дадут результата.
Обеспечение включения. Два разных правила в зависимости от того, кто предложил сделку:
- Предложенный пользователем (MsgStartInference): должен появиться в различиях уже в следующем раунде. Они есть у пользователя во время создания, нет причин для задержки.
- Предложенный хостом (MsgFinishInference и т. д.): льготный период K раундов (TBD). Учет асинхронной задержки. После K раундов без включения хозяева запускают ленивые сплетни и отказываются подписывать.
Каждый ответ хоста включает свой неурегулированный мемпул, поэтому пользователь всегда знает, что находится в ожидании.
Повторите попытку в случае отказа. Если хост отказывается подписывать, потому что пользователь не включил ожидающие транзакции, пользователь повторяет тот же одноразовый номер с добавлением отсутствующих транзакций. Разница в данном nonce доступна только для добавления: повторная попытка должна быть строгим расширением исходной попытки. Хост сохраняет список tx первой попытки и отклоняет любую повторную попытку, которая удаляет или заменяет транзакции из него. Это предотвращает двусмысленность — пользователь не может создать две конфликтующие версии одного и того же nonce. K раундов — это щедро (десятки запросов по всей группе), поэтому пользовательский клиент с хорошим поведением автоматически включает все известные ожидающие транзакции и никогда не получает отказ.
Сценарии
Все работают корректно
Группа = [h1, h2, h3, h4, h5], пользователь отправляет 3 запроса в циклическом порядке.
Пользователь -> h1: различия POST /chat/completions (req1): [MsgStartInference(1)] h1: начинает выполнение, подписывает состояние (nonce=1), возвращает (sig_h1, mempool=[]) h1: после выполнения создает MsgFinishInference(1), сообщает h2..h5 Пользователь -> h2: различия POST /chat/completions (req2): [MsgStartInference(1), MsgStartInference(2)] // пока нет sig_h1 h2: подписывает состояние(nonce=2), возвращает (sig_h2, mempool=[]) Пользователь -> h3: POST /chat/completions (req3) различия: [MsgStartInference(1) + sig_h1, MsgStartInference(2) + sig_h2, MsgFinishInference(1), MsgStartInference(3)] h3: проверяет локальный мемпул: MsgFinishInference(1) присутствует (через сплетни), включено, ок h3: подписывает состояние (nonce=3), возвращает (sig_h3, mempool=[])
Статусы транзакций после 3 запросов:
MsgStartInference(1): установлено (3 сигнала: h1, h2, h3)
MsgStartInference(2): предлагается (2 знака: h2, h3)
MsgFinishInference(1): предложено (1 подпись: h3)
MsgStartInference(3): предлагается (1 подпись: h3)
Пользователь является секвенсором: он решает, в какой nonce помещается каждая транзакция. Все хосты, видящие один и тот же nonce, видят один и тот же контент. Подписи отстают на один или несколько раундов.
Хост не отвечает или не завершает вывод
MsgStartInference(N) существует в состоянии, но MsgFinishInference(N) никогда не приходит. Возможные причины:
Хост действительно недоступен, не получил запрос
Соединение разорвано между пользователем и хостом во время запроса
Хост получил данные, но отказывается вычислять
- Пользователь записал MsgStartInference, но скрыл данные приглашения от хоста.
Атрибуция сложна. Пользователь может атаковать хост, записав MsgStartInference, но скрывая данные подсказки. Хост может атаковать, притворившись, что не получил его. Оба внешне выглядят одинаково. Без механизма восстановления тот, кто честен, будет наказан.
Если хост_i подписал состояние сразу N: хост_i подтвердил получение. Подпись распространяется через более поздние различия, поэтому все хосты могут убедиться, что у хоста есть данные. Если MsgFinishInference(N) не приходит по тайм-ауту, пропущено += 1 для хоста_i. Никакой двусмысленности.
Если хост_i никогда не подписывался: неоднозначно. Протокол восстановления применяется.
Протокол восстановления:
- Host_i посредством распространения nonce обнаруживает, что назначенный ему nonce прошел без получения данных.
- Host_i сообщает группе MsgRequestPrompt(N).
- Каждый хост, который видит MsgRequestPrompt(N), независимо включает его в свой следующий ответ пользователю: «предоставить запрос на одноразовый номер N».
- Небольшая группа ретрансляторов отбирается с использованием хеша блока основной сети в качестве случайности. MsgRequestPrompt включает поле target_height, в котором указана текущая известная высота основной сети + небольшая разница (например, +2 блока). Группа ретрансляции имеет вид hash(escrow_id, inference_id, block_hash_at_target_height) % group_size. Никто не знает хэш блока по адресу target_height при создании MsgRequestPrompt, поэтому пользователь не может найти подходящую группу реле. Для разрешения группы ретрансляции требуется один вызов моста для получения хеша блока после достижения target_height. Это происходит только на пути восстановления (редко). Host_i уже подтвердил заявку (посредством слухов) до того, как блок будет создан.
- Пользователь предоставляет данные подсказки группе реле. Каждый участник подписывает квитанцию и передает ее на хост_i независимо.
- host_i вычисляет, выдает MsgFinishInference(N). Пользователь может повторно подключиться к host_i напрямую для получения ответа или получить его через члена ретрансляции.
- Если хост_i все еще не получил данные, хост_i может повторно запросить их с помощью другого MsgRequestPrompt.
Если пользователь не предоставляет запрос в рамках раундов R_prompt (TBD), хосты отказываются подписывать дальнейшие обновления состояния. хост_я не оштрафован.
Если хост_i получает приглашение через ретранслятор, но все равно не завершает работу по тайм-ауту, пропущено += 1. Несколько хостов могут подтвердить, что приглашение было доставлено.
Тайм-аут. Временная метка в MsgStartInference + T секунд. В основной сети тайм-аут основывался на высоте блока (expirationHeight). В подсети нет блоков, поэтому заменой является время на настенных часах, привязанное к метке времени StartInference. T должен учитывать полный протокол восстановления (распространение nonce + MsgRequestPrompt + ретрансляция подсказки + выполнение).
Стимулы. Протокол восстановления удаляет оба вектора атаки:
- Пользователь не может выборочно ограничить доступ к множеству данных. Группа обнаруживает пробел посредством распространения nonce и запрашивает подсказку через посредников. Если пользователь отказывается в ходе раундов R_prompt, хосты перестают подписывать.
- Хост не может делать вид, что не получил данные. Группа доставит его через эстафету. Если хост по-прежнему не выполняет вычисления, очевидно, он виноват.
Пользователь создает StartInference, но не предоставляет данные на хост_i.
Охвачено протоколом восстановления, описанным выше. Это причина «скрытых пользователем данных подсказки». Как только распространение no-кода обнаруживает пробел, MsgRequestPrompt заставляет пользователя предоставить данные или сталкивается с хостами, отказывающимися подписывать.
Пользователь отправляет запрос на хост_i, но не записывает StartInference
Невозможно. Host_i проверяет различия и отклоняет запросы без соответствующего MsgStartInference. Нет StartInference = нет авторизации платежа = нет оснований для вычислений.
Проверка вывода
Проверка вероятностная, такая же, как и в основной сети. Каждый хост самостоятельно решает, какие выводы проверять, используя детерминированное начальное значение и ту же логику MustValidate.
В основной сети хосты фиксируют начальное число в начале эпохи и раскрывают его в конце эпохи. В подсети нет эпох. Вместо этого начальное число извлекается детерминированным образом из закрытого ключа хоста и escrow_id:seed_i = first_8_bytes(sign(escrow_id_bytes)) . Одно семя на хост за сеанс. Хост не имеет свободы выбора другого начального числа, поскольку подпись является детерминированной и открытый ключ известен.
Ключ подписи закрепляется первой государственной подписью хоста в сеансе. Каждый хост записывает, какой ключ использовали другие хосты. Во время раскрытия исходная подпись должна соответствовать закрепленному ключу. Валидатор с несколькими горячими ключами не может пробовать разные ключи во время раскрытия, чтобы повлиять на то, какие выводы он должен проверять.
Во время сеанса каждый хост использует свое начальное число, чтобы решить, какие готовые выводы следует проверить. Если этот параметр выбран, хост_i повторно выполняет вывод, сравнивает логиты и отправляет MsgValidation в состояние подсети.
Раскрытие начального числа происходит во время обязательного финального раунда (см. «Соглашение»). Каждый хост отправляет MsgRevealSeed(подпись). Другие хосты получают начальное число из подписи, сверяют его с известным открытым ключом, повторно запускают MustValidate для всех завершенных выводов и подсчитывают промахи. Результаты соответствия передаются в файл host_stats перед расчетом.
Мы рассматривали возможность получения начального числа из подписи состояния хоста в каждом nonce (без фиксации-раскрытия, без завершающего раунда). Это позволяет избежать дополнительного раунда, но требует, чтобы подписи были частью штата для проверки соответствия. Сигнатуры намеренно не находятся в состоянии, поскольку они поступают асинхронно и нарушают детерминированное хеширование состояния. Завершающий раунд с раскрытием в целом проще, а также устраняет необходимость в дереве Меркла в поселении.
Примечание. Завершающий раунд потенциально может быть исключен, если процесс проверки будет переработан, чтобы не требовать схемы фиксации-раскрытия (например, начальные значения, полученные из данных, уже находящихся в состоянии). Это позволит заселиться в любой момент, не дожидаясь полного группового раунда, что повысит живость. Требует дальнейшей доработки протокола валидации.
Поселение
Прежде чем отправить расчет в основную сеть, пользователь должен завершить финальный раунд. Пользователь отправляет пустые запросы (без нового MsgStartInference) в циклическом режиме всей группе. Каждый хост прикрепляет ожидающие MsgFinishInference, MsgRevealSeed и все оставшиеся MsgValidation. После полного раунда все выводы разрешаются, все начальные значения раскрываются, проверяется соответствие валидации, а статистика хоста является окончательной.
Затем пользователь отправляет MsgSettleEscrow (см. Основной сетевой протокол выше) в основную сеть. Mainnet проверяет 2/3+ взвешенных по слоту подписей по (state_root || escrow_id || nonce) и рассчитывает условное депонирование: каждый хост оплачивается из условного депонирования в соответствии с хостом_stats[slot].cost, оставшийся баланс возвращается пользователю.
Примечание. В будущем список отдельных подписей можно заменить агрегированной подписью BLS, чтобы уменьшить размер передаваемых данных.
Урегулирование входит в окно спора из X блоков (TBD). В течение этого окна любой хост может отправить конкурирующее состояние с более высоким значением nonce и 2/3+ подписями. Если такое состояние существует, значит, пользователь отправил устаревшее состояние: все оставшееся условное депонирование передается хостам в качестве штрафа. Если в пределах X блоков не появляется ни одного конкурирующего государства, урегулирование завершается.
Пользователь исчезает. Любой член группы может отправить MsgSettleEscrow после таймаута. Все хосты имеют полное состояние в течение одного раунда (распространяется через различия). Если на хосте отсутствует недавнее состояние, он может запросить его у других хостов через конечную точку общедоступного API. Те же требования к подписи 2/3+, то же окно спора. ЗАДАЧА: определить триггер тайм-аута (настенные часы от последнего одноразового номера против высоты истечения срока условного депонирования при создании).
Раздутое состояние. Пользователь заявляет о меньшем использовании, чем было на самом деле (чтобы получить больший возврат средств). Требуется 2/3+ подписей хостов для ложного состояния. Сводится к предположению BFT: безопасно, пока <1/3 хостов, взвешенных по слотам, являются вредоносными.
Примеры запросов
Третий запрос на счастливом пути (отправлен на h3). Содержит все накопленные различия с сигнатурами, собранными на данный момент.
POST /chat/completions Хост: h3 { "model": "Qwen/Qwen3-235B-A22B-Instruct-2507-FP8", "stream": true, "messages": [ {"role": "user", "content": "Написать хайку о Сиэтле."} ], "diffs": [ {"nonce": 1, "txs": ["MsgStartInference(1)"], "sigs": ["sig_h1"]}, {"nonce": 2, "txs": ["MsgStartInference(2)"], "sigs": ["sig_h2"]}, {"nonce": 3, "txs": ["MsgFinishInference(1)", "MsgStartInference(3)"], "sigs": []} ], "state_hash": "<SHA256>" }
Для сравнения, первый запрос (к h1) несет только одну разницу:
{ ... "diffs": [ {"nonce": 1, "txs": ["MsgStartInference(1)"], "sigs": []} ], "state_hash": "<SHA256>" }
Каждый diff представляет собой блок в заданный nonce. Сигнатуры более ранних одноразовых номеров накапливаются с течением времени по мере того, как хосты возвращают их. По третьему запросу sig_h1 (возвращается с ответом req1) и sig_h2 (возвращается с ответом req2) присоединяются к соответствующим одноразовым номерам. Nonce 3 еще не имеет подписей: h3 подпишет его и вернет в ответ sig_h3.
Веса в подсети
При формировании группы подсети повторно используется механизм выборки слотов из проверки PoC (см. предложения/poc/optimize.md (../poc/optimize.md)).
Назначение слотов — это детерминированная функция (app_hash после создания условного депонирования, escrow_id, validator_weights) с использованием того же алгоритма GetSlotsFromSorted, что и в PoC. Цепочке не нужно вычислять его при создании условного депонирования. Любой может получить группу самостоятельно. Цепочка только проверяет правильность группы во время расчета (MsgSettleEscrow).
Каждый слот сопоставляется с хостом. Если хост выбран в 3 слота, он имеет вес 3 в подсети. Каждый слот имеет вес 1. Это сохраняет распределение веса основной сети внутри подсети, не требуя дополнительного отслеживания веса.
Последовательность слотов также определяет порядок циклического обслуживания запросов пользователей.
Требования к количеству слотов менее строгие, чем в PoC. В PoC слоты защищают от состязательной проверки (атак ложных участников). В подсети группе требуется достаточная избыточность только для обеспечения доступности и подписей расчетов. Точное количество слотов (64 против 128) пока не определено.
TODO: определить порог подписи расчета относительно количества слотов

Я бы рассматривал текущую предлагаемую реализацию «подцепи» как набросок. Существует много вариантов того, как это может работать, описанный вариант может быть первой итерацией, поскольку он относительно прост.

Это, безусловно, было бы большим улучшением. Но применение этого шаблона к более масштабируемому реестру BFT, такому как Obyte DAG, позволит масштабировать вещи на гораздо более высокие уровни и добавить богатый набор производных функций.

Пожалуйста, рассмотрите возможность присоединиться к обсуждению этого предложения и поделиться любыми идеями или предложениями. Первоначальная версия будет предоставлена автором предложения, а отзывы участников помогут усовершенствовать подход и прояснить следующие шаги.

Есть ли шанс, что тем, кто в настоящее время исключен, все еще будет разрешено присоединиться, но без права на вознаграждения? Это позволит провести дополнительное тестирование и позволит потенциальным хостам быть полностью готовыми, когда система будет готова разрешить им повторное включение. На данный момент похоже, что прошло слишком много времени, а исключенные хосты не взаимодействовали.

Proposal: Inference Scaling
Problem
Per inference, the following transactions are recorded on-chain:
MsgStartInference
MsgFinishInference
MsgValidation (0 to N_hosts per inference; let's consider case when it's 1 tx for simplicity)
3 txs per inference. Max capacity per block is ~5000 => 5000 / 3 = 1666 inferences per block => 1666 / 6 = 277 inferences per sec
Consider 4xH100 with Qwen3-235B deployed. For 5000/1000 input/output tokens, such a setup can process 3.5-4 RPS (TODO: confirm) => 277 / 3.5 * 4 = 316 H100 GPUs to saturate the chain
Requests could be batched into a single transaction, but the computation and state growth per request makes this not scalable to hundreds of thousands of inferences.
The bottleneck is better with longer requests (more compute per tx) and worse with smaller models (more RPS per GPU, more txs per GPU).
Note: in practice, the main limit is not the transaction count but the computation cost per block. It becomes a problem above a few hundred such transactions per block. Based on profiling it can be optimized 2-10x (or even 100x), but this limitation will hit before the tx count limit. The current proposal still tries to address the whole problem.
Proposal
This proposal describes an approach that moves all per-inference communication off-chain. The chain processes only two transactions: one to put coins in escrow and assign a subgroup of hosts, one to settle at the end. All inference communication and validations happen inside the subgroup directly, over a long session (e.g. one epoch). To close the session, the user submits the final usage state signed by a supermajority of hosts (threshold: 2/3 slot-weighted). Both sides have a clear incentive to settle: the user recovers the unused escrow balance, and the subgroup gets paid from it.
Effectively, as each subgroup would have to achieve consensus for the final state, the architecture will consist of:
main blockchain
many sub-chains / shards with extremely lightweight architecture
Sub-chains will be able to process only the inference related transactions and their decision might affect only the escrows, assigned to such sub-chains
Note: "sub-chain" does not have to mean a real blockchain. Because the group carries no state outside of its assigned user, groups can be dynamic: formed per session, with large overlaps between them. The only thing they share is the mainnet escrow as anchor.
Architecture
+-----------+ +-------------------+ +----------------------------+ | User | | Mainnet | | Subnet (one per session) | +-----------+ +-------------------+ +----------------------------+ | | | | 1. MsgCreateEscrow | | | (100GNK) | | | -----------------> | | | <- escrow_id, | | | group=[h1..hN] | | | | | | 2. POST /chat (req1) --------------------------> | | 3. POST /chat (req2) --------------------------> | | 4. POST /chat (reqN) --------------------------> | | ... | | | | | | 5. MsgSettleEscrow | | | (finalState, | | | signatures, ..) | | | -----------------> | | | <- user refund + | | | hosts paid | | +-----------+ +-------------------+ +----------------------------+
User sends exactly 2 transactions to mainnet: MsgCreateEscrow to open the session, MsgSettleEscrow to close it. All inference requests happen directly with the assigned subnet group; mainnet never sees individual requests.
User Flow
[mainchain]: user creates MsgCreateEscrow(100GNK)
[subchain]: user interact with hosts in subgroup in pre-defined order
[mainnet]: at the end of session, user creates MsgSettleEscrow(state_root, nonce, signatures, usage, host_stats, ...)
Q1: Who decides host punishments, the subchain or mainnet?
If the subchain decides: it needs to aggregate stats across users, which requires shared persistent state per group, which requires fixed groups rather than dynamic per-user ones.
Current approach: mainnet decides. The subchain only records raw per-session stats (missed/invalid counts per host) inside MsgSettleEscrow . Mainnet aggregates across sessions and applies punishment. Can be revisited.
Q2: Do hosts maintain per-group state or per-user state?
If per-group: same consequence as Q1 option A, fixed groups required.
Current approach: per-user, following from Q1. Each host tracks only what happened inside each user session. No shared state between users in the same group. This is what makes dynamic per-session groups possible. Can be revisited.
The further proposal follows this architecture: "chain per user".
Main Network Protocol
MsgCreateEscrow( creatorAddr amount, )
move money to escrow via MsgCreateEscrow
- return id to sample N(64?) slots-hosts using weighted random sampling (see proposals/poc/optimize.md (../poc/optimize.md) for the slot idea)
interact in sub-chain during session
settle on-chain via MsgSettleEscrow
MsgSettleEscrow( escrow_id, # session identifier state_root, # Merkle root: hash(host_stats_hash || rest_hash) nonce, # latest nonce signatures, # map[slot_id -> sig] over (state_root, escrow_id, nonce) rest_hash, # Merkle sibling: hash(balance || inferences_hash) host_stats, # map[slot_id -> HostStats] ) HostStats( missed, # execution misses (started but never finished) invalid, # inferences invalidated by challenge voting cost, # total cost of inferences executed required_validations, # inferences ShouldValidate selected for this host completed_validations, # MsgValidation txs actually submitted )
Mainnet recomputes host_stats_hash from the submitted host_stats, verifies hash(host_stats_hash || rest_hash) == state_root, then checks 2/3+ slot-weighted signatures over (state_root, escrow_id, nonce). Settlement does not require individual inference records. The mandatory finalizing round ensures all inferences are resolved and validation compliance is computed before settlement.
- On the escrow settlement, mainnet verifies the Merkle proof and 2/3+ slot-weighted signatures. Once verified it settles escrow for the user: each host is paid from escrow according to host_stats[slot].cost, remaining balance is refunded to user, host_stats are recorded.
Subnet Protocol
The subnet is a lightweight shard with voting weight provided by mainnet. It settles back to mainnet when the session ends.
Design goals: lightweight, parallelizable, enforce that the user uses all hosts from the group.
What does the user want? Send OpenAPI-compatible REST requests ( /chat/completions , /embeddings , etc.) and know as little as possible about the blockchain.
What does the chain want? Same properties we tried to achieve on mainnet:
- Know when each request starts and finishes. Other hosts measure executor performance against expected throughput and punish underperformance (missed rate).
- Know the hash of prompt (signed by user) and hash of response payload (signed by executor). Prompt signature authorizes payment. Payload signature enables probabilistic inference validation (invalid rate).
- Enforce distribution of requests across executors proportionally to their weight.
The chain needs these properties but does not want to process this data on mainnet.
Subnet transaction types (all off-chain, inside the subnet only):
MsgStartInference (user) -- authorize inference, reserve cost
MsgFinishInference (host) -- record completion, response hash, token counts
MsgValidation (host) -- validation result; valid=false opens challenge voting
MsgValidationVote (host) -- vote during challenge window
MsgTimeoutInference (host) -- declare inference timed out
MsgRequestPrompt (host) -- recovery: request prompt data the user withheld
Per-user state. State is saved per user independently. Each user's history is a chain of diffs. Each diff is essentially a block. Since there is no cross-user state, a node operator can shard its database and resources per user. Each node can participate in any number of subnets simultaneously. Subnet processing scales linearly with user count. Only escrow creation and settlement on mainnet do not.
User-driven propagation. The user is responsible for sequencing and propagating transactions. User attaches accumulated diffs to each inference request. This piggybacks propagation on normal API usage.
Round-robin host ordering. The user must iterate hosts in the group in a predefined order. This naturally distributes requests across hosts (not real work amount, but request count). Each diff carries a nonce that determines the expected recipient: slot_at_position(nonce % group_size) . The receiving host verifies it is the expected recipient for the nonce before processing. If it is not, the request is rejected. This enforces round-robin and prevents skipping.
Signing flow. When a /chat/completions request is sent to host1, the user creates MsgStartInference(1). If host1 is honest, it must immediately return (state, signature) without waiting for execution. After execution, host1 signs MsgFinishInference(1) and the user propagates it to the network in the next round (or later, depends on performance). Locks should only be needed to generate new nonces and compose new messages, not to record incoming data. The user does not block on receiving a host's signature before sending the next request. Signatures arrive asynchronously and get included in later diffs. This keeps request submission fast at the cost of signatures lagging behind by one or more rounds.
Escrow accounting. On each MsgStartInference, the subnet tracks spending against the user's escrow balance. Same idea as mainnet: verify user has enough funds before accepting the request. Minimum escrow balance must be at least subnet_size * max_inference_cost at all times, ensuring enough to cover the worst case where every host in the group is processing a concurrent request.
Host unavailability. If a host is not available, the user continues to the next host in order. Since each request carries ALL accumulated diffs for the current round, it includes the unsigned diff for the unavailable host. Detection and recovery are handled via nonce propagation (see scenarios below).
Nonce propagation. After processing each user request, the receiving host gossips the current nonce to the group. Small constant-overhead message. Each host tracks the highest nonce seen. If host_i sees that nonce has advanced past its assigned position but was never contacted, it detects a gap and can act proactively. This is the only reliable detection mechanism: other hosts cannot distinguish "still computing" from "never received data" by looking at diffs (execution time varies), and signature lag is normal (signatures always trail by at least one round).
Host-proposed transactions. Hosts produce transactions (MsgFinishInference, invalidation triggers, etc.) that must be included in the state. The user is the sequencer, but cannot be trusted to include them. Propagation channels:
- Response body: host returns its proposed transactions to the user alongside the inference result.
- Lazy gossip: host pushes proposed transactions to other hosts only if the user hasn't included them after K rounds. Zero overhead in the happy path.
- Public endpoint: each host exposes its unsettled transactions per session. Fallback if lazy gossip fails.
Inclusion enforcement. Two different rules depending on who proposed the transaction:
- User-proposed (MsgStartInference): must appear in the very next round's diffs. The user has them at creation time, no reason for delay.
- Host-proposed (MsgFinishInference, etc.): K rounds grace period (TBD). Accounts for async lag. After K rounds without inclusion, hosts trigger lazy gossip and refuse to sign.
Each host response includes its unsettled mempool so the user always knows what's pending.
Retry on refusal. If a host refuses to sign because the user hasn't included pending transactions, the user retries the same nonce with the missing transactions appended. The diff at a given nonce is append-only: a retry must be a strict superset of the original attempt. The host stores the first attempt's tx list and rejects any retry that removes or replaces transactions from it. This prevents equivocation -- the user cannot create two conflicting versions of the same nonce. K rounds is generous (tens of requests across the full group), so a well-behaved user client includes all known pending transactions automatically and never hits refusal.
Scenarios
Everyone is working correctly
Group = [h1, h2, h3, h4, h5], user sends 3 requests in round-robin order.
User -> h1: POST /chat/completions (req1) diffs: [MsgStartInference(1)] h1: starts executing, signs state(nonce=1), returns (sig_h1, mempool=[]) h1: after execution, creates MsgFinishInference(1), gossips to h2..h5 User -> h2: POST /chat/completions (req2) diffs: [MsgStartInference(1), MsgStartInference(2)] // no sig_h1 yet h2: signs state(nonce=2), returns (sig_h2, mempool=[]) User -> h3: POST /chat/completions (req3) diffs: [MsgStartInference(1) + sig_h1, MsgStartInference(2) + sig_h2, MsgFinishInference(1), MsgStartInference(3)] h3: checks local mempool:MsgFinishInference(1) present (via gossip), included, ok h3: signs state(nonce=3), returns (sig_h3, mempool=[])
Transaction statuses after 3 requests:
MsgStartInference(1): settled (3 sigs: h1, h2, h3)
MsgStartInference(2): proposed (2 sigs: h2, h3)
MsgFinishInference(1): proposed (1 sig: h3)
MsgStartInference(3): proposed (1 sig: h3)
The user is the sequencer: it decides at which nonce each transaction is placed. All hosts seeing the same nonce see the same content. Signatures lag behind by one or more rounds.
Host doesn't respond or doesn't finish inference
MsgStartInference(N) exists in the state but MsgFinishInference(N) never arrives. Possible causes:
Host genuinely down, didn't receive the request
Connection broke between user and host mid-request
Host received data but refuses to compute
User recorded MsgStartInference but withheld prompt data from the host
Attribution is hard. The user could attack a host by recording MsgStartInference but withholding prompt data. The host could attack by pretending not to have received it. Both look identical from the outside. Without a recovery mechanism, whoever is honest gets punished.
If host_i signed the state at nonce N: host_i acknowledged receipt. The signature propagates through later diffs, so all hosts can verify host_i had the data. If MsgFinishInference(N) doesn't arrive by timeout, missed += 1 for host_i. No ambiguity.
If host_i never signed: ambiguous. Recovery protocol applies.
Recovery protocol:
- host_i detects via nonce propagation that a nonce assigned to it has passed without receiving data.
- host_i gossips MsgRequestPrompt(N) to the group.
- Each host that sees MsgRequestPrompt(N) independently includes it in its next response to the user: "provide prompt for nonce N."
- A small relay group is sampled using a mainnet block hash as randomness. MsgRequestPrompt includes a target_height field set to the current known mainnet height + small delta (e.g., +2 blocks). The relay group is hash(escrow_id, inference_id, block_hash_at_target_height) % group_size . Nobody knows the block hash at target_height when MsgRequestPrompt is created, so the user cannot grind for a favorable relay group. Resolving the relay group requires one bridge call to fetch the block hash once target_height is reached. This only happens on the recovery path (rare). host_i has already committed to the claim (via gossip) before the block is produced.
- User provides prompt data to the relay group. Each member signs a receipt and relays to host_i independently.
- host_i computes, produces MsgFinishInference(N). User can reconnect to host_i directly for the response, or receive it through a relay member.
- If host_i still hasn't received the data, host_i can re-request with another MsgRequestPrompt.
If user doesn't provide prompt within R_prompt rounds (TBD), hosts refuse to sign further state updates. host_i not penalized.
If host_i receives prompt via relay but still doesn't finish by timeout, missed += 1. Multiple hosts can attest the prompt was delivered.
Timeout. Timestamp in MsgStartInference + T seconds. On mainnet, timeout was block-height-based (expirationHeight). In the subnet there are no blocks, so wall-clock time anchored to the StartInference timestamp is the replacement. T must account for the full recovery protocol (nonce propagation + MsgRequestPrompt + prompt relay + execution).
Incentives. The recovery protocol removes both attack vectors:
- User cannot selectively starve a host of data. The group detects the gap via nonce propagation and requests the prompt through intermediaries. If the user refuses within R_prompt rounds, hosts stop signing.
- Host cannot pretend it didn't receive data. The group will deliver it via relay. If the host still doesn't compute, it's clearly at fault.
User creates StartInference but doesn't provide data to host_i
Covered by the recovery protocol above. This is the "user withheld prompt data" cause. Nonce propagation detects the gap, MsgRequestPrompt forces the user to provide data or face hosts refusing to sign.
User sends request to host_i but doesn't record StartInference
Not possible. host_i checks the diffs and rejects requests without a corresponding MsgStartInference. No StartInference = no payment authorization = no reason to compute.
Inference Validation
Validation is probabilistic, same as on mainnet. Each host independently decides which inferences to validate using a deterministic seed and the same ShouldValidate logic.
On mainnet, hosts commit a seed at epoch start and reveal it at epoch end. The subnet has no epochs. Instead, the seed is derived deterministically from the host's private key and the escrow_id: seed_i = first_8_bytes(sign(escrow_id_bytes)) . One seed per host per session. The host has no freedom to choose a different seed since signing is deterministic and the public key is known.
The signing key is pinned by the host's first state signature in the session. Each host records which key other hosts used. At reveal time, the seed signature must match the pinned key. A validator with multiple warm keys cannot try different keys at reveal time to influence which inferences it must validate.
During the session, each host uses its seed to decide which finished inferences to validate. If selected, host_i re-executes the inference, compares logits, and submits MsgValidation into subnet state.
Seed reveal happens during the mandatory finalizing round (see Settlement). Each host submits MsgRevealSeed(signature). Other hosts derive the seed from the signature, verify it against the known public key, re-run ShouldValidate for all finished inferences, and count misses. Compliance results go into host_stats before settlement.
We considered deriving the seed from the host's state signature at each nonce (no commit-reveal, no finalizing round). This avoids the extra round but requires signatures to be part of state for compliance verification. Signatures are deliberately not in state because they arrive asynchronously and would break deterministic state hashing. The finalizing round with reveal is simpler overall and also removes the need for a Merkle tree in settlement.
Note: the finalizing round could potentially be eliminated if the validation process is redesigned to not require a commit-reveal scheme (e.g. seeds derived from data already in state). This would allow settlement at any point without waiting for a full group round, improving liveness. Requires further refinement of the validation protocol.
Settlement
Before submitting settlement to mainnet, the user must complete a finalizing round. The user sends empty requests (no new MsgStartInference) in round-robin to the full group. Each host attaches pending MsgFinishInference, MsgRevealSeed, and any remaining MsgValidation. After the full round, all inferences are resolved, all seeds are revealed, validation compliance is checked, and host_stats are final.
User then submits MsgSettleEscrow (see Main Network Protocol above) to mainnet. Mainnet verifies 2/3+ slot-weighted signatures over (state_root || escrow_id || nonce) and settles the escrow: each host is paid from escrow according to host_stats[slot].cost, remaining balance is refunded to user.
Note: the list of individual signatures can be replaced with an aggregated BLS signature in the future to reduce tx size.
Settlement enters a dispute window of X blocks (TBD). During the window, any host can submit a competing state with a higher nonce and 2/3+ signatures. If such a state exists, the user submitted stale state: all remaining escrow goes to hosts as penalty. If no competing state appears within X blocks, settlement finalizes.
User disappears. Any group member can submit MsgSettleEscrow after a timeout. All hosts have full state within one round (propagated via diffs). If a host is missing recent state, it can request it from other hosts via the public API endpoint. Same 2/3+ signature requirement, same dispute window. TODO: define timeout trigger (wall-clock from last nonce vs escrow expiry height at creation).
Inflated state. User claims less usage than actually happened (to get a larger refund). Requires 2/3+ host signatures over the false state. Reduces to BFT assumption: safe as long as <1/3 of slot-weighted hosts are malicious.
Example requests
Third request in the happy path (sent to h3). Carries all accumulated diffs with signatures collected so far.
POST /chat/completions Host: h3 { "model": "Qwen/Qwen3-235B-A22B-Instruct-2507-FP8", "stream": true, "messages": [ {"role": "user", "content": "Write a haiku about Seattle."} ], "diffs": [ {"nonce": 1, "txs": ["MsgStartInference(1)"], "sigs": ["sig_h1"]}, {"nonce": 2, "txs": ["MsgStartInference(2)"], "sigs": ["sig_h2"]}, {"nonce": 3, "txs": ["MsgFinishInference(1)", "MsgStartInference(3)"], "sigs": []} ], "state_hash": "<SHA256>" }
For comparison, the first request (to h1) carries only one diff:
{ ... "diffs": [ {"nonce": 1, "txs": ["MsgStartInference(1)"], "sigs": []} ], "state_hash": "<SHA256>" }
Each diff is a block at a given nonce. Signatures for earlier nonces accumulate over time as hosts return them. By the 3rd request, sig_h1 (returned with req1 response) and sig_h2 (returned with req2 response) are attached to their respective nonces. Nonce 3 has no signatures yet: h3 will sign it and return sig_h3 in the response.
Weights in subnet
Subnet group formation reuses the slot sampling mechanism from PoC validation (see proposals/poc/optimize.md (../poc/optimize.md) ).
Slot assignment is a deterministic function of (app_hash after escrow creation, escrow_id, validator_weights) using the same GetSlotsFromSorted algorithm as in PoC. The chain does not need to compute it at escrow creation. Anyone can derive the group independently. The chain only verifies the group was correct at settlement time (MsgSettleEscrow).
Each slot maps to a host. If a host is sampled into 3 slots, it has weight 3 in the subnet. Each slot carries weight 1. This preserves the mainnet weight distribution inside the subnet without requiring any additional weight tracking.
The slot sequence also defines the round-robin order for user requests.
Requirements for slot count are less strict than in PoC. In PoC, slots protect against adversarial validation (fake participant attacks). In the subnet, the group only needs enough redundancy for availability and settlement signatures. The exact slot count (64 vs 128) is TBD.
TODO: define settlement signature threshold relative to slot count

I'd consider the current proposed implementation of "subchain" as a sketch. There are many options how it can work, the described option can be the first iteration as it's relatively straightforward

This would certainly be a big improvement. But applying this pattern to a more scalable BFT ledger, such as the Obyte DAG, would scale things to much higher levels and add a rich set of derivative functionality.

Please consider joining the discussion on this proposal and sharing any ideas or suggestions. An initial version will be provided by the proposal author, and feedback from participants will help refine the approach and clarify next steps.

Any chance those currently excluded could be still allowed to join but with no entitlement to rewards? This would allow for more testing and allow prospective hosts to be fully ready when the system is ready to allow them back on. At the moment it looks like too much time will have passed with no interaction from excluded hosts.
Предложение: масштабирование вывода
Проблема
Согласно выводам, следующие транзакции записываются в цепочке:
Мсгстартинференце
MsgFinishInference
MsgValidation (от 0 до N_hosts на вывод; для простоты рассмотрим случай, когда это 1 tx)
3 транзакции за вывод. Максимальная емкость на блок составляет ~5000 => 5000/3 = 1666 выводов на блок => 1666/6 = 277 выводов в секунду.
Рассмотрим 4xH100 с развернутым Qwen3-235B. Для 5000/1000 токенов ввода/вывода такая установка может обрабатывать 3,5-4 RPS (TODO: подтвердить) => 277 / 3,5 * 4 = 316 графических процессоров H100 для насыщения цепочки.
Запросы могут быть объединены в одну транзакцию, но вычисления и рост состояния на каждый запрос делают это не масштабируемым для сотен тысяч выводов.
Узкое место лучше проявляется при более длинных запросах (больше вычислений на передачу) и хуже у моделей меньшего размера (больше запросов в секунду на графический процессор, больше потоков на графический процессор).
Предложение
Это предложение описывает подход, который выводит всю коммуникацию для каждого вывода за пределы цепочки. Цепочка обрабатывает только две транзакции: одну для помещения монет на условное депонирование и назначения подгруппы хостов, другую для расчета в конце. Вся передача выводов и проверки происходят непосредственно внутри подгруппы в течение длительного сеанса (например, одной эпохи). Чтобы закрыть сеанс, пользователь отправляет окончательное состояние использования, подписанное подавляющим большинством хостов (порог: 2/3 взвешенных по слотам). У обеих сторон есть явный стимул к урегулированию: пользователь возвращает неиспользованный остаток условного депонирования, а подгруппа получает от него оплату.
Фактически, поскольку каждая подгруппа должна будет достичь консенсуса относительно окончательного состояния, архитектура будет состоять из:
основной блокчейн
множество подцепей/шардов с чрезвычайно легкой архитектурой
Субцепи смогут обрабатывать только транзакции, связанные с выводом, и их решение может повлиять только на условное депонирование, назначенное таким субцепям.
Архитектура
Пользователь отправляет в основную сеть ровно 2 транзакции: MsgCreateEscrow для открытия сеанса и MsgSettleEscrow для его закрытия. Все запросы на вывод выполняются напрямую с назначенной группой подсети; Mainnet никогда не видит отдельные запросы.
Пользовательский поток
[mainchain]: пользователь создает MsgCreateEscrow(100GNK)
Вопрос 1: Кто решает, какие наказания будут наказывать хосты: субчейн или основная сеть?
Если субцепочка примет решение: ей необходимо агрегировать статистику по пользователям, что требует общего постоянного состояния для каждой группы, что требует фиксированных групп, а не динамических групп для каждого пользователя.
Текущий подход: решает основная сеть. Субцепочка записывает только необработанную статистику за сеанс (пропущенные/недействительные счетчики для каждого хоста) внутри MsgSettleEscrow. Mainnet объединяет данные по сеансам и применяет наказание. Можно пересмотреть.
Вопрос 2. Поддерживаются ли хосты для отдельных групп или для отдельных пользователей?
Если для каждой группы: те же последствия, что и для варианта A Q1, требуются фиксированные группы.
Текущий подход: по каждому пользователю, как следует из Q1. Каждый хост отслеживает только то, что происходит внутри каждого пользовательского сеанса. Нет общего состояния между пользователями в одной группе. Именно это делает возможными динамические группы для каждого сеанса. Можно пересмотреть.
Дальнейшее предложение следует этой архитектуре: «цепочка на пользователя».
Основной сетевой протокол
MsgCreateEscrow (сумма CreatorAddr, )
перевести деньги на условное депонирование через MsgCreateEscrow
взаимодействовать в подцепи во время сеанса
произвести расчет внутри сети через MsgSettleEscrow
MsgSettleEscrow( escrow_id, # идентификатор сеанса state_root, # корень Merkle: hash(host_stats_hash || rest_hash) nonce, # последние подписи nonce, # map[slot_id -> sig] поверх (state_root, escrow_id, nonce) rest_hash, # родной брат Merkle: hash(balance || inferences_hash) host_stats, # map[slot_id -> HostStats] ) HostStats( пропущено, # промахов при выполнении (начато, но не завершено) неверно, # выводы признаны недействительными из-за стоимости голосования по вызову, # общая стоимость выполненных выводов require_validations, # выводов MustValidate, выбранных для этого хоста Completed_validations, # фактически отправленные txs MsgValidation )Основная сеть пересчитывает host_stats_hash из отправленной host_stats, проверяет хэш (host_stats_hash || rest_hash) == state_root, затем проверяет 2/3+ взвешенных по слотам подписей (state_root, escrow_id, nonce). Для расчета не требуются отдельные записи выводов. Обязательный финальный раунд гарантирует, что все выводы будут решены, а соответствие валидации вычислено перед расчетом.
Протокол подсети
Подсеть представляет собой легкий сегмент, вес голосования которого предоставляется основной сетью. Когда сеанс заканчивается, он возвращается в основную сеть.
Цели проектирования: легкий, распараллеливаемый, обеспечить, чтобы пользователь использовал все хосты из группы.
Чего хочет пользователь? Отправляйте запросы REST, совместимые с OpenAPI ( /chat/completions , /embeddings и т. д.) и узнавайте как можно меньше о блокчейне.
Чего хочет сеть? Те же свойства, которые мы пытались достичь в основной сети:
Цепочке нужны эти свойства, но она не хочет обрабатывать эти данные в основной сети.
Типы транзакций подсети (все вне цепочки, только внутри подсети):
MsgFinishInference (хост) — завершение записи, хэш ответа, количество токенов
MsgValidation (хост) — результат проверки; valid=false открывает голосование по вызову
MsgValidationVote (хост) — голосование во время окна вызова
MsgTimeoutInference (хост) — объявить, что время ожидания истекло
Состояние каждого пользователя. Состояние сохраняется для каждого пользователя независимо. История каждого пользователя представляет собой цепочку различий. Каждый дифф по сути представляет собой блок. Поскольку межпользовательского состояния нет, оператор узла может сегментировать свою базу данных и ресурсы для каждого пользователя. Каждый узел может одновременно участвовать в любом количестве подсетей. Обработка подсети линейно масштабируется в зависимости от количества пользователей. Только создание условного депонирования и расчеты в основной сети этого не делают.
Распространение, управляемое пользователем. Пользователь несет ответственность за последовательность и распространение транзакций. Пользователь прикрепляет накопленные различия к каждому запросу вывода. Это параллельное распространение при обычном использовании API.
Циклический заказ хоста. Пользователь должен перебирать хосты в группе в заранее определенном порядке. Это естественным образом распределяет запросы по хостам (не реальный объем работы, а количество запросов). Каждая разница содержит одноразовый номер, который определяет ожидаемого получателя: slot_at_position(nonce % group_size) . Перед обработкой принимающий хост проверяет, что он является ожидаемым получателем nonce. Если это не так, запрос отклоняется. Это обеспечивает циклический перебор и предотвращает пропуски.
Поток подписания. Когда запрос /chat/completions отправляется на хост1, пользователь создает MsgStartInference(1). Если хост1 честен, он должен немедленно вернуть (состояние, подпись), не дожидаясь выполнения. После выполнения хост1 подписывает MsgFinishInference(1), и пользователь передает его в сеть в следующем раунде (или позже, в зависимости от производительности). Блокировки должны быть необходимы только для генерации новых одноразовых номеров и составления новых сообщений, а не для записи входящих данных. Пользователь не блокирует получение подписи хоста перед отправкой следующего запроса. Сигнатуры поступают асинхронно и включаются в последующие различия. Это позволяет ускорить отправку запросов за счет задержки подписей на один или несколько раундов.
Эскроу-учет. При каждом MsgStartInference подсеть отслеживает расходы по условному балансу пользователя. Та же идея, что и в основной сети: убедитесь, что у пользователя достаточно средств, прежде чем принять запрос. Минимальный баланс условного депонирования всегда должен быть как минимум размером subnet_size * max_inference_cost, обеспечивая достаточную сумму для покрытия наихудшего случая, когда каждый хост в группе обрабатывает одновременный запрос.
Недоступность хоста. Если хост недоступен, пользователь переходит к следующему хосту по порядку. Поскольку каждый запрос содержит ВСЕ накопленные различия за текущий раунд, он включает беззнаковые различия для недоступного хоста. Обнаружение и восстановление выполняются посредством распространения nonce (см. сценарии ниже).
Распространение одноразового номера. После обработки каждого пользовательского запроса принимающий хост сообщает группе текущий одноразовый номер. Небольшое сообщение с постоянными накладными расходами. Каждый хост отслеживает самый высокий увиденный nonce. Если хост_i видит, что nonce продвинулся за назначенную ему позицию, но с ним так и не связались, он обнаруживает пробел и может действовать упреждающе. Это единственный надежный механизм обнаружения: другие хосты не могут отличить «все еще вычисления» от «никогда не полученные данные», просматривая различия (время выполнения варьируется), а задержка подписи является нормальной (сигнатуры всегда отстают как минимум на один раунд).
Транзакции, предложенные хостом. Хосты создают транзакции (MsgFinishInference, триггеры аннулирования и т. д.), которые необходимо включить в состояние. Пользователь является секвенсором, но ему нельзя доверять его включение. Каналы распространения:
Обеспечение включения. Два разных правила в зависимости от того, кто предложил сделку:
Каждый ответ хоста включает свой неурегулированный мемпул, поэтому пользователь всегда знает, что находится в ожидании.
Повторите попытку в случае отказа. Если хост отказывается подписывать, потому что пользователь не включил ожидающие транзакции, пользователь повторяет тот же одноразовый номер с добавлением отсутствующих транзакций. Разница в данном nonce доступна только для добавления: повторная попытка должна быть строгим расширением исходной попытки. Хост сохраняет список tx первой попытки и отклоняет любую повторную попытку, которая удаляет или заменяет транзакции из него. Это предотвращает двусмысленность — пользователь не может создать две конфликтующие версии одного и того же nonce. K раундов — это щедро (десятки запросов по всей группе), поэтому пользовательский клиент с хорошим поведением автоматически включает все известные ожидающие транзакции и никогда не получает отказ.
Сценарии
Все работают корректно
Группа = [h1, h2, h3, h4, h5], пользователь отправляет 3 запроса в циклическом порядке.
Пользователь -> h1: различия POST /chat/completions (req1): [MsgStartInference(1)] h1: начинает выполнение, подписывает состояние (nonce=1), возвращает (sig_h1, mempool=[]) h1: после выполнения создает MsgFinishInference(1), сообщает h2..h5 Пользователь -> h2: различия POST /chat/completions (req2): [MsgStartInference(1), MsgStartInference(2)] // пока нет sig_h1 h2: подписывает состояние(nonce=2), возвращает (sig_h2, mempool=[]) Пользователь -> h3: POST /chat/completions (req3) различия: [MsgStartInference(1) + sig_h1, MsgStartInference(2) + sig_h2, MsgFinishInference(1), MsgStartInference(3)] h3: проверяет локальный мемпул: MsgFinishInference(1) присутствует (через сплетни), включено, ок h3: подписывает состояние (nonce=3), возвращает (sig_h3, mempool=[])Статусы транзакций после 3 запросов:
MsgStartInference(1): установлено (3 сигнала: h1, h2, h3)
MsgStartInference(2): предлагается (2 знака: h2, h3)
MsgFinishInference(1): предложено (1 подпись: h3)
MsgStartInference(3): предлагается (1 подпись: h3)
Пользователь является секвенсором: он решает, в какой nonce помещается каждая транзакция. Все хосты, видящие один и тот же nonce, видят один и тот же контент. Подписи отстают на один или несколько раундов.
Хост не отвечает или не завершает вывод
MsgStartInference(N) существует в состоянии, но MsgFinishInference(N) никогда не приходит. Возможные причины:
Хост действительно недоступен, не получил запрос
Соединение разорвано между пользователем и хостом во время запроса
Хост получил данные, но отказывается вычислять
Атрибуция сложна. Пользователь может атаковать хост, записав MsgStartInference, но скрывая данные подсказки. Хост может атаковать, притворившись, что не получил его. Оба внешне выглядят одинаково. Без механизма восстановления тот, кто честен, будет наказан.
Если хост_i подписал состояние сразу N: хост_i подтвердил получение. Подпись распространяется через более поздние различия, поэтому все хосты могут убедиться, что у хоста есть данные. Если MsgFinishInference(N) не приходит по тайм-ауту, пропущено += 1 для хоста_i. Никакой двусмысленности.
Если хост_i никогда не подписывался: неоднозначно. Протокол восстановления применяется.
Протокол восстановления:
Если пользователь не предоставляет запрос в рамках раундов R_prompt (TBD), хосты отказываются подписывать дальнейшие обновления состояния. хост_я не оштрафован.
Если хост_i получает приглашение через ретранслятор, но все равно не завершает работу по тайм-ауту, пропущено += 1. Несколько хостов могут подтвердить, что приглашение было доставлено.
Тайм-аут. Временная метка в MsgStartInference + T секунд. В основной сети тайм-аут основывался на высоте блока (expirationHeight). В подсети нет блоков, поэтому заменой является время на настенных часах, привязанное к метке времени StartInference. T должен учитывать полный протокол восстановления (распространение nonce + MsgRequestPrompt + ретрансляция подсказки + выполнение).
Стимулы. Протокол восстановления удаляет оба вектора атаки:
Пользователь создает StartInference, но не предоставляет данные на хост_i.
Охвачено протоколом восстановления, описанным выше. Это причина «скрытых пользователем данных подсказки». Как только распространение no-кода обнаруживает пробел, MsgRequestPrompt заставляет пользователя предоставить данные или сталкивается с хостами, отказывающимися подписывать.
Пользователь отправляет запрос на хост_i, но не записывает StartInference
Невозможно. Host_i проверяет различия и отклоняет запросы без соответствующего MsgStartInference. Нет StartInference = нет авторизации платежа = нет оснований для вычислений.
Проверка вывода
Проверка вероятностная, такая же, как и в основной сети. Каждый хост самостоятельно решает, какие выводы проверять, используя детерминированное начальное значение и ту же логику MustValidate.
В основной сети хосты фиксируют начальное число в начале эпохи и раскрывают его в конце эпохи. В подсети нет эпох. Вместо этого начальное число извлекается детерминированным образом из закрытого ключа хоста и escrow_id:seed_i = first_8_bytes(sign(escrow_id_bytes)) . Одно семя на хост за сеанс. Хост не имеет свободы выбора другого начального числа, поскольку подпись является детерминированной и открытый ключ известен.
Ключ подписи закрепляется первой государственной подписью хоста в сеансе. Каждый хост записывает, какой ключ использовали другие хосты. Во время раскрытия исходная подпись должна соответствовать закрепленному ключу. Валидатор с несколькими горячими ключами не может пробовать разные ключи во время раскрытия, чтобы повлиять на то, какие выводы он должен проверять.
Во время сеанса каждый хост использует свое начальное число, чтобы решить, какие готовые выводы следует проверить. Если этот параметр выбран, хост_i повторно выполняет вывод, сравнивает логиты и отправляет MsgValidation в состояние подсети.
Раскрытие начального числа происходит во время обязательного финального раунда (см. «Соглашение»). Каждый хост отправляет MsgRevealSeed(подпись). Другие хосты получают начальное число из подписи, сверяют его с известным открытым ключом, повторно запускают MustValidate для всех завершенных выводов и подсчитывают промахи. Результаты соответствия передаются в файл host_stats перед расчетом.
Поселение
Прежде чем отправить расчет в основную сеть, пользователь должен завершить финальный раунд. Пользователь отправляет пустые запросы (без нового MsgStartInference) в циклическом режиме всей группе. Каждый хост прикрепляет ожидающие MsgFinishInference, MsgRevealSeed и все оставшиеся MsgValidation. После полного раунда все выводы разрешаются, все начальные значения раскрываются, проверяется соответствие валидации, а статистика хоста является окончательной.
Затем пользователь отправляет MsgSettleEscrow (см. Основной сетевой протокол выше) в основную сеть. Mainnet проверяет 2/3+ взвешенных по слоту подписей по (state_root || escrow_id || nonce) и рассчитывает условное депонирование: каждый хост оплачивается из условного депонирования в соответствии с хостом_stats[slot].cost, оставшийся баланс возвращается пользователю.
Урегулирование входит в окно спора из X блоков (TBD). В течение этого окна любой хост может отправить конкурирующее состояние с более высоким значением nonce и 2/3+ подписями. Если такое состояние существует, значит, пользователь отправил устаревшее состояние: все оставшееся условное депонирование передается хостам в качестве штрафа. Если в пределах X блоков не появляется ни одного конкурирующего государства, урегулирование завершается.
Пользователь исчезает. Любой член группы может отправить MsgSettleEscrow после таймаута. Все хосты имеют полное состояние в течение одного раунда (распространяется через различия). Если на хосте отсутствует недавнее состояние, он может запросить его у других хостов через конечную точку общедоступного API. Те же требования к подписи 2/3+, то же окно спора. ЗАДАЧА: определить триггер тайм-аута (настенные часы от последнего одноразового номера против высоты истечения срока условного депонирования при создании).
Раздутое состояние. Пользователь заявляет о меньшем использовании, чем было на самом деле (чтобы получить больший возврат средств). Требуется 2/3+ подписей хостов для ложного состояния. Сводится к предположению BFT: безопасно, пока <1/3 хостов, взвешенных по слотам, являются вредоносными.
Примеры запросов
Третий запрос на счастливом пути (отправлен на h3). Содержит все накопленные различия с сигнатурами, собранными на данный момент.
POST /chat/completions Хост: h3 { "model": "Qwen/Qwen3-235B-A22B-Instruct-2507-FP8", "stream": true, "messages": [ {"role": "user", "content": "Написать хайку о Сиэтле."} ], "diffs": [ {"nonce": 1, "txs": ["MsgStartInference(1)"], "sigs": ["sig_h1"]}, {"nonce": 2, "txs": ["MsgStartInference(2)"], "sigs": ["sig_h2"]}, {"nonce": 3, "txs": ["MsgFinishInference(1)", "MsgStartInference(3)"], "sigs": []} ], "state_hash": "<SHA256>" }Для сравнения, первый запрос (к h1) несет только одну разницу:
Каждый diff представляет собой блок в заданный nonce. Сигнатуры более ранних одноразовых номеров накапливаются с течением времени по мере того, как хосты возвращают их. По третьему запросу sig_h1 (возвращается с ответом req1) и sig_h2 (возвращается с ответом req2) присоединяются к соответствующим одноразовым номерам. Nonce 3 еще не имеет подписей: h3 подпишет его и вернет в ответ sig_h3.
Веса в подсети
При формировании группы подсети повторно используется механизм выборки слотов из проверки PoC (см. предложения/poc/optimize.md (../poc/optimize.md)).
Назначение слотов — это детерминированная функция (app_hash после создания условного депонирования, escrow_id, validator_weights) с использованием того же алгоритма GetSlotsFromSorted, что и в PoC. Цепочке не нужно вычислять его при создании условного депонирования. Любой может получить группу самостоятельно. Цепочка только проверяет правильность группы во время расчета (MsgSettleEscrow).
Каждый слот сопоставляется с хостом. Если хост выбран в 3 слота, он имеет вес 3 в подсети. Каждый слот имеет вес 1. Это сохраняет распределение веса основной сети внутри подсети, не требуя дополнительного отслеживания веса.
Последовательность слотов также определяет порядок циклического обслуживания запросов пользователей.
Требования к количеству слотов менее строгие, чем в PoC. В PoC слоты защищают от состязательной проверки (атак ложных участников). В подсети группе требуется достаточная избыточность только для обеспечения доступности и подписей расчетов. Точное количество слотов (64 против 128) пока не определено.
TODO: определить порог подписи расчета относительно количества слотов