LLM大语言模型发展到今天,已经形成很多成熟的产业链。但绝大部分开发者是有没有算力条件去DIY一个完整的小模型来理解整个LLM的生命周期和训练过程的。现在以Tiny9为例帮助理解整个模型从数据、预训练到最终运行的简单过程示意,帮助理解LLM的运行机制。
基础设施与环境准备 在开始之前,我们需要搭建一个标准的 GPU 训练环境。
以Tiny9模型为例,因为参数足够小。
硬件假设 :4张 NVIDIA A100 (80GB) 或 8张 RTX 4090 (24GB)。
软件依赖 :
1 2 3 4 5 6 7 8 9 conda create -n tiny9 python=3.10 conda activate tiny9 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers==4.38.0 datasets==2.17.0 accelerate==0.27.0 pip install trl==0.7.10 deepspeed==0.13.1 peft==0.8.0 pip install vllm==0.3.3 autoawq==0.2.0
一、 阶段一:数据工程(Data Engineering) 1. 训练专属 Tokenizer Tiny9 需要自己的分词器。我们使用 tokenizers 库基于 BPE 算法训练。
**实施步骤与代码 (train_tokenizer.py)**:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from tokenizers import Tokenizer, models, pre_tokenizers, trainersfrom datasets import load_datasetdataset = load_dataset("text" , data_files={"train" : "data/corpus.txt" }) tokenizer = Tokenizer(models.BPE(unk_token="<unk>" )) tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False ) trainer = trainers.BpeTrainer( vocab_size=32000 , special_tokens=["<pad>" , "<unk>" , "<bos>" , "<eos>" ], min_frequency=2 , limit_alphabet=6000 ) tokenizer.train_from_iterator(dataset["train" ]["text" ], trainer=trainer) tokenizer.save("tiny9_tokenizer.json" ) print ("Tokenizer 训练完成!" )
2. 数据打包 (Sequence Packing) 痛点 :如果直接把短文本喂给模型,大量的 <pad> 会浪费算力。方案 :将多条文本拼接,用 <eos> 隔开,并重新生成 Labels ,确保模型只在真实 token 上计算 Loss。
graph LR
A[Text 1] -->|加 eos| B[Text 1 ]
C[Text 2] -->|加 eos| D[Text 2 ]
B --> E[拼接]
D --> E
E --> F[截断/填充至 2048]
F --> G[生成 Labels: 右移一位] **核心逻辑代码 (data_packing.py)**:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def packing_examples (examples, max_seq_length=2048 , eos_token_id=3 ): """将多条短文本打包成固定长度的序列""" packed_input_ids, packed_attention_mask, packed_labels = [], [], [] current_ids, current_labels = [], [] for text in examples["text" ]: encoded = tokenizer(text, add_special_tokens=False ) input_ids = encoded["input_ids" ] + [eos_token_id] if len (current_ids) + len (input_ids) > max_seq_length: remain_length = max_seq_length - len (current_ids) current_ids.extend(input_ids[:remain_length]) current_labels = current_ids[1 :] + [-100 ] packed_input_ids.append(current_ids) packed_labels.append(current_labels) packed_attention_mask.append([1 ] * max_seq_length) current_ids = input_ids[remain_length:] else : current_ids.extend(input_ids) return { "input_ids" : packed_input_ids, "attention_mask" : packed_attention_mask, "labels" : packed_labels }
二、 阶段二:模型架构与预训练 (Pre-training) 1. 核心代码:实现 Tiny9 的 MoE 层 这是理解 MoE 的关键。我们将标准的 FFN 替换为 MoE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import torchimport torch.nn as nnimport torch.nn.functional as Fclass Expert (nn.Module): """单个专家网络 (简单的 MLP)""" def __init__ (self, dim, hidden_dim ): super ().__init__() self.w1 = nn.Linear(dim, hidden_dim, bias=False ) self.w2 = nn.Linear(hidden_dim, dim, bias=False ) self.act = nn.SiLU() def forward (self, x ): return self.w2(self.act(self.w1(x))) class MoELayer (nn.Module): """混合专家层 (MoE)""" def __init__ (self, dim, hidden_dim, num_experts=8 , top_k=2 ): super ().__init__() self.num_experts = num_experts self.top_k = top_k self.experts = nn.ModuleList([Expert(dim, hidden_dim) for _ in range (num_experts)]) self.gate = nn.Linear(dim, num_experts, bias=False ) self.aux_loss_coef = 0.01 def forward (self, x ): batch_size, seq_len, dim = x.shape x_flat = x.view(-1 , dim) router_logits = self.gate(x_flat) routing_probs = F.softmax(router_logits, dim=-1 ) top_k_probs, top_k_indices = torch.topk(routing_probs, self.top_k, dim=-1 ) top_k_probs = top_k_probs / top_k_probs.sum (dim=-1 , keepdim=True ) output = torch.zeros_like(x_flat) for i in range (self.top_k): expert_idx = top_k_indices[:, i] prob = top_k_probs[:, i].unsqueeze(-1 ) for exp_id in range (self.num_experts): mask = (expert_idx == exp_id) if mask.any (): token_for_expert = x_flat[mask] expert_output = self.experts[exp_id](token_for_expert) output[mask] += prob[mask] * expert_output aux_loss = self._compute_aux_loss(routing_probs, top_k_indices) return output.view(batch_size, seq_len, dim), aux_loss def _compute_aux_loss (self, routing_probs, top_k_indices ): mean_routing_prob = routing_probs.mean(dim=0 ) expert_mask = F.one_hot(top_k_indices, self.num_experts).float () mean_expert_freq = expert_mask.mean(dim=0 ).sum (dim=0 ) / self.top_k return (mean_routing_prob * mean_expert_freq).sum () * self.num_experts * self.aux_loss_coef
2. 预训练实施步骤与 DeepSpeed 配置 预训练需要分布式。我们使用 DeepSpeed ZeRO-3 来切分模型状态。
ds_config_zero3.json :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "zero_optimization" : { "stage" : 3 , "offload_optimizer" : { "device" : "cpu" , "pin_memory" : true } , "offload_param" : { "device" : "cpu" , "pin_memory" : true } , "overlap_comm" : true , "contiguous_gradients" : true , "sub_group_size" : 1e9 , "reduce_bucket_size" : "auto" } , "fp16" : { "enabled" : "auto" , "loss_scale" : 0 , "initial_scale_power" : 16 } , "bf16" : { "enabled" : "auto" } , "gradient_accumulation_steps" : "auto" , "gradient_clipping" : 1.0 , "train_batch_size" : "auto" , "train_micro_batch_size_per_gpu" : "auto" }
训练启动命令 (train_pretrain.sh) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 accelerate launch --num_processes 4 --multi_gpu --mixed_precision bf16 \ --main_process_port 29500 \ run_pretrain.py \ --model_name_or_path ./tiny9_config \ --dataset_name ./packed_data \ --output_dir ./checkpoints/tiny9-pt \ --deepspeed ds_config_zero3.json \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --learning_rate 3e-4 \ --max_steps 100000 \ --save_steps 2000 \ --logging_steps 10
三、 阶段三:对齐与微调 (SFT & DPO) 预训练后的 Tiny9 只会“续写”,我们需要用 trl (Transformer Reinforcement Learning) 库让它“听话”。
1. 监督微调 (SFT) 数据格式 :{"instruction": "你好", "output": "你好!我是Tiny9。"}
SFT 核心代码 (run_sft.py) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from transformers import AutoModelForCausalLM, AutoTokenizerfrom trl import SFTTrainer, SFTConfigfrom datasets import load_datasetmodel = AutoModelForCausalLM.from_pretrained("./checkpoints/tiny9-pt" , torch_dtype=torch.bfloat16) tokenizer = AutoTokenizer.from_pretrained("./tiny9_tokenizer" ) tokenizer.pad_token = tokenizer.eos_token def formatting_prompts_func (example ): text = f"<bos>### 指令:\n{example['instruction' ]} \n\n### 回答:\n{example['output' ]} <eos>" return {"text" : text} dataset = load_dataset("json" , data_files="data/sft_data.json" )["train" ] dataset = dataset.map (formatting_prompts_func) training_args = SFTConfig( output_dir="./checkpoints/tiny9-sft" , per_device_train_batch_size=4 , learning_rate=2e-5 , num_train_epochs=3 , logging_steps=10 , save_strategy="epoch" , bf16=True , report_to="tensorboard" ) trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset, tokenizer=tokenizer, max_seq_length=2048 , ) trainer.train()
2. 直接偏好优化 (DPO) SFT 后,模型可能产生“虽然格式正确,但人类不喜欢”的回答。DPO 通过对比 chosen (好回答) 和 rejected (坏回答) 来优化。
数据格式 :{"prompt": "如何评价苹果?", "chosen": "苹果很好吃...", "rejected": "苹果是手机..."}
DPO 核心代码 (run_dpo.py) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from trl import DPOTrainer, DPOConfigmodel = AutoModelForCausalLM.from_pretrained("./checkpoints/tiny9-sft" ) ref_model = AutoModelForCausalLM.from_pretrained("./checkpoints/tiny9-sft" ) training_args = DPOConfig( output_dir="./checkpoints/tiny9-dpo" , beta=0.1 , learning_rate=5e-6 , per_device_train_batch_size=2 , gradient_accumulation_steps=4 , max_length=1024 , max_prompt_length=512 , bf16=True ) trainer = DPOTrainer( model=model, ref_model=ref_model, args=training_args, train_dataset=load_dataset("json" , data_files="data/dpo_data.json" )["train" ], tokenizer=tokenizer, ) trainer.train()
四、 阶段四:量化与推理部署 (Quantization & Inference) 训练好的 Tiny9 (BF16) 大约需要 2GB 显存。为了在边缘设备或高并发下部署,我们进行 INT4 量化。
1. 模型量化 (AWQ) AWQ (Activation-aware Weight Quantization) 是目前效果最好的 PTQ(训练后量化)算法之一。
量化实施代码 (quantize_awq.py) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from awq import AutoAWQForCausalLMfrom transformers import AutoTokenizermodel_path = "./checkpoints/tiny9-dpo" quant_path = "./checkpoints/tiny9-dpo-awq-int4" quant_config = { "zero_point" : True , "q_group_size" : 128 , "version" : "GEMM" } model = AutoAWQForCausalLM.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) print ("开始 AWQ INT4 量化..." )model.quantize(tokenizer, quant_config=quant_config) model.save_quantized(quant_path) tokenizer.save_pretrained(quant_path) print (f"量化完成,保存至 {quant_path} " )
2. 推理部署 (vLLM) 使用 vLLM 部署高性能推理服务,支持 Continuous Batching 和 PagedAttention。
部署启动命令 :
1 2 3 4 5 6 7 8 9 python -m vllm.entrypoints.openai.api_server \ --model ./checkpoints/tiny9-dpo-awq-int4 \ --tokenizer ./checkpoints/tiny9-dpo-awq-int4 \ --dtype float16 \ --max-model-len 2048 \ --gpu-memory-utilization 0.9 \ --port 8000 \ --host 0.0.0.0
客户端调用测试 :
1 2 3 4 5 6 7 8 9 10 11 from openai import OpenAIclient = OpenAI(base_url="http://localhost:8000/v1" , api_key="empty" ) response = client.chat.completions.create( model="tiny9-dpo-awq-int4" , messages=[{"role" : "user" , "content" : "请用一句话解释什么是 MoE 架构?" }], temperature=0.7 , max_tokens=100 ) print (response.choices[0 ].message.content)
五、 完整实施 Checklist (避坑指南) 在真正跑通这套流程时,请务必对照以下 Checklist:
📊 数据阶段 🏗️ 预训练阶段 🎯 对齐阶段 (SFT/DPO) 🚀 部署阶段 延伸阅读 模型技术和算法革新经常在变,以 Tiny9 的模型名称与参数配置用于教学示例,文中的代码是逐步构建研发系统的基础实现,并非已完成训练、全面验证和性能优化的商业级模型发布包。