Tiny Universe, часть 2: Qwen-blog — путь тензора через Qwen2
Полный перевод раздела Qwen-blog из tiny-universe: устройство Qwen2 в Hugging Face Transformers глазами входного тензора — слой декодера, внимание с GQA, RoPE, причинная маска, SwiGLU-MLP и RMSNorm.
Общее устройство Qwen
Заголовок раздела «Общее устройство Qwen»Эфир с разбором архитектуры Qwen: запись трансляции.
Общая архитектура Qwen похожа на Llama2 — см. рисунок (img/framework.JPG).
flowchart TB T["Текст"] --> TOK["tokenizer: текст → id из словаря"] TOK --> EMB["embedding: id → вектор"] EMB --> L["N × Qwen2DecoderLayer<br/>(RMSNorm → Attention → +residual<br/>RMSNorm → MLP → +residual)"] M["attention_mask"] --> L L --> N["финальная RMSNorm"] N --> H1["Linear → логиты следующего токена<br/>(CausalLM)"] N --> H2["Linear → классы<br/>(SequenceClassification)"]
Здесь:
tokenizerпревращает текст в числа — номера токенов в словаре.- Числа проходят через
embeddingи превращаются во взаимно однозначно соответствующие им векторы. attention_maskзадаёт, что модель «видит»: только левую часть, правую, обе стороны и т. п.- Для разных прикладных задач —
Causal(генерация),seqcls(классификация последовательностей) и других — к базовой моделиmodelпо сути просто приставляется свой слойLinearи своя функция потерь.
1. Qwen2Config
Заголовок раздела «1. Qwen2Config»Qwen2Config хранит набор настраиваемых гиперпараметров: vocab_size, hidden_size, num_hidden_layers,
num_attention_heads и т. д. К ним можно обращаться как к полям словаря: config.pad_token_id.
1.1 Qwen2Model
Заголовок раздела «1.1 Qwen2Model»1.1.1 Инициализация
Заголовок раздела «1.1.1 Инициализация»- Задаются два атрибута модели:
padding_idx(индекс токена-заполнителя) иvocab_size(размер словаря). - Инициализируются слой эмбеддингов, слои декодера и слой нормализации.
- Слой эмбеддингов (
nn.Embedding): модель отображает входные токены в плотные векторные представления. - Слои декодера (
nn.ModuleList()): модель состоит из нескольких слоёв декодера, каждый задан классомQwen2DecoderLayer. - Слой нормализации
Qwen2RMSNorm: используется Root Mean Square Layer Normalization (RMSNorm). - Задаётся, использовать ли
gradient_checkpoint— в основном для экономии видеопамяти. - Вызывается
post_init()— код, который завершает инициализацию и делает подготовительные проверки.
class Qwen2Model(Qwen2PreTrainedModel): def __init__(self, config: Qwen2Config): super().__init__(config) self.padding_idx = config.pad_token_id self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx) self.layers = nn.ModuleList( [Qwen2DecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)] ) self.norm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.gradient_checkpointing = False # Инициализируем веса и делаем финальную обработку self.post_init()Функция post_init в основном инициализирует параметры и настраивает контрольные точки градиента
(gradient checkpointing):
def post_init(self): """ Метод, который выполняется в конце инициализации каждой модели Transformer, — для кода, которому нужны уже правильно созданные модули модели (например, для инициализации весов). """ self.init_weights() self._backward_compatibility_gradient_checkpointing()1.1.2 Forward
Заголовок раздела «1.1.2 Forward»Разберём только главный путь:
inputs_embeds = self.embed_tokens(input_ids)# эмбеддинги позиций здесь не добавляются — позиция войдёт через RoPE внутри вниманияhidden_states = inputs_embeds
for idx, decoder_layer in enumerate(self.layers): # складываем все hidden_states в кортеж if output_hidden_states: all_hidden_states += (hidden_states,) # передаём hs в очередной слой декодера layer_outputs = decoder_layer( hidden_states, attention_mask=attention_mask, position_ids=position_ids, past_key_value=past_key_value, output_attentions=output_attentions, use_cache=use_cache, ) # берём hs — выход этого слоя — и подаём в следующий слой; # нужен только первый элемент, второй — объект кэша hidden_states = layer_outputs[0]
# нормализуем hidden_states после последнего слояhidden_states = self.norm(hidden_states)
# добавляем hidden_states последнего слояif output_hidden_states: all_hidden_states += (hidden_states,)- Если включено сохранение
output_hidden_states, то в кортеже окажутся: первым — эмбеддингиinput_ids, затем выходыhsслоёв декодера доn-1-го включительно и, наконец, выход последнего слоя послеnorm. - Возвращается всё в виде объекта
BaseModelOutputWithPast.
1.2 Qwen2DecoderLayer
Заголовок раздела «1.2 Qwen2DecoderLayer»Рисунок в оригинале: img/decoderlayer.png — блок декодера: RMSNorm → самовнимание → сложение с остатком → RMSNorm → MLP → сложение с остатком.
1.2.1 Инициализация
Заголовок раздела «1.2.1 Инициализация»«Три кита» слоя: attn + MLP + norm.
QWEN2_ATTENTION_CLASSES = { "eager": Qwen2Attention, # обычный случай "flash_attention_2": Qwen2FlashAttention2, "sdpa": Qwen2SdpaAttention,}
class Qwen2DecoderLayer(nn.Module): def __init__(self, config: Qwen2Config, layer_idx: int): super().__init__() self.hidden_size = config.hidden_size self.self_attn = QWEN2_ATTENTION_CLASSES[config._attn_implementation](config, layer_idx)
self.mlp = Qwen2MLP(config) self.input_layernorm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps) self.post_attention_layernorm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)input_layernorm и post_attention_layernorm устроены одинаково — различается только место, где они применяются.
1.2.2 Forward
Заголовок раздела «1.2.2 Forward»Читать удобнее вместе с рисунком:
- Сначала копируем
hidden_statesвresidual, затем пропускаемhidden_statesчерезNormи модульattn. - Получив выход
attn, прибавляемresidual; снова сохраняем копию вresidual, пропускаемhidden_statesчерезNormиmlpи опять складываем сresidual. Результат — и есть выходнойhidden_states.
residual = hidden_states# нормализуем и подаём во вниманиеhidden_states = self.input_layernorm(hidden_states) # нормализация RMSNorm# Self Attentionhidden_states, self_attn_weights, present_key_value = self.self_attn( hidden_states=hidden_states, attention_mask=attention_mask, position_ids=position_ids, past_key_value=past_key_value, output_attentions=output_attentions, use_cache=use_cache, **kwargs,)
# складываем остаток с новыми hidden_stateshidden_states = residual + hidden_states
# Полносвязная частьresidual = hidden_states# та же нормализация RMSNormhidden_states = self.post_attention_layernorm(hidden_states)hidden_states = self.mlp(hidden_states)hidden_states = residual + hidden_states
outputs = (hidden_states,)
return outputs1.3 Qwen2Attention
Заголовок раздела «1.3 Qwen2Attention»Рисунок в оригинале: img/Qwen2Attention.png — схема модуля внимания: проекции Q/K/V, RoPE, повтор K/V по группам, скалярное произведение, маска, softmax, умножение на V, выходная проекция.
1.3.1 Инициализация
Заголовок раздела «1.3.1 Инициализация»Главные параметры:
num_key_value_heads— число голов для ключей и значений;num_key_value_groups— число групп ключей-значений, считается какnum_heads // num_key_value_heads. Это и есть реализация GQA!q_proj,k_proj,v_proj,o_proj— четыре слояLinear. Именно их обычно и «режет» LoRA при дообучении.
class Qwen2Attention(nn.Module): """Многоголовое внимание из статьи 'Attention Is All You Need'"""
def __init__(self, config: Qwen2Config, layer_idx: int): super().__init__() self.config = config self.layer_idx = layer_idx self.hidden_size = config.hidden_size self.num_heads = config.num_attention_heads self.head_dim = self.hidden_size // self.num_heads self.num_key_value_heads = config.num_key_value_heads self.num_key_value_groups = self.num_heads // self.num_key_value_heads self.max_position_embeddings = config.max_position_embeddings self.rope_theta = config.rope_theta self.is_causal = True self.attention_dropout = config.attention_dropout
if (self.head_dim * self.num_heads) != self.hidden_size: raise ValueError( f"hidden_size must be divisible by num_heads (got `hidden_size`: {self.hidden_size}" f" and `num_heads`: {self.num_heads})." ) self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=config.attention_bias) self.k_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias) self.v_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias) self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=config.attention_bias)
self.rotary_emb = Qwen2RotaryEmbedding( self.head_dim, max_position_embeddings=self.max_position_embeddings, base=self.rope_theta, )- Смысл параметров из
configсмотрите в описанииQwen2Config:
max_position_embeddings (`int`, *optional*, defaults to 32768): Максимальная длина последовательности, с которой модель вообще может работать.
rope_theta (`float`, *optional*, defaults to 10000.0): Базовый период эмбеддингов RoPE.1.3.2 Forward
Заголовок раздела «1.3.2 Forward»- Сначала пропускаем
hidden_statesчерез слоиLinearи получаемquery,keyиvalue. - Применяем вращательное позиционное кодирование
rotary_emb: берём его косинусную и синусную части, умножаем наqueryиkeyи складываем результаты — так и получается эффект вращательного кодирования. - Повторяем
key_statesиvalue_statesgroupраз и выполняем скалярное внимание (dot attn). - После скалярного произведения получаем
attn_weights, прибавляемattention_mask— так скрываются позиции, которые видеть нельзя, — затем применяемsoftmaxи умножаем наvalue_states. Получаемattn_output. - Меняем форму
attn_output(reshape), подаём вo_projи получаем окончательный выход.
# Размеры: на входе hidden_states формы (bs, T, hd)bsz, q_len, _ = hidden_states.size()
# Линейными слоями получаем query, key и valuequery_states = self.q_proj(hidden_states)key_states = self.k_proj(hidden_states)value_states = self.v_proj(hidden_states)
# Делим на головы — (bs, T, heads, hd_d), затем переставляем оси — (bs, heads, T, hd_d)query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
# Применяем вращательное кодирование к запросам и ключам: умножаем их на косинусную и синусную части# и складываем результаты — так получается вращениеcos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin, position_ids)
# Повторяем key_states и value_states num_key_value_groups разkey_states = repeat_kv(key_states, self.num_key_value_groups)value_states = repeat_kv(value_states, self.num_key_value_groups)
# Скалярное внимание: q·kᵀ / sqrt(hd_d)attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)
# Прибавляем attention_mask — задаём порядок «чтения»attn_weights = attn_weights + attention_mask
# softmax + dropout + умножение на value_statesattn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)attn_output = torch.matmul(attn_weights, value_states)
# Переставляем оси обратно и меняем формуattn_output = attn_output.transpose(1, 2).contiguous()attn_output = attn_output.reshape(bsz, q_len, self.hidden_size)
# Последний шаг — o_projattn_output = self.o_proj(attn_output)
# Возвращаем результатreturn attn_output, attn_weights, past_key_value1.3.3 Детали под отладчиком
Заголовок раздела «1.3.3 Детали под отладчиком»1.3.3.1 GQA
Заголовок раздела «1.3.3.1 GQA»Рисунок в оригинале: img/GQA.png — сравнение MHA, GQA и MQA: у MHA своя пара K/V на каждую голову запроса, у GQA одна пара K/V на группу голов, у MQA одна пара на все головы.
Главная мысль: при GQA и MQA во время инференса не нужно хранить столько KV-кэша, значит, кэш занимает меньше видеопамяти, и LLM-сервис может одновременно обслужить больше запросов.
- Задаём исходные тензоры:
import mathimport torch
## форма: (batch, seq_len, head, head_dim)query = torch.randn(10, 128, 8, 128)key = torch.randn(10, 128, 2, 128)value = torch.randn(10, 128, 2, 128)
## здесь число групп равно 4groups = query.shape[-2] // key.shape[-2]head_dim = query.shape[-1] # 128 (добавлено переводчиком: переменная нужна ниже)- Затем расширяем
keyиvalue. ВGQAуkeyиvalueголов вgroupраз меньше, чем уquery. Чтобы потом было удобно перемножать матрицы, мы сначала с помощьюexpandрасширяем головыkeyиvalueдо той же размерности, что уquery.
# На входе x; n_rep — сколько раз повторить, обычно это число группdef repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
batch, num_key_value_heads, slen, head_dim = hidden_states.shape # повторять не нужно — это обычное многоголовое внимание if n_rep == 1: return hidden_states # сначала расширяем x до (bs, head, group, seq_len, head_dim) hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim) # reshape превращает head -> head * group return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)- Перемножаем матрицы и получаем
scoreиoutput. Дальше — обычное перемножениеq,k,v:
# (bs, head, seq_len, head_dim)query = query.transpose(1, 2)key = repeat_kv(key.transpose(1, 2), 4)value = repeat_kv(value.transpose(1, 2), 4)scores = torch.matmul(query, key.transpose(2, 3)) / math.sqrt(head_dim)scores = torch.nn.functional.softmax(scores, dim=-1)
out = torch.matmul(scores, value)# выше мы переставили оси — переставляем обратноout = out.transpose(1, 2)print(out.shape) # torch.Size([10, 128, 8, 128]) (строка добавлена переводчиком)Дополнение. Почему сначала expand, а потом reshape, а не сразу встроенный repeat тензора?
expandрасширяет тензор, не выделяя новой памяти: возвращённый тензор делит данные с исходным.repeatрасширяет тензор, по-настоящему копируя данные: новый тензор не делит данные с исходным и занимает больше памяти.
1.3.3.2 apply_rotary_pos_emb
Заголовок раздела «1.3.3.2 apply_rotary_pos_emb»Позиционное кодирование придаёт каждой координате (dim) каждого токена свою информацию о позиции.
Определение (в оригинале — рисунок img/ROPE1.png; ниже — стандартная запись из статьи RoFormer):
Словами: запрос токена на позиции поворачивается на угол, пропорциональный , ключ на позиции — на угол, пропорциональный ; в их скалярное произведение входит только разность углов, то есть относительное расстояние .
Идея: вращательное кодирование даёт каждому токену и относительную, и абсолютную позицию.
- Его можно понимать как поправку к матрице самовнимания , которая напрямую отражает относительное положение двух токенов, — и в то же время разложить на векторы и , где кодируется абсолютная позиция каждого токена.
- По сути RoPE — это вращение векторов признаков. Возьмём для примера двумерный вектор признаков: для пары соседних координат используется один и тот же угол , а вращение задаётся так (в оригинале — img/ROPE2.png):
Отсюда видно главное: результат поворота (или ) — это сам вектор, умноженный на косинус, плюс вектор с переставленными координатами, у одной из которых сменён знак, умноженный на синус.
- Для многомерных векторов чередовать чётные и нечётные координаты парами неудобно, поэтому сейчас реализацию
упрощают: делят координаты пополам и образуют пары «-я координата первой половины ↔ -я координата второй
половины» (рисунок img/ROPE3.png).
Операция над первой и второй половинами и есть
rotate_half.
Реализация в коде. Сначала задаём углы поворота:
class Qwen2RotaryEmbedding(nn.Module): def __init__(self, dim, max_position_embeddings=2048, base=10000, device=None): super().__init__() # начальные значения self.dim = dim self.max_position_embeddings = max_position_embeddings self.base = base # частоты поворота inv_freq = 1.0 / (self.base ** (torch.arange(0, self.dim, 2, dtype=torch.int64).float().to(device) / self.dim)) self.register_buffer("inv_freq", inv_freq, persistent=False)
# Строим кэш здесь, чтобы работал `torch.jit.trace`. self._set_cos_sin_cache( seq_len=max_position_embeddings, device=self.inv_freq.device, dtype=torch.get_default_dtype() ) # для каждого токена последовательности получаем свой уникальный набор углов (внешнее произведение) def _set_cos_sin_cache(self, seq_len, device, dtype): self.max_seq_len_cached = seq_len t = torch.arange(self.max_seq_len_cached, device=device, dtype=torch.int64).type_as(self.inv_freq)
freqs = torch.outer(t, self.inv_freq) # получаем углы (через механизм буферов создаём self.cos_cached и self.sin_cached) emb = torch.cat((freqs, freqs), dim=-1) self.register_buffer("cos_cached", emb.cos().to(dtype), persistent=False) self.register_buffer("sin_cached", emb.sin().to(dtype), persistent=False)
def forward(self, x, seq_len=None): # x: [bs, num_attention_heads, seq_len, head_size] if seq_len > self.max_seq_len_cached: self._set_cos_sin_cache(seq_len=seq_len, device=x.device, dtype=x.dtype)
return ( self.cos_cached[:seq_len].to(dtype=x.dtype), self.sin_cached[:seq_len].to(dtype=x.dtype), )Сначала вычисляем базовые углы:
где — номер пары координат, принимает значения (рисунок img/ROPE4.png).
Затем умножаем эти углы на номер каждой позиции, чтобы различать слова последовательности. Это равносильно
где — номер строки, то есть позиции (рисунок img/ROPE5.png).
emb склеивает две копии — получается размерность dim, причём значения повторяются с периодом dim/2
(рисунок img/ROPE6.png).
Когда нужно достать позиционную информацию cos и sin, из таблицы вырезается часть длиной seq: заранее задано
1024 — максимальная позиция, а при использовании берётся только текущая seq_len. Внешнее произведение выше нужно
как раз для того, чтобы у каждого слова последовательности был свой из 1024 вариантов позиционного кода
(рисунок img/ROPE7.png).
Применяем вращательное кодирование:
# Вторая половина координат меняется местами с первой, и у неё меняется знак.def rotate_half(x): """Поворачивает половину скрытых координат входа.""" x1 = x[..., : x.shape[-1] // 2] x2 = x[..., x.shape[-1] // 2 :] return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(q, k, cos, sin, position_ids, unsqueeze_dim=1): """Применяет вращательное позиционное кодирование (RoPE) к тензорам запросов и ключей.
Возвращает query и key, повёрнутые с помощью RoPE. """ cos = cos[position_ids].unsqueeze(unsqueeze_dim) sin = sin[position_ids].unsqueeze(unsqueeze_dim) q_embed = (q * cos) + (rotate_half(q) * sin) k_embed = (k * cos) + (rotate_half(k) * sin) return q_embed, k_embedСоответствующая формула (в оригинале — img/ROPE8.png), для вектора размерности на позиции , где повторяются с периодом :
Здесь индекс означает, что речь идёт об одной строке, то есть об одной позиции в seq_len; для двух элементов,
образующих пару… (на этом месте фраза в оригинале обрывается).
1.3.3.3 attention_mask: порядок чтения
Заголовок раздела «1.3.3.3 attention_mask: порядок чтения»На первом шаге модель видит только себя, на втором — позиции 0 и 1, и т. д.; всё остальное — минус бесконечность (рисунок img/Mask1.png).
После softmax веса на позициях с минус бесконечностью становятся нулевыми — так и получается чтение только слева направо (рисунок img/Mask2.png).
attnимеет форму (bs, heads, T, T),values— (bs, heads, T, hd); в итоге всё сводится к умножению матриц [30, 30] × [30, 128], где 30 — длина фразы в шагах, то есть число слов.- Каждое слово в
valueописывается 128 координатами. Для первого слова, посколькуattnнижнетреугольная, каждая координата умножается только на первый ненулевой элемент — единицу, а всё остальное умножается на 0.- Во второй строке уже два разных веса: каждая из 128 координат
valueберётся по этим двум весам из первых двух строк — так вычисляются 128 координат второго слова выхода (второго шага, второй строки). Такое взвешивание и выражает учёт связи с предыдущими словами.
- Во второй строке уже два разных веса: каждая из 128 координат
- На шаге соответственно весов: они описывают связи между шагами с 1-го по -й, и по ним вычисляется каждая координата.
- Результат каждого умножения матриц — значение координаты для следующего слова; scores по маске определяют, по каким предыдущим словам можно взвешивать значения этой координаты, чтобы получить её значение у следующего слова (рисунок img/Mask3.png).
- При инференсе запросы разной длины не проблема: все веса модели имеют размер dim × dim, а матрица attention score — seq × seq, так что веса от длины последовательности не зависят. Они относятся к координатам dim.
- При инференсе
attention_maskможно и не задавать: токены выдаются по одному, каждый новый дописывается (cat) к входу следующего шага, и каждый раз берётся последний выход — это и есть предсказание следующего токена.
1.4 Qwen2 MLP
Заголовок раздела «1.4 Qwen2 MLP»Рисунок в оригинале: img/MLP1.png.
Входной hidden_state параллельно подаётся в два слоя Linear; выход одного пропускается через функцию активации и
умножается на выход другого, а результат проходит ещё через один Linear — это и есть итоговый выход.
class Qwen2MLP(nn.Module): def __init__(self, config): super().__init__() # эти двое в пояснениях не нуждаются self.config = config self.hidden_size = config.hidden_size self.intermediate_size = config.intermediate_size
# три полносвязных слоя self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) self.act_fn = ACT2FN[config.hidden_act]
def forward(self, x): down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) return down_proj1.5 Qwen2RMSNorm
Заголовок раздела «1.5 Qwen2RMSNorm»Формула (в оригинале — рисунок img/RMSNorm_formulation.jpg):
Здесь:
- — входной
hidden_stateслоя; - — значение, относящееся к последнему измерению
hidden_state; - — число элементов в последнем измерении входа;
- — очень маленькое число, чтобы не делить на ноль.
class Qwen2RMSNorm(nn.Module): # слой нормализации def __init__(self, hidden_size, eps=1e-6): """ Qwen2RMSNorm эквивалентна T5LayerNorm """ super().__init__() self.weight = nn.Parameter(torch.ones(hidden_size)) self.variance_epsilon = eps
def forward(self, hidden_states): input_dtype = hidden_states.dtype hidden_states = hidden_states.to(torch.float32) variance = hidden_states.pow(2).mean(-1, keepdim=True) hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) return self.weight * hidden_states.to(input_dtype)torch.rsqrt— это «производная от корня из входа»..pow(2).mean(-1, keepdim=True)— возвести в квадрат и усреднить по последнему измерению.
От переводчика: что сделать руками
Заголовок раздела «От переводчика: что сделать руками»- Откройте
modeling_qwen2.pyв установленной у вас библиотекеtransformers(python -c "import transformers, os; print(os.path.dirname(transformers.__file__))", далееmodels/qwen2/modeling_qwen2.py) и найдите все пять классов из статьи. В новых версиях код отрефакторен (eager_attention_forward, класс кэша), но формулы те же. - Загрузите маленькую модель Qwen (например, Qwen2.5-0.5B-Instruct), поставьте точку останова в
Qwen2Attention.forwardи проследите формы тензоров, как делают авторы в разделе «Детали под отладчиком». - Посчитайте, во сколько раз GQA сокращает KV-кэш у Qwen2.5-0.5B: возьмите
num_attention_headsиnum_key_value_headsиз еёconfig.json. Сверьтесь с уроком 5.2.1.
Словарик
Заголовок раздела «Словарик»| Русский | Пояснение |
|---|---|
| слой эмбеддингов (embedding) | id токена → вектор |
| слой (блок) декодера (decoder layer) | внимание + MLP + нормализации |
| остаточная связь (residual) | x + f(x) |
| нормализация RMSNorm | делим на среднеквадратичное |
| вращательное позиционное кодирование (RoPE) | позиция = угол поворота и |
| внимание с групповыми запросами (GQA) | одна пара K/V на группу голов |
| KV-кэш (KV cache) | сохранённые ключи и значения прошлых токенов |
| маска внимания (attention mask) | запрещает смотреть «в будущее» |
| пересчёт активаций (gradient checkpointing) | экономия памяти за счёт времени |
| гейт, «вентиль» (gate_proj) | ветвь MLP, которая умножается на другую |