English translation is not available yet. Showing Russian content.

Что такое packing sequences и зачем он нужен?

Краткий тезис

Packing sequences — это техника оптимизации обучения LLM, при которой несколько коротких последовательностей объединяются в одну длинную с помощью специального токена-разделителя (например, EOS). Это позволяет избежать потерь вычислительных ресурсов на padding (добавление пустых токенов) и увеличить throughput (пропускную способность) обучения на 30–50%. В контексте Agentic RAG, где система часто обрабатывает множество коротких запросов и ответов инструментов, packing особенно эффективен.


1. Проблема: неэффективность паддинга при обучении LLM

При обучении языковых моделей на батчах (batch) последовательности разной длины обычно выравниваются с помощью padding — добавления специального токена [Вики/padded sequences|PAD до максимальной длины в батче. Это приводит к двум проблемам:

  • Вычислительные потери: модель вычисляет attention и для паддинг-токенов, хотя они не несут информации. Например, если в батче есть последовательности длиной 10 и 1000 токенов, 99% вычислений тратится на паддинг.
  • Искажение обучения: паддинг-токены могут влиять на градиенты (если не маскируются), хотя обычно используется attention mask для их игнорирования. Но даже с маской вычисления на паддинг не отменяются.

Термин throughput — количество обработанных токенов в секунду. Паддинг снижает throughput, так как GPU тратит время на бесполезные токены.


2. Что такое packing sequences

Packing sequences (упаковка последовательностей) — это метод, при котором несколько коротких последовательностей из батча конкатенируются (склеиваются) в одну длинную последовательность. Между ними вставляется специальный токен-разделитель, обычно EOS (End of Sequence). Затем эта упакованная последовательность подаётся на вход модели, а attention маскируется так, чтобы токены из разных исходных последовательностей не «видели» друг друга.

Пример:

  • Исходные последовательности: ["Привет", "Как дела?", "Что нового?"]
  • После packing: "Привет<EOS>Как дела?<EOS>Что нового?<EOS>" (или без последнего EOS, зависит от реализации).

Attention mask для такой упакованной последовательности — это блочно-диагональная матрица, где каждый блок соответствует одной исходной последовательности. Токены внутри блока видят друг друга, но не видят токены из других блоков.


3. Зачем нужен packing sequences

Основные цели:

  • Увеличение throughput: GPU обрабатывает больше реальных токенов за один проход. Типичный прирост — 30–50% (иногда до 2x) в зависимости от распределения длин.
  • Снижение времени обучения: меньше wasted computation на паддинг.
  • Экономия памяти: хотя упакованная последовательность длиннее, чем каждая отдельная, общее количество токенов в батче остаётся примерно тем же, но без паддинга. Однако attention mask становится разреженной, что может увеличить overhead, но в современных реализациях (например, FlashAttention) это компенсируется.

В контексте Agentic RAG агент может генерировать множество коротких вызовов инструментов и ответов. Packing позволяет эффективно обучать модель на таких данных, не раздувая батч паддингом.


4. Как работает attention masking при packing

Для упакованной последовательности attention mask строится следующим образом:

  • Каждая исходная последовательность получает уникальный идентификатор (например, sequence_id).
  • Маска разрешает attention только между токенами с одинаковым sequence_id.
  • Токены-разделители (EOS) обычно приписываются к предыдущей последовательности или игнорируются.

Пример маски для двух последовательностей [A, B] и [C, D], упакованных как [A, B, EOS, C, D]:

ABEOSCD
A11000
B11000
EOS00000
C00011
D00011

Здесь EOS изолирован (не участвует в attention), а блоки A-B и C-D независимы.


5. Сравнение подходов: padding vs bucketing vs packing

МетодОписаниеПреимуществаНедостатки
PaddingДополнение до макс. длины в батчеПростота реализацииПотеря compute, снижение throughput
BucketingГруппировка последовательностей похожей длины в батчиМеньше паддинга, чем random batchingСложнее реализация, может смещать распределение
PackingКонкатенация нескольких последовательностей в однуМаксимальная эффективность, нет паддингаТребует специальной маски, overhead на маску, возможны артефакты при неправильной маскировке

Packing даёт наибольший прирост throughput, особенно когда длины сильно варьируются.


6. Влияние на качество обучения

Packing sequences не должен ухудшать качество модели, если attention mask корректно изолирует последовательности. Однако есть нюансы:

  • Positional encoding: если используются абсолютные позиции (например, sinusoidal), то токены из второй последовательности получат позиции, следующие за первой. Это может нарушить индуктивное смещение модели (она привыкла, что позиция соответствует порядку в одной последовательности). Решение: использовать relative positional encoding (RoPE, ALiBi) или сбрасывать позиции для каждой последовательности (например, через position_ids).
  • Cross-contamination: если маска не идеальна, модель может «подглядывать» в соседние последовательности. Это критично для задач, где контекст строго ограничен (например, в RAG каждый документ — отдельный контекст).
  • Loss computation: обычно loss считается только для токенов, которые не являются паддингом и не являются разделителями. При packing loss суммируется по всем токенам всех последовательностей, но важно не учитывать EOS как предсказание (если он не является частью обучения).

Современные фреймворки (Hugging Face Transformers, Megatron-LM, FlashAttention-2) поддерживают packing через параметр packing=True или через кастомные коллаторы.


7. Реализация packing в PyTorch (пример)

from transformers import DataCollatorForSeq2Seq, AutoTokenizer
from torch.nn.utils.rnn import pad_sequence

class PackingDataCollator:
    """Коллатор, который упаковывает последовательности в одну."""
    def __init__(self, tokenizer, max_length=2048):
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __call__(self, features):
        # features — список словарей с 'input_ids' и 'labels'
        all_input_ids = []
        all_labels = []
        for f in features:
            all_input_ids.extend(f['input_ids'] + [self.tokenizer.eos_token_id])
            all_labels.extend(f['labels'] + [-100])  # -100 игнорируется в loss
        # Обрезать до max_length
        if len(all_input_ids) > self.max_length:
            all_input_ids = all_input_ids[:self.max_length]
            all_labels = all_labels[:self.max_length]
        # Создаём attention mask: блочно-диагональная
        # (упрощённо: каждая последовательность — блок, но здесь мы просто маскируем паддинг)
        # Для реального packing нужна информация о границах.
        # Используем специальную маску из библиотеки flash-attn или xformers.
        return {
            'input_ids': torch.tensor([all_input_ids]),
            'labels': torch.tensor([all_labels]),
            'attention_mask': torch.ones(1, len(all_input_ids))  # placeholder
        }

На практике лучше использовать готовые решения: transformers.DataCollatorForSeq2Seq с padding=False и кастомной логикой, или библиотеки вроде packing_utils из flash-attn.


8. Packing в контексте Agentic RAG

В Agentic RAG агент выполняет несколько шагов: запрос к векторной БД, вызов инструмента, генерация ответа. Каждый шаг порождает короткую последовательность (например, "<tool_call>search('query')</tool_call>"). При обучении такого агента на логах взаимодействий:

  • Много коротких последовательностей (запросы, ответы инструментов, финальные ответы).
  • Длины сильно варьируются (от 10 до 500 токенов).
  • Паддинг был бы крайне неэффективен.

Packing позволяет объединить несколько таких шагов в один батч, увеличив throughput в 2–3 раза. Это критично для быстрой итерации при fine-tuning агентов.


9. Альтернативы и дополнения

  • Dynamic batching (динамическое батчирование): последовательности группируются по длине на лету, но всё равно возможен паддинг внутри группы.
  • Gradient accumulation с packing: можно накапливать градиенты от нескольких упакованных батчей.
  • Sequence parallelism: распределение одной длинной последовательности на несколько GPU — ортогональная техника.

Пет-проект для закрепления

Задача: Реализовать packing sequences для fine-tuning небольшой модели (например, GPT-2) на датасете из коротких текстов (твиты, диалоги). Сравнить throughput и loss с обычным паддингом.

Инструменты: Python, PyTorch, Hugging Face Transformers, FlashAttention-2 (опционально).

Шаги:

  1. Загрузить датасет (например, tweet_eval или daily_dialog).
  2. Токенизировать, сохранить длины.
  3. Реализовать два коллатора: стандартный (с паддингом до макс. длины в батче) и packing (конкатенация с EOS, блочно-диагональная маска).
  4. Обучить модель на небольшом количестве шагов (100–200) с обоими коллаторами.
  5. Замерить: время на шаг, throughput (токенов/сек), loss.
  6. Визуализировать результаты.

Ожидаемый результат: Packing даёт прирост throughput на 30–50% при сопоставимом loss. Если loss отличается, нужно проверить корректность маски и positional encoding.


Связь с другими вопросами

ВопросТема
466Что такое FlashAttention и как он ускоряет обучение?
468Как работает gradient checkpointing?
470Какие существуют стратегии bucketing для батчей?
472Как оптимизировать throughput при fine-tuning LLM?
480Что такое document masking в контексте RAG?

Навигация