Качество поиска в продукте ИИ-поиска ограничено двумя факторами: качеством модели эмбеддингов и стоимостью её запуска по всему индексу. На этой неделе инженерная команда Perplexity опубликовала материал Быстрые эмбеддинги на GPU — подробный разбор второго аспекта, то есть инфраструктуры обслуживания, лежащей в основе pplx-embed и моделей ранжирования, используемых в Perplexity Search, Computer и API Platform.
Команда Perplexity утверждает, что инференс эмбеддингов на стороне GPU в значительной степени унифицировался между разными движками на зрелом оборудовании Hopper и Blackwell. Основные улучшения находятся в среде выполнения и вспомогательной инфраструктуре вокруг модели: управлении графами CUDA, абстракции асинхронного отслеживания результатов и пути обработки запросов на Rust.
Два типа трафика, один движок
Perplexity рассматривает обслуживание эмбеддингов как две рабочие нагрузки. Пакетное создание эмбеддингов выполняется при построении или переиндексации векторной базы данных, где пропускная способность снижает стоимость. Онлайн-создание эмбеддингов происходит во время запроса, когда короткий запрос необходимо быстро преобразовать в эмбеддинг. Ранжирование занимает промежуточное положение: после векторного поиска ранжируются большие пакеты документов, что требует баланса между обоими факторами.
Ключевое решение заключается в том, что Perplexity не стала создавать отдельный движок эмбеддингов. Поскольку модели эмбеддингов представляют собой небольшие трансформеры, пакетное создание эмбеддингов напоминает вычислительно-зависимую предварительную обработку, а онлайн-создание эмбеддингов, часто состоящее из нескольких токенов, — декодирование, ограниченное пропускной способностью памяти. Поэтому исследовательская команда повторно использует ядра предварительной обработки и декодирования из своего стека LLM.
Ivy, Tulip и ROSE
Запрос обрабатывается тремя сервисами:
- Ivy — это HTTP-шлюз на Rust. Он выполняет работу на стороне CPU: разбирает JSON, осуществляет токенизацию, применяет шаблоны к входным данным, разделяет пакеты и преобразует запросы в пользовательский протокол gRPC. Он также разбивает запросы с большими пакетами на части и балансирует их между репликами, устраняя дисбаланс нагрузки, возникающий при разном размере рабочих данных в production.
- Tulip — это интерфейс сервера инференса: gRPC-сервер, созданный с использованием Rust,
tokio и tonic, который занимается планированием и пакетированием перед передачей запросов движку.
- ROSE (Runtime-Optimized Serving Engine) реализует инференс модели. Он преимущественно написан на Python, предоставляет ядра, слои и определения моделей, управляет графами CUDA и предоставляет Tulip функцию
step().
Почему планировщик намеренно сделан простым
Tulip выбирает последовательности в порядке поступления, пока запросы накапливаются. Такая простота оправдана измерениями: для небольших моделей эмбеддингов и длин последовательностей, с которыми работает Perplexity, линейная стоимость плотных слоёв преобладает над квадратичной стоимостью механизма внимания. Поэтому задержка примерно пропорциональна количеству токенов, а не количеству последовательностей. Когда пакет насыщает GPU — примерно при 512 токенах для модели с менее чем миллиардом параметров — добавление новых последовательностей больше не повышает эффективность.
Графы CUDA и LazyTensors
На небольших пакетах запуск ядер на стороне CPU может занимать больше времени, чем их выполнение на GPU. Perplexity создаёт графы CUDA для всей модели всех моделей эмбеддингов, объединяя каждый запуск в один вызов драйвера. Поскольку модели эмбеддингов небольшие, точка, в которой вычисления на GPU начинают превышать стоимость запусков, достигается при пакетах в несколько тысяч токенов и десятках последовательностей. Некоторые реализации механизма внимания препятствуют созданию графов всей модели, поскольку зависят от динамических входных данных на стороне хоста; Perplexity внесла изменения в основную ветку FlashInfer, чтобы сделать захват возможным.
Графы необходимо захватывать для каждой конфигурации, поэтому количество токенов дополняется до значений, кратных 64 или 256. Даже это приводит к появлению тысяч графов и нескольким минутам захвата для каждой модели. Решение — ленивый захват: для каждой конфигурации выполняется прогревочный запуск в обычном режиме, после чего при втором обращении запускаются захват и воспроизведение. При запуске это увеличивает задержку p99, но распределяет несколько минут предварительной работы на несколько часов.
Второй компонент — это LazyTensor, который отслеживает закреплённый в памяти буфер хоста, а также cudaMemcpyAsync и событие CUDA. Вместо того чтобы функция step() блокировалась в ожидании устройства, она возвращает LazyTensor, позволяя асинхронной задаче Rust ожидать пакет N, пока CPU ставит в очередь пакет N+1.
Отправить запрос</button></div>
</div>
</div>
<div class="pe-panel" id="peP1">
<div class="pe-card">
<div class="pe-txt">Для небольших пакетов запуск ядер на стороне CPU может занимать больше времени, чем работа GPU. Граф CUDA для всей модели объединяет каждый запуск в один вызов драйвера, освобождая CPU для постановки в очередь следующего пакета. Переключите два режима.</div>
<div class="pe-ctl" style="margin:0 0 12px">
<button class="pe-btn ghost on" id="peEager">Запуски в обычном режиме</button>
<button class="pe-btn ghost" id="peGraph">Граф CUDA</button>
</div>
<div class="pe-lane"><div class="pe-lbl">Хост / CPU</div><div class="pe-track" id="peCpuT"></div></div>
<div class="pe-lane"><div class="pe-lbl">Устройство / GPU</div><div class="pe-track" id="peGpuT"></div></div>
<div class="pe-stats">
<div class="pe-stat"><div class="v" id="peLaunches">—</div><div class="k">Вызовы драйвера</div></div>
<div class="pe-stat"><div class="v" id="peGap">—</div><div class="k">Простои GPU</div></div>
</div>
<div class="pe-txt" style="margin:12px 0 0;font-size:11.5px;color:#6E8285">Схематично. Ширина блоков иллюстрирует описанную в публикации схему накладных расходов на запуск, а не измеренное время выполнения.</div>
</div>
</div>
<div class="pe-panel" id="peP2">
<div class="pe-card">
<div class="pe-txt">Обычное чтение результатов обратно требует синхронизации с хостом. <b>LazyTensor</b> отслеживает закреплённый в памяти буфер хоста, асинхронное копирование с устройства на хост и событие CUDA, поэтому Tulip может ожидать пакет N, пока CPU уже подготавливает пакет N+1.</div>
<div class="pe-lane"><div class="pe-lbl">CPU — подготовка / синхронизация</div><div class="pe-track" id="peLzC"></div></div>
<div class="pe-lane"><div class="pe-lbl">GPU — прямой проход</div><div class="pe-track" id="peLzG"></div></div>
<div class="pe-ctl">
<button class="pe-btn" id="peLzRun"> Запустить 3 пакета</button>
<button class="pe-btn ghost on" id="peLzOn">С перекрытием</button>
<button class="pe-btn ghost" id="peLzOff">С блокировкой</button>
</div>
<div class="pe-note" style="margin-top:12px" id="peLzNote">С перекрытием: пока GPU обрабатывает пакет N, CPU уже токенизирует и упаковывает пакет N+1.</div>
</div>
</div>
<div class="pe-panel" id="peP3">
<div class="pe-card">
<div class="pe-txt">Для небольших моделей эмбеддингов при таких длинах последовательностей линейная стоимость плотных слоёв преобладает над квадратичной стоимостью механизма внимания, поэтому задержка зависит от <b>количества токенов, а не количества последовательностей</b>. После примерно <b>512 токенов</b> в модели с менее чем 1 млрд параметров GPU насыщается, и добавление новых последовательностей перестаёт помогать.</div>
<div class="pe-lbl" style="margin-top:6px">Токенов в пакете: <span id="peTokV" style="color:#3FB6C4">512</span></div>
<input type="range" id="peTok" min="32" max="4096" step="32" value="512">
<div class="pe-lbl">Утилизация GPU</div>
<div class="pe-bar"><div class="pe-fill" id="peUtil"></div></div>
<div class="pe-stats">
<div class="pe-stat"><div class="v" id="peUtilV">—</div><div class="k">Насыщение</div></div>
<div class="pe-stat"><div class="v" id="peState">—</div><div class="k">Режим</div></div>
</div>
<div class="pe-txt" style="margin:12px 0 0;font-size:11.5px;color:#6E8285">Иллюстративная кривая. Точка насыщения примерно в 512 токенов указана в публикации; форма кривой между точками условна и не является результатом бенчмарка.</div>
</div>
</div>
<div class="pe-foot">
<span>Источник: Perplexity Engineering, «Быстрые эмбеддинги на GPU» (4 сентября 2026 г.)</span>
<span><a href="https://www.marktechpost.com">Создано Marktechpost</a></span>
</div>
<script>
(function(){
var R=document.getElementById('pplxEmbedExplainer');
var NOTES=[
'<b>Ivy</b> — разбирает JSON, выполняет токенизацию с помощью внутреннего unigram-токенизатора, применяет шаблоны к входным данным и разделяет большие пакеты, а затем преобразует их в пользовательский протокол gRPC. Он также балансирует части между репликами.',
'<b>Tulip</b> — gRPC-сервер на Rust, работающий на tokio и tonic. Запросы накапливаются, пока сервер выполняет их или ожидает; последовательности выбираются в порядке поступления и объединяются в пакет для ускорителя.',
'<b>ROSE</b> — Runtime-Optimized Serving Engine. Ядра и слои, определённые на Python, управление графами CUDA и функция step(), возвращающая дескриптор вычислений на GPU. Для эмбеддингов KV-кэш не выделяется.'
];
function q(s){return R.querySelector(s)} function qa(s){return R.querySelectorAll(s)}
/* вкладки */
qa('.pe-tab').forEach(function(t){t.addEventListener('click',function(){
qa('.pe-tab').forEach(function(x){x.classList.remove('on')});t.classList.add('on');
qa('.pe-panel').forEach(function(p,i){p.classList.toggle('on',i==+t.dataset.p)});});});
/* 1 поток */
var note=q('#peNote'),dot=q('#peDot');
function sel(i){qa('.pe-node').forEach(function(n,k){n.classList.toggle('hot',k==i)});note.innerHTML=NOTES[i];}
qa('.pe-node').forEach(function(n){n.addEventListener('click',function(){sel(+n.dataset.i)})});
sel(0);
var busy=false;
q('#peGo').addEventListener('click',function(){
if(busy)return;busy=true;var steps=[[0,'3%'],[1,'40%'],[2,'76%']],k=0;
dot.style.transition='none';dot.style.left='3%';dot.style.opacity='1';
sel(0);
var iv=setInterval(function(){k++;if(k>2){clearInterval(iv);dot.style.opacity='0';busy=false;return;}
dot.style.transition='left .8s cubic-bezier(.4,0,.2,1)';dot.style.left=steps[k][1];sel(k);},900);
});
/* 2 графы CUDA */
var cpuT=q('#peCpuT'),gpuT=q('#peGpuT'),mode='eager';
function blk(p,l,w,cls,txt){var d=document.createElement('div');d.className='pe-blk '+cls;d.style.left=l+'%';d.style.width=w+'%';d.textContent=txt||'';p.appendChild(d);}
function drawG(){
cpuT.innerHTML='';gpuT.innerHTML='';
if(mode=='eager'){
for(var i=0;i<6;i++){blk(cpuT,1+i*16.4,7,'pe-cpu','запуск');blk(gpuT,8.4+i*16.4,7.6,'pe-gpu','ядро');if(i<5)blk(gpuT,16+i*16.4,7.2,'pe-idle','простой');}
q('#peLaunches').textContent='6';q('#peGap').textContent='5';
}else{
blk(cpuT,1,12,'pe-cpu','запуск графа');blk(cpuT,15,26,'pe-cpu','подготовка следующего пакета');
blk(gpuT,13.5,84,'pe-gpu','6 ядер — одно воспроизведение, без пробелов');
q('#peLaunches').textContent='1';q('#peGap').textContent='0';
}}
q('#peEager').addEventListener('click',function(){mode='eager';q('#peEager').classList.add('on');q('#peGraph').classList.remove('on');drawG();});
q('#peGraph').addEventListener('click',function(){mode='graph';q('#peGraph').classList.add('on');q('#peEager').classList.remove('on');drawG();});
drawG();
/* 3 lazytensor */
var lzC=q('#peLzC'),lzG=q('#peLzG'),lzMode='on',lzBusy=false,lzNote=q('#peLzNote');
function drawLz(){
lzC.innerHTML='';lzG.innerHTML='';
for(var i=0;i<3;i++){
if(lzMode=='on'){blk(lzC,2+i*32,14,'pe-cpu','подг. '+(i+1));blk(lzG,17+i*32,26,'pe-gpu','пакет '+(i+1));}
else{blk(lzC,2+i*32,12,'pe-cpu','подг. '+(i+1));blk(lzC,15+i*32,16,'pe-idle','ожидание');blk(lzG,15+i*32,15,'pe-gpu','пакет '+(i+1));}
}}
function lzRun(){
if(lzBusy)return;lzBusy=true;drawLz();
var bs=lzG.querySelectorAll('.pe-blk'),cs=lzC.querySelectorAll('.pe-blk');
[].forEach.call(bs,function(b){b.style.opacity='.15'});[].forEach.call(cs,function(b){b.style.opacity='.15'});
var all=[].concat([].slice.call(cs),[].slice.call(bs)),j=0;
var iv=setInterval(function(){if(j>=all.length){clearInterval(iv);lzBusy=false;return;}all[j].style.opacity='1';j++;},220);
}
q('#peLzRun').addEventListener('click',lzRun);
q('#peLzOn').addEventListener('click',function(){lzMode='on';q('#peLzOn').classList.add('on');q('#peLzOff').classList.remove('on');lzNote.innerHTML='С перекрытием: пока GPU обрабатывает пакет N, CPU уже токенизирует и упаковывает пакет N+1.';drawLz();});
q('#peLzOff').addEventListener('click',function(){lzMode='off';q('#peLzOff').classList.add('on');q('#peLzOn').classList.remove('on');lzNote.innerHTML='С блокировкой: каждый вызов step() ожидает устройство, поэтому CPU простаивает, а GPU запускается с задержкой.';drawLz();});
drawLz();
/* 4 форма пакета */
var tok=q('#peTok');
function drawT(){
var v=+tok.value;q('#peTokV').textContent=v;
var u=Math.min(100,Math.round(100*(1-Math.exp(-v/230))));
q('#peUtil').style.width=u+'%';q('#peUtilV').textContent=u+'%';
q('#peState').textContent=v<512?'Недозаполнен':(v<1200?'Насыщен':'Ограничен пропускной способностью');
q('#peState').style.color=v<512?'#C7A24E':'#3FB6C4';
}
tok.addEventListener('input',drawT);drawT();
})();
</script>
</div>
<script>
(function(){function h(){var e=document.getElementById('pplxEmbedExplainer');if(!e)return;parent.postMessage({pplxEmbedH:e.offsetHeight+40},'*');}
window.addEventListener('load',h);setTimeout(h,300);setTimeout(h,1200);
document.addEventListener('click',function(){setTimeout(h,250)});
document.addEventListener('input',function(){setTimeout(h,120)});
if(window.ResizeObserver){var e=document.getElementById('pplxEmbedExplainer');if(e)new ResizeObserver(h).observe(e);}})();
</script>
</body></html>">
Ядра по-прежнему важны
ROSE поддерживает несколько бэкендов механизма внимания для неравномерных входных данных: FlashInfer 2, FlashInfer 3 и FlashAttention 4. Команда Perplexity сообщает, что FlashAttention 4 в целом работает быстрее, однако FlashInfer 3 превосходит его на моделях на базе Qwen при очень большой длине последовательности, поэтому бэкенд выбирается в каждом случае отдельно. Примечательно, что при обслуживании модели эмбеддингов ROSE не создаёт KV-кэш и использует варианты механизма внимания для неравномерных входных данных, чтобы избежать дополнения последовательностей.
Бенчмарки
Perplexity проводит сравнение с vLLM v0.22.0 в формате BF16 на реальных весах и входных данных, полученных из оценочных наборов, а прогревочные запуски подтверждают, что расхождение косинусного сходства не превышает 0,1%. На графиках представлены четыре набора тестов: эмбеддинги с низкой задержкой (пакет 1; 128/512/4096 токенов), ранжирование с низкой задержкой (пакеты 5/25/50 по 512 токенов), эмбеддинги с высокой пропускной способностью (пакет 100, четыре параллельных процесса) и эмбеддинги с высокой степенью параллелизма (от 1 до 16 параллельных запросов, включая токенизацию Ivy и сетевые накладные расходы).
Главные выводы
- Стек эмбеддингов Perplexity повторно использует ядра предварительной обработки и декодирования LLM, а не отдельный движок.
- Задержка зависит от количества токенов, а не от количества последовательностей; примерно 512 токенов насыщают модель с менее чем 1 млрд параметров.
- Графы CUDA для всей модели вместе с ленивым захватом сокращают накладные расходы на запуск без многоминутного времени старта.
LazyTensor позволяет одновременно подготавливать пакет на CPU и выполнять работу на GPU, вместо блокировки в ожидании синхронизации.
- Ivy, Tulip и ROSE являются внутренними компонентами; получить доступ к pplx-embed можно через API эмбеддингов Perplexity.
Ознакомьтесь с техническими подробностями. Также подписывайтесь на нас в Twitter и не забудьте присоединиться к нашему сабреддиту о машинном обучении с более чем 150 тыс. участников и подписаться на нашу рассылку. Стоп! Вы есть в Telegram? Теперь вы также можете присоединиться к нам в Telegram.
Хотите сотрудничать с нами в продвижении вашего репозитория GitHub, страницы Hugging Face, релиза продукта, вебинара и т. д.? Свяжитесь с нами
Переведено автоматически с английского. Оригинал статьи — по ссылке ниже.