How to Expand Llama3 Embedding and LM Head Layers for Vocabulary Extension
To expand Llama3 embedding and LM head layers for a larger vocabulary, use the expand_embedding_and_lmhead.py script to resize the weight matrices, initialize new tokens by averaging their compositional sub-token embeddings, and export a compatible PyTorch checkpoint.
Expanding the vocabulary of LLaMA 3 to support additional languages like Chinese requires resizing the model's embedding and language modeling head matrices without destroying pretrained English knowledge. The crazyboym/llama3-chinese-chat repository provides a specialized script that automates this process while preserving the semantic structure of the original embedding space. This guide explains the exact steps to expand Llama3 embedding and LM head layers using the official tooling and source code.
Understanding the Embedding and LM Head Architecture
LLaMA 3 stores token representations in two critical weight matrices that must remain synchronized during vocabulary expansion.
The Token Embedding Layer
The embedding layer (model.embed_tokens.weight) holds a matrix of shape (vocab_size, d_model) that converts integer token IDs into dense vectors fed to the first Transformer block. Each row represents a unique token's vector representation in the model's hidden dimension.
The Language Model Head
The LM head (lm_head.weight) uses an identical shape (vocab_size, d_model) to project final hidden states back to vocabulary logits during generation. In LLaMA 3, this matrix is typically tied to the embedding weights, meaning both matrices must expand simultaneously to accommodate new tokens.
The Vocabulary Expansion Process
The tools/expand_embedding_and_lmhead.py script automates the resizing and initialization through a five-step pipeline.
Loading Model Shards and Weights
The script first consolidates sharded Safetensors checkpoints into a single dictionary. It reads all shards (model-{index:05d}-of-{total:05d}.safetensors) sequentially and extracts the two target matrices (model.embed_tokens.weight and lm_head.weight) from the merged state dict.
According to the source code in tools/expand_embedding_and_lmhead.py, lines 69-83 handle the shard loading and tensor extraction, supporting any number of input shards via the num_shards parameter.
Allocating Expanded Matrices
After determining the new vocabulary size from the target tokenizer, the script allocates zero-filled tensors of shape (new_vocab_size, d_model) for both layers. It then copies the existing English embedding and LM-head rows into the corresponding positions of the new matrices, leaving rows for new tokens initialized to zero.
This allocation occurs in lines 84-90 of the expansion script, preserving the original dtype (float16 or bfloat16) to avoid precision mismatches.
Initializing New Tokens via Averaging
For each new token ID, the init_embeddings_average function (lines 11-35) performs compositional initialization:
- Decodes the new token text using the new tokenizer
- Re-tokenizes that text using the old tokenizer to obtain constituent sub-token IDs
- Averages the embedding vectors (and LM-head vectors) of those original sub-tokens
- Writes the averaged vector into the new token's position
This approach ensures that a Chinese token composed of multiple English sub-pieces receives a semantically reasonable starting point derived from its components, rather than random initialization. The function is invoked at lines 92-99 after matrix allocation.
Handling Unknown Tokens
If a new token cannot be represented by the old tokenizer's vocabulary, the script falls back to the <unk> token ID (0) and prints a warning (lines 28-31), ensuring the process continues without failure.
Running the Expansion Script
Execute the vocabulary expansion from your terminal using the following command structure:
# Install dependencies
pip install torch safetensors transformers fire matplotlib
# Run expansion
python tools/expand_embedding_and_lmhead.py \
--old_tokenizer /path/to/llama3-original/tokenizer \
--new_tokenizer /path/to/chinese-extended/tokenizer \
--num_shards 4 \
--old_model /path/to/llama3/model/shards \
--new_model /path/to/output/expanded_model \
--save_embedding_plots true
The --num_shards argument must match the number of Safetensors files in your original model directory. The --save_embedding_plots option triggers the draw function (lines 37-54) to generate PNG visualizations of the first 128 dimensions, allowing visual verification that new rows contain non-zero values.
Loading and Validating the Expanded Model
After expansion, load the resulting pytorch_model.bin using standard Transformers workflows:
from transformers import AutoTokenizer, AutoModelForCausalLM
# Load new tokenizer and expanded model
tokenizer = AutoTokenizer.from_pretrained("/path/to/new/tokenizer")
model = AutoModelForCausalLM.from_pretrained(
"/path/to/output/expanded_model",
torch_dtype="auto",
device_map="auto"
)
# Test Chinese generation
prompt = "今天天气怎么样?"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Remember to manually update config.json in the output directory to reflect the new vocab_size before loading the model.
Critical Implementation Details
Tokenizer Alignment Requirements
The old and new tokenizers must share the same base vocabulary. The script relies on old_tokenizer(text)["input_ids"] to map new token strings back to existing IDs. If the base tokenizers differ fundamentally (e.g., different pre-tokenization rules), the averaging logic will produce nonsensical embeddings.
Checkpoint Format Conversion
The input uses Safetensors sharding, but the output is a single monolithic pytorch_model.bin file. If you require sharded outputs for distribution, use the companion tools/merge_weight.py utility to re-split the expanded checkpoint.
Data Type Preservation
New tensors inherit the dtype of the original weights. If your base model uses bfloat16, the expanded matrices maintain bfloat16 precision throughout the process.
Summary
- File location: The expansion logic resides in
tools/expand_embedding_and_lmhead.pywithin thecrazyboym/llama3-chinese-chatrepository. - Initialization method: New tokens are initialized by averaging embeddings of their compositional sub-tokens via the
init_embeddings_averagefunction. - Matrix targets: The script modifies
model.embed_tokens.weightandlm_head.weight, both shaped (vocab_size, d_model). - Output format: Produces a single
pytorch_model.bincheckpoint from sharded Safetensors inputs. - Post-processing: You must manually update
config.jsonto match the new vocabulary size before inference.
Frequently Asked Questions
Why initialize new embeddings by averaging existing tokens?
Averaging provides a semantically grounded starting point that respects the original embedding space. When a new Chinese token decomposes into multiple English sub-tokens (e.g., byte-pair encoding pieces), their average represents a centroid in vector space that approximates the new token's meaning while preserving the model's existing knowledge.
Can I use this script with sharded Safetensors checkpoints?
Yes. The num_shards parameter accepts any integer value, and the script sequentially loads all model-XXXXX-of-XXXXX.safetensors files into a unified dictionary before processing. The output consolidates these into a single PyTorch checkpoint.
Do I need to modify config.json after running the expansion?
Yes. The script saves the expanded weight matrices but does not automatically update the configuration file. You must manually edit config.json in the output directory to set "vocab_size": <new_size> to match your expanded tokenizer.
What happens if a new token cannot be tokenized by the old tokenizer?
The script falls back to the unknown token ID (0) and initializes that row from the <unk> token's embedding. It prints a warning to stderr indicating which tokens triggered this fallback, allowing you to audit the coverage of your vocabulary expansion.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →