mirror of
https://github.com/hanzoai/moe.git
synced 2026-10-30 18:27:47 +00:00
6.4 MiB
6.4 MiB
In [ ]:
# Import tqdm for progress bars and snapshot_download for downloading model files
from tqdm import tqdm
from huggingface_hub import snapshot_download
# Define the Hugging Face repository ID and the local directory to save the files
repo_id = "huihui-ai/Huihui-MoE-0.8B-2E"
local_dir = "Huihui-MoE-0.8B-2E"
# Download the model snapshot from Hugging Face, excluding .bin files
# This will fetch config, tokenizer, and safetensors weights only
snapshot_download(
repo_id=repo_id,
local_dir=local_dir,
ignore_patterns=["*.bin"], # Skip large .bin files, only get safetensors
tqdm_class=tqdm # Use standard tqdm for progress bar
)In [3]:
from pathlib import Path
from tokenizers import Tokenizer
import json
import torch
# This file contains the vocabulary, merge rules, and configuration.
tokenizer_path = Path("Huihui-MoE-0.8B-2E/tokenizer.json")
# The Tokenizer.from_file() method is the standard way to load tokenizers
# from the Hugging Face ecosystem.
tokenizer = Tokenizer.from_file(str(tokenizer_path))
# We can also load them from the special_tokens_map.json for confirmation
with open("Huihui-MoE-0.8B-2E/special_tokens_map.json", "r") as f:
special_tokens_map = json.load(f)
print(f"Special tokens from file: {special_tokens_map}")
# We'll follow the encode -> decode pattern to ensure it works correctly.
prompt = "The only thing I know is that I know"
# .encode() returns an Encoding object, we access the token IDs via .ids
encoded = tokenizer.encode(prompt)
print(f"\nOriginal prompt: '{prompt}'")
print(f"Encoded token IDs: {encoded.ids}")
# .decode() converts the token IDs back to a string.
decoded = tokenizer.decode(encoded.ids)
print(f"Decoded back to text: '{decoded}'")
# Step 5: Verify the vocabulary size
vocab_size = tokenizer.get_vocab_size()
print(f"\nTokenizer vocabulary size: {vocab_size}")Special tokens from file: {'additional_special_tokens': ['<|im_start|>', '<|im_end|>', '<|object_ref_start|>', '<|object_ref_end|>', '<|box_start|>', '<|box_end|>', '<|quad_start|>', '<|quad_end|>', '<|vision_start|>', '<|vision_end|>', '<|vision_pad|>', '<|image_pad|>', '<|video_pad|>'], 'eos_token': {'content': '<|im_end|>', 'lstrip': False, 'normalized': False, 'rstrip': False, 'single_word': False}, 'pad_token': {'content': '<|endoftext|>', 'lstrip': False, 'normalized': False, 'rstrip': False, 'single_word': False}}
Original prompt: 'The only thing I know is that I know'
Encoded token IDs: [785, 1172, 3166, 358, 1414, 374, 429, 358, 1414]
Decoded back to text: 'The only thing I know is that I know'
Tokenizer vocabulary size: 151669
In [4]:
# Get the vocabulary as a dictionary: {token_string: token_id}
vocab = tokenizer.get_vocab()
# Display a slice of the vocabulary for inspection (tokens 5600 to 5609)
sample_vocab_slice = list(vocab.items())[5600:5610]
sample_vocab_sliceOut [4]:
[('_PARAM', 12073),
('ĠTCHAR', 67069),
('ĠMunicip', 35703),
('Ġnguyên', 129031),
('Javascript', 64567),
('Ġric', 29950),
('ðŁİĢ', 145315),
('ë°ı', 144499),
('ĠTul', 42216),
('ä¼ļ议室', 112818)]In [5]:
# Define the path to the configuration file.
config_path = Path("Huihui-MoE-0.8B-2E/config.json")
# Open and load the JSON file into a Python dictionary.
with open(config_path, "r") as f:
config = json.load(f)
# Print the configuration to see all the parameters.
# This gives us a complete overview of the model we're about to build.
print(json.dumps(config, indent=4)){
"architectures": [
"Qwen3MoeForCausalLM"
],
"attention_bias": false,
"attention_dropout": 0.0,
"bos_token_id": 151643,
"decoder_sparse_step": 1,
"eos_token_id": 151645,
"head_dim": 128,
"hidden_act": "silu",
"hidden_size": 1024,
"initializer_range": 0.02,
"intermediate_size": 3072,
"max_position_embeddings": 40960,
"max_window_layers": 28,
"mlp_only_layers": [],
"model_type": "qwen3_moe",
"moe_intermediate_size": 3072,
"norm_topk_prob": true,
"num_attention_heads": 16,
"num_experts": 2,
"num_experts_per_tok": 1,
"num_hidden_layers": 28,
"num_key_value_heads": 8,
"output_router_logits": false,
"rms_norm_eps": 1e-06,
"rope_scaling": null,
"rope_theta": 1000000,
"router_aux_loss_coef": 0.001,
"sliding_window": null,
"tie_word_embeddings": true,
"torch_dtype": "bfloat16",
"transformers_version": "4.52.4",
"use_cache": true,
"use_sliding_window": false,
"vocab_size": 151936
}
In [ ]:
# --- Main Architecture Parameters ---
# Extract model hyperparameters from the config dictionary.
# Embedding dimension (hidden size of the model)
dim = config["hidden_size"]
# Number of transformer layers
n_layers = config["num_hidden_layers"]
# Number of attention heads
n_heads = config["num_attention_heads"]
# Number of key/value heads (for grouped-query attention)
n_kv_heads = config["num_key_value_heads"]
# Vocabulary size
vocab_size = config["vocab_size"]
# RMSNorm epsilon value for numerical stability
norm_eps = config["rms_norm_eps"]
# Rotary positional embedding theta parameter
rope_theta = torch.tensor(config["rope_theta"])
# Dimension of each attention head
head_dim = config["head_dim"] # <-- This is important for attention calculations
# --- Mixture-of-Experts (MoE) Specific Parameters ---
# Number of experts in the MoE layer
num_experts = config["num_experts"]
# Number of experts selected per token by the router
num_experts_per_tok = config["num_experts_per_tok"]
# Intermediate size of the MoE feed-forward network
moe_intermediate_size = config["moe_intermediate_size"]
# --- Print out all key architecture parameters for verification ---
print(f"Embedding Dimension (dim): {dim}")
print(f"Number of Layers (n_layers): {n_layers}")
print(f"Number of Heads (n_heads): {n_heads}")
print(f"Number of KV Heads (n_kv_heads): {n_kv_heads}")
print(f"Head Dimension (head_dim): {head_dim}") # <-- Print head_dim for clarity
print(f"Vocabulary Size (vocab_size): {vocab_size}")
print(f"RoPE Theta: {rope_theta}")
print(f"--- MoE Params ---")
print(f"Total Experts (num_experts): {num_experts}")
print(f"Experts per Token (num_experts_per_tok): {num_experts_per_tok}")
print(f"MoE Intermediate Size: {moe_intermediate_size}")Embedding Dimension (dim): 1024 Number of Layers (n_layers): 28 Number of Heads (n_heads): 16 Number of KV Heads (n_kv_heads): 8 Head Dimension (head_dim): 128 Vocabulary Size (vocab_size): 151936 RoPE Theta: 1000000 --- MoE Params --- Total Experts (num_experts): 2 Experts per Token (num_experts_per_tok): 1 MoE Intermediate Size: 3072
In [ ]:
# Import the safetensors loader
from safetensors.torch import load_file
# Define the path to the model weights file
model_weights_path = Path("Huihui-MoE-0.8B-2E/model.safetensors")
# Load the weights into a dictionary; each key is a layer/parameter name, and each value is a torch tensor
model_weights = load_file(model_weights_path)
# Inspect the loaded weights: print the first 20 layer names to confirm successful loading
print("First 20 keys in model_weights:")
print(json.dumps(list(model_weights.keys())[:20], indent=4))[
"model.embed_tokens.weight",
"model.layers.0.input_layernorm.weight",
"model.layers.0.mlp.experts.0.down_proj.weight",
"model.layers.0.mlp.experts.0.gate_proj.weight",
"model.layers.0.mlp.experts.0.up_proj.weight",
"model.layers.0.mlp.experts.1.down_proj.weight",
"model.layers.0.mlp.experts.1.gate_proj.weight",
"model.layers.0.mlp.experts.1.up_proj.weight",
"model.layers.0.mlp.gate.weight",
"model.layers.0.post_attention_layernorm.weight",
"model.layers.0.self_attn.k_norm.weight",
"model.layers.0.self_attn.k_proj.weight",
"model.layers.0.self_attn.o_proj.weight",
"model.layers.0.self_attn.q_norm.weight",
"model.layers.0.self_attn.q_proj.weight",
"model.layers.0.self_attn.v_proj.weight",
"model.layers.1.input_layernorm.weight",
"model.layers.1.mlp.experts.0.down_proj.weight",
"model.layers.1.mlp.experts.0.gate_proj.weight",
"model.layers.1.mlp.experts.0.up_proj.weight"
]
In [ ]:
import torch.nn as nn
# Get special token IDs using the tokenizer
im_start_id = tokenizer.token_to_id("<|im_start|>")
im_end_id = tokenizer.token_to_id("<|im_end|>")
newline_id = tokenizer.encode("\n").ids[0] # ID for newline character
# Get token IDs for 'user' and 'assistant'
user_ids = tokenizer.encode("user").ids
assistant_ids = tokenizer.encode("assistant").ids
# Print token IDs for verification
print(f"ID for '<|im_start|>': {im_start_id}")
print(f"ID for '<|im_end|>': {im_end_id}")
print(f"ID for '\\n': {newline_id}")
print(f"IDs for 'user': {user_ids}")
print(f"IDs for 'assistant': {assistant_ids}\n")
# Encode the main prompt
prompt_ids = tokenizer.encode(prompt).ids
# Build the chat template token sequence
# Template: <|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n
prefix_ids = [im_start_id] + user_ids + [newline_id]
suffix_ids = [im_end_id, newline_id, im_start_id] + assistant_ids + [newline_id]
tokens_list = prefix_ids + prompt_ids + suffix_ids
tokens = torch.tensor(tokens_list)
print(f"Final combined token IDs: {tokens}")
print(f"Number of tokens: {len(tokens)}")
# Decode each token individually for inspection
prompt_split_as_tokens = [tokenizer.decode([token.item()]) for token in tokens]
print(f"\nPrompt split into tokens: {prompt_split_as_tokens}")ID for '<|im_start|>': 151644
ID for '<|im_end|>': 151645
ID for '\n': 198
IDs for 'user': [872]
IDs for 'assistant': [77091]
Final combined token IDs: tensor([151644, 872, 198, 785, 1172, 3166, 358, 1414, 374,
429, 358, 1414, 151645, 198, 151644, 77091, 198])
Number of tokens: 17
Prompt split into tokens: ['', 'user', '\n', 'The', ' only', ' thing', ' I', ' know', ' is', ' that', ' I', ' know', '', '\n', '', 'assistant', '\n']
In [7]:
dimOut [7]:
1024
In [8]:
import torch.nn as nn
embedding_layer = nn.Embedding(vocab_size, dim)
embedding_layer.weight.data.copy_(model_weights["model.embed_tokens.weight"])
token_embeddings_unnormalized = embedding_layer(tokens).to(torch.bfloat16)
print("\nShape of the token embeddings:", token_embeddings_unnormalized.shape)Shape of the token embeddings: torch.Size([17, 1024])
In [9]:
# First, let's define the RMSNorm function.
# This function will be reused throughout the model.
def rms_norm(tensor, norm_weights):
"""
Apply Root Mean Square Normalization.
The formula is: (tensor / sqrt(mean(tensor^2) + epsilon)) * weights
Using rsqrt (reciprocal square root) is a common optimization.
"""
# Save the original data type of the input tensor
input_dtype = tensor.dtype
# Perform the calculation in float32 for numerical stability
tensor_float = tensor.to(torch.float32)
variance = tensor_float.pow(2).mean(-1, keepdim=True)
normalized_tensor = tensor_float * torch.rsqrt(variance + norm_eps)
# Apply the weights and cast the result back to the original data type
return (normalized_tensor * norm_weights).to(input_dtype)In [10]:
# --- Building the first part of the first Transformer layer ---
# Step 1: Normalize the token embeddings.
# This is the "input_layernorm" for the first attention block. We retrieve
# its specific weights from our loaded model_weights dictionary.
token_embeddings_normalized = rms_norm(token_embeddings_unnormalized, model_weights["model.layers.0.input_layernorm.weight"])
# Step 2: Verify the shape.
# Normalization doesn't change the shape of the tensor, only the values within it.
# The shape should still be [17, 1024].
print("Shape of the normalized token embeddings:", token_embeddings_normalized.shape)Shape of the normalized token embeddings: torch.Size([17, 1024])
In [11]:
# --- Unwrapping the Query weights ---
# Shape: [n_heads * head_dim, dim] -> [16 * 128, 1024] -> [2048, 1024]
# New Shape: [n_heads, head_dim, dim] -> [16, 128, 1024]
q_layer0 = model_weights["model.layers.0.self_attn.q_proj.weight"]
q_layer0 = q_layer0.view(n_heads, head_dim, dim)
# --- Unwrapping the Key and Value weights ---
# Shape: [n_kv_heads * head_dim, dim] -> [8 * 128, 1024] -> [1024, 1024]
# New Shape: [n_kv_heads, head_dim, dim] -> [8, 128, 1024]
k_layer0 = model_weights["model.layers.0.self_attn.k_proj.weight"]
k_layer0 = k_layer0.view(n_kv_heads, head_dim, dim)
v_layer0 = model_weights["model.layers.0.self_attn.v_proj.weight"]
v_layer0 = v_layer0.view(n_kv_heads, head_dim, dim)
print(f"Reshaped Query weights shape: {q_layer0.shape}")
print(f"Reshaped Key weights shape: {k_layer0.shape}")
print(f"Reshaped Value weights shape: {v_layer0.shape}")Reshaped Query weights shape: torch.Size([16, 128, 1024]) Reshaped Key weights shape: torch.Size([8, 128, 1024]) Reshaped Value weights shape: torch.Size([8, 128, 1024])
In [12]:
# --- Calculating Q, K, and V for the first head of the first layer ---
# Step 1: Get the weight matrix for the first head (head 0).
# Shape of each is [head_dim, dim] -> [128, 1024]
q_layer0_head0 = q_layer0[0]
k_layer0_head0 = k_layer0[0] # For the first head, we use the first KV head
v_layer0_head0 = v_layer0[0]
# Step 2: Calculate the Query, Key, and Value vectors for each token.
# We matrix-multiply our normalized embeddings with the head's weight matrices.
# Input: [17, 1024] @ [1024, 128] -> Output: [17, 128]
q_per_token = torch.matmul(token_embeddings_normalized, q_layer0_head0.T)
k_per_token = torch.matmul(token_embeddings_normalized, k_layer0_head0.T)
v_per_token = torch.matmul(token_embeddings_normalized, v_layer0_head0.T)
# --- Step 3: Verify the shapes ---
# We now have a 128-dimensional Q, K, and V vector for each of our 17 input tokens.
print("\n--- Output for Head 0 ---")
print("Shape of Query vectors per token:", q_per_token.shape)
print("Shape of Key vectors per token:", k_per_token.shape)
print("Shape of Value vectors per token:", v_per_token.shape)--- Output for Head 0 --- Shape of Query vectors per token: torch.Size([17, 128]) Shape of Key vectors per token: torch.Size([17, 128]) Shape of Value vectors per token: torch.Size([17, 128])
In [13]:
import matplotlib.pyplot as plt
# --- Step 1: Pre-compute the RoPE frequencies ---
# We will pre-compute the rotation matrices (in complex number form) for all positions.
# The `max_position_embeddings` from the config tells us the maximum sequence length.
max_seq_len = config["max_position_embeddings"]
# Calculate the frequencies (theta_i) for each of the 64 pairs.
# The formula is theta_i = 1.0 / (rope_theta^(2i / head_dim))
freqs = 1.0 / (rope_theta ** (torch.arange(0, head_dim, 2) / head_dim))
# Create a tensor of all possible positions [0, 1, ..., max_seq_len-1]
t = torch.arange(max_seq_len)
# Create the full frequency matrix by taking the outer product of positions and frequencies.
# Shape: [max_seq_len, head_dim / 2] -> [40960, 64]
freqs_for_each_token = torch.outer(t, freqs)
# Convert this to a matrix of complex numbers using Euler's formula: e^(i*theta) = cos(theta) + i*sin(theta).
# torch.polar is perfect for this. The magnitude is 1, the angle is the frequency.
# Shape: [max_seq_len, head_dim / 2] -> [40960, 64]
freqs_cis = torch.polar(torch.ones_like(freqs_for_each_token), freqs_for_each_token)
# --- Step 2: Visualize the rotations for one position (optional but insightful) ---
# This shows how the rotation angle changes for each of the 64 pairs for a single token.
value = freqs_cis[3] # Let's look at the rotations for the 4th token (index 3)
plt.figure()
for i, element in enumerate(value):
plt.plot([0, element.real], [0, element.imag], color='blue', linewidth=1)
plt.annotate(f"{i}", xy=(element.real, element.imag), color='red')
plt.xlabel('Real')
plt.ylabel('Imaginary')
plt.title('RoPE Rotations for each 2D-pair at a single token position')
plt.grid(True)
plt.show()In [14]:
# --- Step 3: Apply RoPE to Query and Key vectors of Head 0 ---
# Take the Q vectors for our 17 tokens and view them as 64 pairs of real numbers.
# Shape: [17, 128] -> [17, 64, 2]
q_per_token_split_into_pairs = q_per_token.float().view(q_per_token.shape[0], -1, 2)
# Now, view these pairs as complex numbers.
# Shape: [17, 64, 2] -> [17, 64]
q_per_token_as_complex_numbers = torch.view_as_complex(q_per_token_split_into_pairs)
# Get the rotation frequencies for just our 17 token positions.
# Shape of freqs_cis is [40960, 64], we slice the first 17 rows.
freqs_cis_for_tokens = freqs_cis[:len(tokens)]
# Apply the rotation by performing element-wise complex number multiplication.
# Shape: [17, 64] * [17, 64] -> [17, 64]
q_per_token_as_complex_numbers_rotated = q_per_token_as_complex_numbers * freqs_cis_for_tokens
# Convert the rotated complex numbers back into pairs of real numbers.
# Shape: [17, 64] -> [17, 64, 2]
q_per_token_split_into_pairs_rotated = torch.view_as_real(q_per_token_as_complex_numbers_rotated)
# Finally, merge the pairs back into our original vector shape.
# Shape: [17, 64, 2] -> [17, 128]
q_per_token_rotated = q_per_token_split_into_pairs_rotated.view(q_per_token.shape)
# --- Repeat the exact same process for the Key vectors ---
k_per_token_split_into_pairs = k_per_token.float().view(k_per_token.shape[0], -1, 2)
k_per_token_as_complex_numbers = torch.view_as_complex(k_per_token_split_into_pairs)
k_per_token_as_complex_numbers_rotated = k_per_token_as_complex_numbers * freqs_cis_for_tokens
k_per_token_split_into_pairs_rotated = torch.view_as_real(k_per_token_as_complex_numbers_rotated)
k_per_token_rotated = k_per_token_split_into_pairs_rotated.view(k_per_token.shape)
# --- Step 4: Verify the shapes ---
# The shapes are unchanged, but the vectors now contain positional information.
print("\n--- Output for Head 0 after RoPE ---")
print("Shape of rotated Query vectors:", q_per_token_rotated.shape)
print("Shape of rotated Key vectors:", k_per_token_rotated.shape)--- Output for Head 0 after RoPE --- Shape of rotated Query vectors: torch.Size([17, 128]) Shape of rotated Key vectors: torch.Size([17, 128])
In [ ]:
import matplotlib.pyplot as plt
# Calculate the raw attention scores by multiplying rotated Q and K vectors
qk_per_token = torch.matmul(q_per_token_rotated, k_per_token_rotated.T)
# Scale the attention scores by the square root of the head dimension
qk_per_token_scaled = qk_per_token / (head_dim**0.5)
# Visualize the raw attention scores before masking
print("--- Raw Attention Scores (Before Masking) ---")
def display_qk_heatmap(qk_matrix, title="Attention Heatmap"):
_, ax = plt.subplots()
im = ax.imshow(qk_matrix.to(torch.float32).detach(), cmap='viridis')
ax.set_xticks(range(len(prompt_split_as_tokens)))
ax.set_yticks(range(len(prompt_split_as_tokens)))
ax.set_xticklabels(prompt_split_as_tokens, rotation=90)
ax.set_yticklabels(prompt_split_as_tokens)
ax.figure.colorbar(im, ax=ax)
plt.title(title)
plt.show()
display_qk_heatmap(qk_per_token_scaled, title="Raw Attention Scores (Before Masking)")--- Raw Attention Scores (Before Masking) ---
In [16]:
# --- Step 1: Create the causal mask ---
mask = torch.full((len(tokens), len(tokens)), float("-inf"))
mask = torch.triu(mask, diagonal=1)
# --- Step 2: Apply the mask to the scaled scores ---
qk_per_token_masked = qk_per_token_scaled + mask
# --- Step 3: Visualize the masked scores ---
print("\n--- Attention Scores After Causal Masking ---")
display_qk_heatmap(qk_per_token_masked, title="Attention Scores After Masking")--- Attention Scores After Causal Masking ---
In [17]:
maskOut [17]:
tensor([[0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf, -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., -inf],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])In [18]:
# --- Step 1: Apply softmax ---
qk_per_token_after_masking_after_softmax = torch.nn.functional.softmax(qk_per_token_masked.float(), dim=1).to(torch.bfloat16)
# --- Step 2: Visualize the final attention weights ---
print("\n--- Final Attention Weights (After Softmax) ---")
display_qk_heatmap(qk_per_token_after_masking_after_softmax, title="Final Attention Weights")
# --- Step 3: Verify the shape ---
print("Shape of the final attention weights matrix:", qk_per_token_after_masking_after_softmax.shape)--- Final Attention Weights (After Softmax) ---
Shape of the final attention weights matrix: torch.Size([17, 17])
In [19]:
# --- Step 1: Calculate the output of the attention head ---
# Matrix multiply the final attention weights with the Value vectors.
# Input: [17, 17] @ [17, 128] -> Output: [17, 128]
qkv_attention = torch.matmul(qk_per_token_after_masking_after_softmax, v_per_token)
# --- Step 2: Verify the shape ---
# The output tensor has the same shape as the Q, K, and V vectors. Each of the 17
# tokens now has a new 128-dimensional vector that contains context from other tokens.
print("Shape of the final attention output for Head 0:", qkv_attention.shape)Shape of the final attention output for Head 0: torch.Size([17, 128])
In [20]:
# Create an empty list to store the attention output of each head.
qkv_attention_store = []
# Iterate over each of the 16 attention heads.
for head in range(n_heads):
# Get the Q, K, and V weights for the current head.
# Note the use of `head // 4` for K and V due to Grouped-Query Attention.
# Every 4 query heads share the same key and value heads.
q_layer0_head = q_layer0[head]
k_layer0_head = k_layer0[head // (n_heads // n_kv_heads)]
v_layer0_head = v_layer0[head // (n_heads // n_kv_heads)]
# Project the normalized embeddings into Q, K, and V vectors for this head.
q_per_token = torch.matmul(token_embeddings_normalized, q_layer0_head.T)
k_per_token = torch.matmul(token_embeddings_normalized, k_layer0_head.T)
v_per_token = torch.matmul(token_embeddings_normalized, v_layer0_head.T)
# Apply RoPE to the Q and K vectors for this head.
q_per_token_split_into_pairs = q_per_token.float().view(q_per_token.shape[0], -1, 2)
q_per_token_as_complex_numbers = torch.view_as_complex(q_per_token_split_into_pairs)
q_per_token_as_complex_numbers_rotated = q_per_token_as_complex_numbers * freqs_cis_for_tokens
q_per_token_split_into_pairs_rotated = torch.view_as_real(q_per_token_as_complex_numbers_rotated)
q_per_token_rotated = q_per_token_split_into_pairs_rotated.view(q_per_token.shape)
k_per_token_split_into_pairs = k_per_token.float().view(k_per_token.shape[0], -1, 2)
k_per_token_as_complex_numbers = torch.view_as_complex(k_per_token_split_into_pairs)
k_per_token_as_complex_numbers_rotated = k_per_token_as_complex_numbers * freqs_cis_for_tokens
k_per_token_split_into_pairs_rotated = torch.view_as_real(k_per_token_as_complex_numbers_rotated)
k_per_token_rotated = k_per_token_split_into_pairs_rotated.view(k_per_token.shape)
# Calculate and scale the attention scores.
qk_per_token = torch.matmul(q_per_token_rotated, k_per_token_rotated.T) / (head_dim**0.5)
# Apply the causal mask.
qk_per_token_masked = qk_per_token + mask
# Apply softmax to get the attention weights.
qk_per_token_after_masking_after_softmax = torch.nn.functional.softmax(qk_per_token_masked.float(), dim=1).to(torch.bfloat16)
# Aggregate the Value vectors using the attention weights.
qkv_attention = torch.matmul(qk_per_token_after_masking_after_softmax, v_per_token)
# Append the result of this head to our list.
qkv_attention_store.append(qkv_attention)
# --- Verify the result ---
# We should have a list containing 16 tensors, each of shape [17, 128].
print("Number of heads processed:", len(qkv_attention_store))
print("Shape of the output for a single head:", qkv_attention_store[0].shape)Number of heads processed: 16 Shape of the output for a single head: torch.Size([17, 128])
In [21]:
# --- Step 1: Concatenate the outputs of all heads ---
# We combine the 16 tensors of shape [17, 128] along the last dimension.
# The result is a single tensor of shape [17, 16 * 128], which is [17, 2048].
stacked_qkv_attention = torch.cat(qkv_attention_store, dim=-1)
# --- Step 2: Pass through the output projection layer ---
# This is a final linear layer that mixes the information from all heads.
# Its weight matrix 'o_proj.weight' has a shape of [dim, dim] -> [1024, 2048]
# which is incorrect for this model. Qwen3's o_proj is [dim, n_heads*head_dim] -> [1024, 2048]
# but since hidden_size (dim) is 1024, the effective output dimension should match.
# Let's check the weight shape.
w_layer0 = model_weights["model.layers.0.self_attn.o_proj.weight"]
# The expected shape is [dim, n_heads * head_dim] -> [1024, 2048] for this specific model,
# which has a different hidden size vs. attention output size.
# Actually, the attention output size IS the hidden size. n_heads*head_dim = 16*128 = 2048.
# The `o_proj` weight must project this 2048-dim vector back to the model's `dim` of 1024.
# So, the weight shape must be [dim, n_heads * head_dim] -> [1024, 2048].
# Let's re-verify this logic.
# Ah, I see a mismatch in my previous comments vs. the config.
# `hidden_size` is 1024, `n_heads` is 16, `head_dim` is 128.
# Total attention output dimension = 16 * 128 = 2048.
# This 2048-dim vector must be projected back down to `hidden_size` (1024).
# So `w_layer0` should be [1024, 2048]. Let's check.
print("Shape of the output projection weights (o_proj):", w_layer0.shape)
# Perform the matrix multiplication.
# Input: [17, 2048] @ [2048, 1024] -> Output: [17, 1024]
embedding_delta = torch.matmul(stacked_qkv_attention, w_layer0.T)
# --- Step 3: Verify the shape ---
# The output is now back to the model's main embedding dimension.
# This `embedding_delta` is the final result of the entire attention block.
print("\nShape of the final attention output (embedding_delta):", embedding_delta.shape)Shape of the output projection weights (o_proj): torch.Size([1024, 2048]) Shape of the final attention output (embedding_delta): torch.Size([17, 1024])
In [22]:
# --- Step 1: Add the attention output to the original embeddings ---
# We add the output of the attention block back to the input of the layer.
# This creates the output of the first sub-layer.
embedding_after_attention = token_embeddings_unnormalized + embedding_delta
# --- Step 2: Verify the shape ---
# The shape remains the same: [17, 1024].
print("Shape of the embeddings after the first residual connection:", embedding_after_attention.shape)Shape of the embeddings after the first residual connection: torch.Size([17, 1024])
In [23]:
# --- Step 1: Apply RMSNorm before the MoE block ---
# This is the 'post_attention_layernorm' in the model's weight names.
embedding_after_attention_normalized = rms_norm(embedding_after_attention, model_weights["model.layers.0.post_attention_layernorm.weight"])
# --- Step 2: Verify the shape ---
print("Shape after MoE input normalization:", embedding_after_attention_normalized.shape)Shape after MoE input normalization: torch.Size([17, 1024])
In [24]:
# --- Step 2: The MoE Router ---
# The router is a simple linear layer that determines which expert to send each token to.
# It projects our [17, 1024] tensor to a [17, num_experts] tensor of scores (logits).
gate = model_weights["model.layers.0.mlp.gate.weight"]
router_logits = torch.matmul(embedding_after_attention_normalized, gate.T)
# We apply softmax to the logits to get probabilities, and then find the expert with the
# highest probability for each token.
routing_weights = torch.nn.functional.softmax(router_logits.float(), dim=1).to(torch.bfloat16)
routing_expert_indices = torch.argmax(routing_weights, dim=1)
print("Router logits shape:", router_logits.shape)
print("Expert chosen for each of the 17 tokens:", routing_expert_indices)
# --- Step 3: The Expert Layers ---
# Each expert is a SwiGLU-style Feed-Forward Network, similar to Llama 3's FFN,
# but smaller. We'll load the weights for both experts.
expert0_w1 = model_weights["model.layers.0.mlp.experts.0.gate_proj.weight"]
expert0_w2 = model_weights["model.layers.0.mlp.experts.0.down_proj.weight"]
expert0_w3 = model_weights["model.layers.0.mlp.experts.0.up_proj.weight"]
expert1_w1 = model_weights["model.layers.0.mlp.experts.1.gate_proj.weight"]
expert1_w2 = model_weights["model.layers.0.mlp.experts.1.down_proj.weight"]
expert1_w3 = model_weights["model.layers.0.mlp.experts.1.up_proj.weight"]
# --- Step 4: Process tokens with their chosen expert ---
# We'll create a final output tensor and iterate through each token, sending it
# to its assigned expert.
final_expert_output = torch.zeros_like(embedding_after_attention_normalized)
for i, token_embedding in enumerate(embedding_after_attention_normalized):
chosen_expert_index = routing_expert_indices[i]
# Get the weights for the chosen expert
if chosen_expert_index == 0:
w1, w2, w3 = expert0_w1, expert0_w2, expert0_w3
else:
w1, w2, w3 = expert1_w1, expert1_w2, expert1_w3
# Apply the SwiGLU activation for this token's chosen expert
# This is the same FFN logic as in the Llama 3 notebook.
silu_output = torch.nn.functional.silu(torch.matmul(token_embedding, w1.T))
gated_output = silu_output * torch.matmul(token_embedding, w3.T)
expert_output = torch.matmul(gated_output, w2.T)
# Weight the expert's output by its routing probability
final_expert_output[i] = expert_output * routing_weights[i, chosen_expert_index]
# --- Step 5: Verify the shape ---
# The output of the MoE block has the same shape as its input.
print("\nShape of the final MoE output:", final_expert_output.shape)Router logits shape: torch.Size([17, 2]) Expert chosen for each of the 17 tokens: tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Shape of the final MoE output: torch.Size([17, 1024])
In [25]:
# --- Step 1: Add the MoE output to the output from the attention block ---
# This is the second and final residual connection in the transformer block.
layer_0_embedding = embedding_after_attention + final_expert_output
# --- Step 2: Verify the shape ---
# The shape is still [17, 1024]. This tensor is now the final output of the entire first layer
# and will serve as the input to the second layer.
print("Shape of the final output for Layer 0:", layer_0_embedding.shape)Shape of the final output for Layer 0: torch.Size([17, 1024])
In [26]:
# The final embedding starts as the output from the token embedding layer.
# We will update this tensor in-place as it passes through the layers.
final_embedding = token_embeddings_unnormalized
# Loop through each of the 28 layers of the transformer.
for layer in range(n_layers):
# --- Attention Sub-Layer ---
# 1. RMS Normalization before attention
attention_input = rms_norm(final_embedding, model_weights[f"model.layers.{layer}.input_layernorm.weight"])
# 2. Multi-Head Attention
q_layer = model_weights[f"model.layers.{layer}.self_attn.q_proj.weight"].view(n_heads, head_dim, dim)
k_layer = model_weights[f"model.layers.{layer}.self_attn.k_proj.weight"].view(n_kv_heads, head_dim, dim)
v_layer = model_weights[f"model.layers.{layer}.self_attn.v_proj.weight"].view(n_kv_heads, head_dim, dim)
w_layer = model_weights[f"model.layers.{layer}.self_attn.o_proj.weight"]
qkv_attention_store = []
for head in range(n_heads):
q_layer_head = q_layer[head]
k_layer_head = k_layer[head // (n_heads // n_kv_heads)]
v_layer_head = v_layer[head // (n_heads // n_kv_heads)]
q_per_token = torch.matmul(attention_input, q_layer_head.T)
k_per_token = torch.matmul(attention_input, k_layer_head.T)
v_per_token = torch.matmul(attention_input, v_layer_head.T)
q_per_token_rotated = torch.view_as_real(torch.view_as_complex(q_per_token.float().view(q_per_token.shape[0], -1, 2)) * freqs_cis_for_tokens).view(q_per_token.shape)
k_per_token_rotated = torch.view_as_real(torch.view_as_complex(k_per_token.float().view(k_per_token.shape[0], -1, 2)) * freqs_cis_for_tokens).view(k_per_token.shape)
qk_per_token = torch.matmul(q_per_token_rotated, k_per_token_rotated.T) / (head_dim**0.5)
qk_per_token_masked = qk_per_token + mask
qk_per_token_after_masking_after_softmax = torch.nn.functional.softmax(qk_per_token_masked.float(), dim=1).to(torch.bfloat16)
qkv_attention = torch.matmul(qk_per_token_after_masking_after_softmax, v_per_token)
qkv_attention_store.append(qkv_attention)
stacked_qkv_attention = torch.cat(qkv_attention_store, dim=-1)
embedding_delta = torch.matmul(stacked_qkv_attention, w_layer.T)
# 3. First Residual Connection
embedding_after_attention = final_embedding + embedding_delta
# --- Mixture-of-Experts Sub-Layer ---
# 1. RMS Normalization before MoE
moe_input = rms_norm(embedding_after_attention, model_weights[f"model.layers.{layer}.post_attention_layernorm.weight"])
# 2. Router
gate = model_weights[f"model.layers.{layer}.mlp.gate.weight"]
router_logits = torch.matmul(moe_input, gate.T)
routing_weights = torch.nn.functional.softmax(router_logits.float(), dim=1).to(torch.bfloat16)
routing_expert_indices = torch.argmax(routing_weights, dim=1)
# 3. Expert Layers
final_expert_output = torch.zeros_like(moe_input)
expert0_w1 = model_weights[f"model.layers.{layer}.mlp.experts.0.gate_proj.weight"]
expert0_w2 = model_weights[f"model.layers.{layer}.mlp.experts.0.down_proj.weight"]
expert0_w3 = model_weights[f"model.layers.{layer}.mlp.experts.0.up_proj.weight"]
expert1_w1 = model_weights[f"model.layers.{layer}.mlp.experts.1.gate_proj.weight"]
expert1_w2 = model_weights[f"model.layers.{layer}.mlp.experts.1.down_proj.weight"]
expert1_w3 = model_weights[f"model.layers.{layer}.mlp.experts.1.up_proj.weight"]
for i, token_embedding in enumerate(moe_input):
chosen_expert_index = routing_expert_indices[i]
if chosen_expert_index == 0:
w1, w2, w3 = expert0_w1, expert0_w2, expert0_w3
else:
w1, w2, w3 = expert1_w1, expert1_w2, expert1_w3
silu_output = torch.nn.functional.silu(torch.matmul(token_embedding, w1.T))
gated_output = silu_output * torch.matmul(token_embedding, w3.T)
expert_output = torch.matmul(gated_output, w2.T)
final_expert_output[i] = expert_output * routing_weights[i, chosen_expert_index]
# 4. Second Residual Connection
final_embedding = embedding_after_attention + final_expert_output
# --- Verify the final shape ---
print("Shape of the final embeddings after all layers:", final_embedding.shape)Shape of the final embeddings after all layers: torch.Size([17, 1024])
In [27]:
# --- Step 1: Apply the final RMS Normalization ---
# This uses the 'model.norm.weight' tensor.
final_embedding_normalized = rms_norm(final_embedding, model_weights["model.norm.weight"])
# --- Step 2: Get the weights for the LM Head ---
# Due to weight tying, the LM head weights are the same as the token embedding weights.
lm_head_weights = model_weights["model.embed_tokens.weight"]
# --- Step 3: Select the embedding for the last token ---
# To predict the next token, we only need the output corresponding to the
# last token of our input sequence (which was 'assistant\n').
last_token_embedding = final_embedding_normalized[-1]
# --- Step 4: Calculate the Logits ---
# Perform the final matrix multiplication to get the scores for each word in the vocabulary.
# Input: [1024] @ [151936, 1024].T -> Output: [151936]
logits = torch.matmul(last_token_embedding, lm_head_weights.T)
# --- Step 5: Verify the shapes ---
print("Shape of the final normalized embeddings:", final_embedding_normalized.shape)
print("Shape of the last token's embedding:", last_token_embedding.shape)
print("Shape of the final logits:", logits.shape)Shape of the final normalized embeddings: torch.Size([17, 1024]) Shape of the last token's embedding: torch.Size([1024]) Shape of the final logits: torch.Size([151936])
In [28]:
# --- Step 1: Find the token with the highest logit score ---
# torch.argmax() finds the index of the maximum value in the logits tensor.
# This index is the predicted token ID.
next_token_id = torch.argmax(logits, dim=-1)
print(f"Predicted Token ID: {next_token_id.item()}")
# --- Step 2: Decode the token ID to get the word ---
# We use our tokenizer's .decode() method to turn the ID back into text.
predicted_word = tokenizer.decode([next_token_id.item()])
print(f"\nPredicted Word: '{predicted_word}'")Predicted Token ID: 64088 Predicted Word: 'stem'




