Как бы вы добавили "отмену" (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}

Логика

  1. Gateway получает request_id.
  2. Проверяет, существует ли такой запрос (в Redis или in-memory store).
  3. Отправляет сигнал отмены в Backend Service (через message queue или прямой gRPC-вызов).
  4. Возвращает пользователю {"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) поддерживают прерывание запроса.

vLLM

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 до пользователя: немедленный ответ

Когда отмена сработала, важно немедленно сообщить пользователю.

Варианты

  1. HTTP Streaming (SSE): Сервер отправляет специальное событие event: cancelled\ndata: {"request_id": "..."}.
  2. WebSocket Сервер отправляет сообщение {"type": "cancelled", "request_id": "..."}.
  3. 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 секунд буфера).
  • Для стриминга — таймаут на первый токен (TTFTTime 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 с кнопкой "Отмена")

Шаги:

  1. Настройка vLLM Запустите vLLM с поддержкой abort (--enable-abort).
  2. Backend API
    • Эндпоинт POST /generate — принимает prompt, возвращает request_id.
    • Эндпоинт DELETE /generate/{request_id} — отменяет запрос.
    • Используйте asyncio.Event для cancellation token.
  3. UI
    • Поле ввода + кнопка "Отправить".
    • После отправки — кнопка "Отмена" (активна до завершения).
    • Используйте Server-Sent Events для получения токенов и события отмены.
  4. Интеграция
    • При нажатии "Отмена" клиент отправляет 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 запросов?

Навигация