English translation is not available yet. Showing Russian content.
Как бы вы добавили "отмену" (cancellation) для длительных LLM операций?
Краткий тезис
Отмена длительных LLM-операций — это критически важная фича для UX и экономии ресурсов. Она строится на сквозной цепочке: от UI-кнопки «Отмена» до прерывания генерации на сервере. Архитектурно это требует Request ID для каждой операции, асинхронного паттерна с корутинами и cancellation-токенами на клиенте, а также поддержки abort на стороне LLM-сервера (vLLM, TGI). Ключевой принцип — «отмена должна быть немедленной и распространяться до пользователя».
1. Термин: Cancellation (отмена) в контексте LLM
Cancellation — это механизм, позволяющий пользователю или системе прервать выполнение длительной операции генерации текста до её завершения.
Почему это важно
- UX Пользователь не должен ждать 30+ секунд, если передумал или ошибся в запросе. Немедленная обратная связь («Отменено») повышает удовлетворённость.
- Экономия ресурсов Прерывание генерации освобождает GPU/CPU для других запросов. Каждая лишняя секунда генерации — это затраты на compute.
- Безопасность Если LLM начала генерировать нежелательный контент, отмена позволяет остановить процесс.
Термин «Длительная операция» (Long-running operation) — любая генерация, которая занимает больше времени, чем пользователь готов ждать (обычно > 5-10 секунд). Для LLM это типично: генерация длинных ответов, агентные цепочки, batch-обработка.
2. Сквозная архитектура отмены
Отмена — это не одна функция, а цепочка, проходящая через все слои системы.
[UI/Клиент] --> [API Gateway] --> [Backend Service] --> [LLM Server]
| | | |
|-- Кнопка |-- DELETE /gen |-- Cancel токен |-- Abort
| "Отмена" | {id} | в корутине | запроса
2.1. Уровень 1: UI/Клиент — Request ID и кнопка
Каждая генерация получает уникальный Request ID (UUID). Это ключ для всей цепочки отмены.
Пример на Python (клиентская библиотека):
import asyncio
import uuid
class LLMClient:
def __init__(self, base_url):
self.base_url = base_url
self.active_requests = {} # request_id -> asyncio.Task
async def generate(self, prompt):
request_id = str(uuid.uuid4())
# Создаём асинхронную задачу
task = asyncio.create_task(self._do_generate(request_id, prompt))
self.active_requests[request_id] = task
try:
result = await task
return result
except asyncio.CancelledError:
# Обрабатываем отмену
await self._cancel_on_server(request_id)
return {"status": "cancelled", "request_id": request_id}
finally:
self.active_requests.pop(request_id, None)
async def cancel(self, request_id):
task = self.active_requests.get(request_id)
if task:
task.cancel() # Отменяем корутину
async def _do_generate(self, request_id, prompt):
# Здесь реальный HTTP-запрос к API с streaming
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/generate",
json={"prompt": prompt, "request_id": request_id},
) as resp:
# Читаем поток
async for chunk in resp.content:
# Обрабатываем чанки
yield chunk
Термин «Streaming» — генерация ответа частями (токенами), а не целиком. Это позволяет пользователю видеть прогресс и отменять на ранней стадии.
2.2. Уровень 2: API Gateway — DELETE /generations/{id}
API Gateway предоставляет RESTful эндпоинт для отмены.
Пример эндпоинта
DELETE /api/v1/generations/{request_id}
Логика
- Gateway получает request_id.
- Проверяет, существует ли такой запрос (в Redis или in-memory store).
- Отправляет сигнал отмены в Backend Service (через message queue или прямой gRPC-вызов).
- Возвращает пользователю {"status": "cancelled", "request_id": "..."}.
Термин «Idempotency» (идемпотентность) — повторный вызов DELETE с тем же request_id не должен вызывать ошибку. Если запрос уже отменён или завершён, возвращаем 200 OK с соответствующим статусом.
2.3. Уровень 3: Backend Service — Корутины и Cancellation Token
В Python для асинхронной работы используем asyncio и cancellation token.
Термин «Cancellation Token» — объект, который передаётся в асинхронную функцию и позволяет сигнализировать об отмене. В Python это asyncio.CancelledError или кастомный asyncio.Event.
Пример:
import asyncio
async def generate_with_cancellation(prompt, cancel_event: asyncio.Event):
"""Генерация с поддержкой отмены."""
for i in range(100):
# Проверяем, не отменён ли запрос
if cancel_event.is_set():
raise asyncio.CancelledError("Generation cancelled by user")
# Симуляция генерации токена
await asyncio.sleep(0.1)
yield f"token_{i}"
async def main():
cancel_event = asyncio.Event()
# Запускаем генерацию
task = asyncio.create_task(
async_generator_to_list(generate_with_cancellation("prompt", cancel_event))
)
# Через 2 секунды отменяем
await asyncio.sleep(2)
cancel_event.set()
try:
result = await task
except asyncio.CancelledError:
print("Generation was cancelled")
Термин «Graceful cancellation» (корректная отмена) — процесс, при котором сервер завершает текущую операцию без потери данных и освобождает ресурсы. Не путать с «kill» (принудительное завершение процесса).
2.4. Уровень 4: LLM Server — Abort Request
Современные LLM-серверы (vLLM, TGI, Triton Inference Server) поддерживают прерывание запроса.
from vllm import AsyncLLMEngine, SamplingParams
engine = AsyncLLMEngine(...)
async def generate_with_abort(prompt, request_id):
sampling_params = SamplingParams(temperature=0.7)
results_generator = engine.generate(prompt, sampling_params, request_id)
# В другом месте можно вызвать engine.abort(request_id)
async for result in results_generator:
yield result
TGI (Text Generation Inference):
- Поддерживает DELETE /generate/stream/{request_id} для отмены стриминговой генерации.
Термин «Abort» — принудительное прерывание генерации на уровне сервера. После abort сервер не возвращает больше токенов и освобождает GPU-память.
3. Propagation до пользователя: немедленный ответ
Когда отмена сработала, важно немедленно сообщить пользователю.
Варианты
- HTTP Streaming (SSE): Сервер отправляет специальное событие event: cancelled\ndata: {"request_id": "..."}.
- WebSocket Сервер отправляет сообщение {"type": "cancelled", "request_id": "..."}.
- Polling: Клиент периодически проверяет статус запроса (менее предпочтительно из-за задержки).
Пример SSE
event: token
data: {"token": "Hello"}
event: token
data: {"token": " world"}
event: cancelled
data: {"request_id": "abc-123", "reason": "user_cancelled"}
Термин «Server-Sent Events (SSE)» — протокол, позволяющий серверу отправлять данные клиенту в реальном времени через одно HTTP-соединение.
4. Обработка краевых случаев
| Сценарий | Решение |
|---|---|
| Отмена после завершения | Идемпотентность: возвращаем 200 OK с {"status": "completed"} |
| Отмена во время генерации | Немедленный abort на сервере, освобождение ресурсов |
| Отмена, когда сервер упал | Timeout на клиенте: если нет ответа > N секунд, считаем отменённым |
| Множественные отмены | Игнорируем повторные вызовы (идемпотентность) |
| Отмена batch-запроса | Отменяем все подзапросы в batch'е |
5. Таймауты как форма отмены
Помимо пользовательской отмены, есть таймауты (timeouts) — автоматическая отмена по истечении времени.
Термин «Timeout» — максимальное время ожидания ответа от LLM.
Пример:
async def generate_with_timeout(prompt, timeout=10):
try:
async with asyncio.timeout(timeout):
result = await llm.generate(prompt)
return result
except asyncio.TimeoutError:
# Автоматическая отмена
await cancel_request(request_id)
return {"status": "timeout", "message": "Generation timed out"}
Рекомендации
- Устанавливать таймауты на основе ожидаемой длины ответа (например, 1 секунда на 100 токенов + 5 секунд буфера).
- Для стриминга — таймаут на первый токен (TTFT — Time to First Token).
6. Мониторинг и логирование
Важно логировать все отмены для анализа.
Пример структуры лога
{
"event": "cancellation",
"request_id": "abc-123",
"user_id": "user_456",
"reason": "user_cancelled",
"tokens_generated": 42,
"latency_ms": 3200,
"timestamp": "2024-01-15T10:30:00Z"
}
Метрики для мониторинга
cancellation_rate— доля отменённых запросов (норма: < 5%).cancellation_latency— время от нажатия кнопки до остановки генерации.tokens_wasted— количество сгенерированных токенов до отмены.
Пет-проект для закрепления
Задача Реализовать систему отмены для чат-бота на основе LLM с веб-интерфейсом.
Инструменты
- Python + FastAPI (backend)
- asyncio + aiohttp (асинхронный клиент)
- vLLM (LLM сервер)
- Redis (хранение статусов запросов)
- HTML/JS (UI с кнопкой "Отмена")
Шаги:
- Настройка vLLM Запустите vLLM с поддержкой abort (
--enable-abort). - Backend API
- Эндпоинт
POST /generate— принимает prompt, возвращаетrequest_id. - Эндпоинт
DELETE /generate/{request_id}— отменяет запрос. - Используйте
asyncio.Eventдля cancellation token.
- Эндпоинт
- UI
- Поле ввода + кнопка "Отправить".
- После отправки — кнопка "Отмена" (активна до завершения).
- Используйте Server-Sent Events для получения токенов и события отмены.
- Интеграция
- При нажатии "Отмена" клиент отправляет
DELETE /generate/{request_id}. - Backend устанавливает cancellation event, vLLM abort'ит запрос.
- Клиент получает
event: cancelledи отображает "Генерация отменена".
- При нажатии "Отмена" клиент отправляет
Ожидаемый результат
- Работающий чат-бот, где можно отменить генерацию в любой момент.
- Логирование всех отмен в Redis.
- Метрики: время отклика на отмену, количество отменённых токенов.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 87 | Как бы вы спроектировали систему для batch-обработки LLM запросов? |
| 89 | Как бы вы реализовали rate limiting для LLM API? |
| 90 | Как бы вы спроектировали систему для A/B тестирования LLM промптов? |
| 91 | Как бы вы добавили поддержку streaming в LLM API? |
| 92 | Как бы вы спроектировали систему для логирования всех LLM запросов? |
| 93 | Как бы вы реализовали retry logic для LLM запросов? |
Навигация
- Предыдущий: 87
- Следующий: 89
- Индекс: 00. Индекс разборов