練到推理部署的完整鏈路)
接這個(gè)項(xiàng)目的時(shí)候我面前只有一臺(tái)沒(méi)裝任何深度學(xué)習(xí)環(huán)境的服務(wù)器。任務(wù)說(shuō)得很簡(jiǎn)單從一個(gè)空目錄開始搭出一條完整可用的AI工程鏈路并且讓這條路真的跑通產(chǎn)出能讓人直接調(diào)用的推理能力。這就是我后來(lái)在項(xiàng)目筆記里寫的ai-engineering-from-scratch。當(dāng)時(shí)“from scratch”恰好是圈子里最熱的話題不管是“從零構(gòu)建大語(yǔ)言模型”還是“從零構(gòu)建一個(gè)推理模型”大家其實(shí)都在討論同一件事不直接套現(xiàn)成方案而是自己把數(shù)據(jù)、訓(xùn)練、評(píng)估、部署整個(gè)鏈條逐一打通。這篇文章是我對(duì)這段經(jīng)歷的完整復(fù)盤適合正在準(zhǔn)備自建AI能力的團(tuán)隊(duì)、想獨(dú)立做模型項(xiàng)目的工程師以及那些希望真正理解大模型內(nèi)部工作方式而不是只會(huì)調(diào)API的學(xué)習(xí)者。1. 項(xiàng)目解讀先想清楚“從零”到底是從哪里開始1.1 “AI工程”不是“AI調(diào)參”很多人聽(tīng)到AI工程第一反應(yīng)是訓(xùn)練模型。真上手后才會(huì)發(fā)現(xiàn)訓(xùn)練只是鏈條里很小的一段。我當(dāng)時(shí)的實(shí)際任務(wù)包括收集和處理數(shù)據(jù)、設(shè)計(jì)tokenize鏈路、配置分布式訓(xùn)練、處理斷點(diǎn)續(xù)訓(xùn)、搭建評(píng)估體系、做推理服務(wù)化、上線后還要盯監(jiān)控和迭代。這些事加在一起才配得上“工程”這個(gè)詞。所以這個(gè)項(xiàng)目的第一個(gè)設(shè)計(jì)原則是不要把自己定位成“寫模型的人”而是“建設(shè)系統(tǒng)的人”。模型結(jié)構(gòu)只是系統(tǒng)的一個(gè)組件跟數(shù)據(jù)、算力、代碼、配置、監(jiān)控是平級(jí)的。這個(gè)心態(tài)轉(zhuǎn)過(guò)來(lái)之后很多決策就會(huì)變得順理成章。比如你會(huì)愿意花三天時(shí)間搭實(shí)驗(yàn)追蹤體系因?yàn)檫@能避免后面十次實(shí)驗(yàn)變成一鍋粥你會(huì)愿意在還沒(méi)訓(xùn)練之前就寫評(píng)估腳本因?yàn)槟悴幌朐谀玫侥P秃蟛砰_始糾結(jié)“怎樣才算效果變好”。這也是我把項(xiàng)目命名為ai-engineering-from-scratch的原因重點(diǎn)是engineering不是model。1.2 為什么選“from scratch”而不是直接調(diào)現(xiàn)成接口項(xiàng)目啟動(dòng)前團(tuán)隊(duì)內(nèi)部討論過(guò)直接用商業(yè)API不行嗎或者拿開源模型微調(diào)一下不就行了嗎當(dāng)時(shí)從零開始的原因其實(shí)兼而有之既有技術(shù)層面的考慮也有長(zhǎng)期成本和數(shù)據(jù)合規(guī)的因素。我把三種方案的權(quán)衡列出來(lái)供你參考方案優(yōu)點(diǎn)缺點(diǎn)適合場(chǎng)景商業(yè)API上手快、迭代迅速長(zhǎng)期成本高、數(shù)據(jù)要過(guò)第三方、定制能力弱快速驗(yàn)證想法、非核心業(yè)務(wù)開源模型SFT微調(diào)可以私有化、定制程度較高環(huán)境搭建復(fù)雜、訓(xùn)練調(diào)優(yōu)仍需工程能力有一定基礎(chǔ)設(shè)施的團(tuán)隊(duì)全部自建鏈路from scratch完全可控、學(xué)習(xí)價(jià)值最大、可深度優(yōu)化工程量大、周期長(zhǎng)、需要跨領(lǐng)域能力研究型團(tuán)隊(duì)、核心能力建設(shè)、深度學(xué)習(xí)者需要澄清一點(diǎn)from scratch不是讓你從線性代數(shù)開始手寫Transformer、手寫反向傳播?,F(xiàn)代AI工程說(shuō)得直白點(diǎn)是“從空倉(cāng)庫(kù)開始自己搭體系”。框架該用就用預(yù)訓(xùn)練權(quán)重該加載就加載真正自建的是環(huán)境、數(shù)據(jù)通道、訓(xùn)練策略、評(píng)估閉環(huán)、部署管線以及對(duì)整個(gè)系統(tǒng)每個(gè)環(huán)節(jié)的掌控能力。1.3 選型先于編碼五個(gè)關(guān)鍵決策點(diǎn)在寫第一行代碼之前我花了兩天時(shí)間做決策。這一步別省因?yàn)楹竺鎺缀跛锌佣紒?lái)自前期沒(méi)想清楚。我的決策順序是這樣第一步定硬件邊界。當(dāng)時(shí)手頭有單張24GB的消費(fèi)級(jí)顯卡后來(lái)又借到一臺(tái)4卡A100機(jī)器。我按最低配置做了設(shè)計(jì)保證“單卡能跑通、多卡能擴(kuò)展”。第二步定訓(xùn)練規(guī)模。先用1B左右的小模型跑通端到端再考慮切到7B級(jí)。這條路線幫我至少省了兩周的排錯(cuò)時(shí)間。第三步定部署形態(tài)。目標(biāo)很明確最終要能對(duì)外提供HTTP推理接口需要支持并發(fā)請(qǐng)求因此推理優(yōu)化必須在設(shè)計(jì)階段就留出位置。第四步定評(píng)估標(biāo)準(zhǔn)。先固化一套離線評(píng)測(cè)集每次模型更新都必須在這個(gè)評(píng)測(cè)集上跑出指標(biāo)不許憑感覺(jué)判斷“這版好像更好”。第五步定框架選型。訓(xùn)練統(tǒng)一走PyTorch生態(tài)分布式用DeepSpeed/DDP推理單獨(dú)用優(yōu)化過(guò)的服務(wù)框架。這套選型思路的核心是“先低估后擴(kuò)展”。不要一開始就規(guī)劃一個(gè)80GB顯存才能訓(xùn)練的模型方案而是先找到當(dāng)前硬件下最小可運(yùn)行的閉環(huán)把它跑通再逐步放量。2. 環(huán)境與工程骨架搭建可復(fù)現(xiàn)的地基2.1 環(huán)境搭建的版本矩陣與三步驗(yàn)證環(huán)境搭建看起來(lái)是小事實(shí)際是項(xiàng)目前期最消磨耐心的地方。CUDA、PyTorch、cuDNN、Python版本之間一旦錯(cuò)配出現(xiàn)的報(bào)錯(cuò)千奇百怪有的說(shuō)顯存不足有的說(shuō)算子不存在有的直接段錯(cuò)誤。我當(dāng)時(shí)的策略是先確定一個(gè)自己驗(yàn)證過(guò)的版本組合然后整個(gè)項(xiàng)目期間不隨意升級(jí)。以我當(dāng)時(shí)的版本組合為例可以這樣操作第一步創(chuàng)建干凈的Python虛擬環(huán)境conda create -n ai-eng python3.11 -y conda activate ai-eng第二步根據(jù)CUDA驅(qū)動(dòng)版本安裝對(duì)應(yīng)PyTorch。這里先用nvidia-smi看驅(qū)動(dòng)支持的CUDA最高版本而不是盲目裝最新版nvidia-smi pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121第三步驗(yàn)證環(huán)境可用python -c import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))看到True和顯卡型號(hào)基礎(chǔ)環(huán)境才算通。這里有個(gè)我踩過(guò)的坑很多人裝了PyTorch后只檢查import torch不報(bào)錯(cuò)卻沒(méi)檢查torch.cuda.is_available()結(jié)果訓(xùn)練時(shí)才發(fā)現(xiàn)模型跑在CPU上慢到懷疑人生。2.2 項(xiàng)目目錄設(shè)計(jì)數(shù)據(jù)與代碼的四區(qū)分離項(xiàng)目骨架我按照“四區(qū)分離”的思路設(shè)計(jì)這個(gè)結(jié)構(gòu)后來(lái)被好幾個(gè)朋友抄走確實(shí)好用ai-engineering-from-scratch/ ├── configs/ # 所有實(shí)驗(yàn)配置YAML/JSON ├── data/ # 原始數(shù)據(jù)、中間數(shù)據(jù)、最終數(shù)據(jù)集 ├── scripts/ # 一次性腳本、數(shù)據(jù)清洗腳本、部署腳本 ├── src/ │ ├── data/ # 數(shù)據(jù)加載、tokenize、dataset實(shí)現(xiàn) │ ├── models/ # 模型結(jié)構(gòu)定義 │ ├── train/ # 訓(xùn)練循環(huán)、分布式邏輯、checkpoint管理 │ └── eval/ # 評(píng)估指標(biāo)、評(píng)測(cè)集加載 ├── experiments/ # 每次實(shí)驗(yàn)的輸出、日志、曲線 └── tests/ # 單元測(cè)試、數(shù)據(jù)流水線測(cè)試這樣設(shè)計(jì)背后的邏輯是數(shù)據(jù)、代碼、配置、實(shí)驗(yàn)記錄四者彼此隔離又通過(guò)顯式路徑關(guān)聯(lián)。比如configs/exp_001.yaml里寫明了數(shù)據(jù)集的路徑指向experiments/exp_001/下保存的是這次實(shí)驗(yàn)的輸出。任何人包括未來(lái)三天的我自己打開目錄都能快速定位“這版模型是什么配置、什么數(shù)據(jù)、什么代碼跑出來(lái)的”。2.3 顯存估算的速算方法先知道死不死訓(xùn)練還沒(méi)開始就遇到OOM是新手最常見(jiàn)的第一課。與其反復(fù)試錯(cuò)不如先做一次顯存估算。我后來(lái)總結(jié)出一個(gè)速算方法準(zhǔn)確度夠用。以7B參數(shù)模型為例全參數(shù)混合精度訓(xùn)練的顯存構(gòu)成模型權(quán)重BF167 × 10^9 × 2字節(jié) 約14GB梯度BF16同樣約14GBAdamW優(yōu)化器狀態(tài)每個(gè)參數(shù)需要存儲(chǔ)fp32的動(dòng)量(m)和方差(v)外加fp32權(quán)重副本合計(jì)每參數(shù)約12字節(jié)也就是約84GB三者相加全參數(shù)訓(xùn)練7B模型大約需要112GB顯存單張80GB的A100/H100也裝不下。這也是為什么很多人說(shuō)“7B全參微調(diào)門檻很高”。但如果改用LoRA可訓(xùn)練參數(shù)往往只占全部參數(shù)的0.5%到1%。優(yōu)化器狀態(tài)直接從幾十GB降到1GB以內(nèi)整體顯存占用可以壓到20GB量級(jí)單張24GB的消費(fèi)級(jí)顯卡就能跑。這就是LoRA能普及的根本原因。方案7B模型估算顯存單卡24GB是否可行全參數(shù)微調(diào)約112GB否LoRA微調(diào)約20GB左右是需開gradient checkpointing4bit量化推理約6GB是這個(gè)估算方法基于標(biāo)準(zhǔn)AdamW和混合精度訓(xùn)練的常見(jiàn)假設(shè)具體項(xiàng)目應(yīng)以實(shí)際的profiler為準(zhǔn)。但用它做前期方案可行性判斷足夠了。2.4 從第一天就做實(shí)驗(yàn)跟蹤別信自己的腦子項(xiàng)目做到第三周時(shí)我已經(jīng)跑了十幾個(gè)實(shí)驗(yàn)。如果沒(méi)用系統(tǒng)記錄我根本說(shuō)不清哪一版數(shù)據(jù)清洗邏輯、哪個(gè)學(xué)習(xí)率、哪次隨機(jī)種子產(chǎn)出了當(dāng)前的模型。后來(lái)養(yǎng)成的習(xí)慣是每次訓(xùn)練任務(wù)結(jié)束強(qiáng)制登記一份實(shí)驗(yàn)記錄內(nèi)容包含git commit號(hào)數(shù)據(jù)集版本數(shù)據(jù)目錄的hash值配置文件的完整內(nèi)容訓(xùn)練關(guān)鍵指標(biāo)最終loss、eval指標(biāo)、顯存峰值、訓(xùn)練時(shí)長(zhǎng)當(dāng)時(shí)的硬件環(huán)境哪臺(tái)機(jī)器、幾張卡我用最簡(jiǎn)單的JSON格式記錄不需要額外平臺(tái)只要能追溯就行。下面是當(dāng)時(shí)某次實(shí)驗(yàn)的節(jié)選{ experiment_id: exp_014, git_commit: a3f9c2e1, data_hash: sha256:9f2c..., model: qwen2.5-1.5b-instruct, train_method: sft lora, learning_rate: 2e-5, batch_size: 8, gradient_accumulation: 8, effective_batch_size: 64, gpu: 1x RTX 4090, final_loss: 1.32, eval_score: 0.71, max_memory_used_gb: 18.5 }這套機(jī)制讓我后面復(fù)現(xiàn)模型時(shí)輕松很多。所謂“engineer的感覺(jué)”就是每個(gè)產(chǎn)物的家底清清楚楚。3. 數(shù)據(jù)流水線與訓(xùn)練鏈路核心環(huán)節(jié)的實(shí)現(xiàn)3.1 數(shù)據(jù)是最大的工程難點(diǎn)比模型結(jié)構(gòu)更費(fèi)心思很多人以為大模型項(xiàng)目里最有價(jià)值的是模型結(jié)構(gòu)。實(shí)際上對(duì)做工程的人來(lái)說(shuō)數(shù)據(jù)才是最耗精力的部分。模型結(jié)構(gòu)有大量現(xiàn)成實(shí)現(xiàn)數(shù)據(jù)卻無(wú)法從別處搬走每一份都和應(yīng)用場(chǎng)景強(qiáng)綁定。我當(dāng)時(shí)做數(shù)據(jù)清洗時(shí)列了一個(gè)操作清單逐項(xiàng)執(zhí)行去重、去除頁(yè)面模板噪聲、過(guò)濾低質(zhì)量?jī)?nèi)容、統(tǒng)一格式、過(guò)濾超長(zhǎng)樣本、做敏感信息脫敏。其中去重這一步很關(guān)鍵原本以為自己的數(shù)據(jù)很干凈做完MinHash去重后發(fā)現(xiàn)重復(fù)率高達(dá)12%。如果帶著這些重復(fù)數(shù)據(jù)訓(xùn)練模型會(huì)反復(fù)背誦某些段落評(píng)估時(shí)看著不錯(cuò)一上真實(shí)場(chǎng)景就露餡。另一個(gè)要防的是數(shù)據(jù)泄漏。如果訓(xùn)練集里混入了評(píng)估集的題目或答案評(píng)估分?jǐn)?shù)會(huì)虛高而且你自己不知道。我在切分?jǐn)?shù)據(jù)時(shí)用了一個(gè)硬性規(guī)則先按內(nèi)容hash全局去重再去切分訓(xùn)練集和評(píng)估集保證兩個(gè)集合沒(méi)有交集。這一步對(duì)后續(xù)所有評(píng)估都至關(guān)重要的。3.2 從文本到訓(xùn)練樣本tokenize里的魔鬼細(xì)節(jié)數(shù)據(jù)清洗完后要把自然語(yǔ)言變成模型能讀的token序列。這里的細(xì)節(jié)比想象中多padding策略、truncation長(zhǎng)度、attention mask處理、是否做sequence packing。我最初寫的dataloader非常樸素每條樣本單獨(dú)tokenizepadding到相同長(zhǎng)度。結(jié)果訓(xùn)練速度很慢因?yàn)榇罅繕颖緦?shí)際長(zhǎng)度遠(yuǎn)短于max_len算力浪費(fèi)嚴(yán)重。后來(lái)改成動(dòng)態(tài)batch sequence packing才把訓(xùn)練吞吐提上去。def tokenize_and_pack(examples, tokenizer, max_len2048): texts examples[text] all_ids [] for text in texts: ids tokenizer.encode(text, add_special_tokensTrue) all_ids.extend(ids) all_ids.append(tokenizer.eos_token_id) # 樣本間加分隔 packed [] for i in range(0, len(all_ids) - max_len 1, max_len): chunk all_ids[i:imax_len] packed.append({input_ids: chunk}) return packed這段代碼背后的想法是不再給每條樣本單獨(dú)padding而是把一堆樣本的token連接起來(lái)再切段。每段都是滿長(zhǎng)度的顯存利用率高訓(xùn)練速度快很多。代價(jià)是樣本邊界變得不清晰需要讓loss忽略掉那些跨越樣本邊界的部分。實(shí)現(xiàn)上可以給每段再做一個(gè)segment_id或者干脆在接受少量噪聲的前提下用簡(jiǎn)單做法。用這個(gè)方法同樣的硬件和數(shù)據(jù)量訓(xùn)練吞吐量提升了約40%。這是我在這個(gè)項(xiàng)目里早期收益最大的一次優(yōu)化。3.3 訓(xùn)練循環(huán)混合精度、梯度累積與斷點(diǎn)續(xù)訓(xùn)訓(xùn)練循環(huán)看起來(lái)就是“前向、反向、更新”但真正工程化之后每行代碼背后都有講究。我當(dāng)時(shí)的訓(xùn)練循環(huán)簡(jiǎn)寫如下from torch.cuda.amp import autocast, GradScaler import torch.distributed as dist scaler GradScaler() for step, batch in enumerate(train_loader): with autocast(): outputs model(**batch) loss outputs.loss / gradient_accumulation_steps scaler.scale(loss).backward() if (step 1) % gradient_accumulation_steps 0: scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update() optimizer.zero_grad()這段代碼里有幾個(gè)關(guān)鍵決策梯度累積是為了模擬更大的batch size。當(dāng)時(shí)我單卡能塞下的batch是8但實(shí)驗(yàn)計(jì)劃里的有效batch是64于是設(shè)置累積步數(shù)為8?;旌暇扔?xùn)練讓顯存壓力和計(jì)算速度都改善很多但loss scaling容易出問(wèn)題表現(xiàn)為訓(xùn)練后期loss突然發(fā)散或變成nan。grad clip則是對(duì)抗這種發(fā)散的第一道防線我習(xí)慣設(shè)max_norm1.0。斷點(diǎn)續(xù)訓(xùn)是我節(jié)約時(shí)間的大功臣。訓(xùn)練7B模型動(dòng)輒幾十個(gè)小時(shí)不可能保證中途不出問(wèn)題。我的checkpoint保存邏輯后來(lái)在第五節(jié)詳細(xì)講這里只說(shuō)結(jié)論不保存optimizer和scheduler狀態(tài)的“假斷點(diǎn)”毫無(wú)意義恢復(fù)訓(xùn)練后不僅指標(biāo)對(duì)不上還可能因?yàn)閷W(xué)習(xí)率狀態(tài)錯(cuò)亂把模型訓(xùn)崩。3.4 從語(yǔ)言模型到推理模型一條可以“抄作業(yè)”的路線項(xiàng)目后半程我開始收到一個(gè)新的需求讓模型不只是“接話”而是在回答前先進(jìn)行推理思考。這也是現(xiàn)在圈子里很熱的“從零構(gòu)建推理模型”路線的本質(zhì)。一條可行的簡(jiǎn)化路線是這樣走的第一步做SFT用帶思維鏈的樣本微調(diào)基礎(chǔ)模型。樣本格式大概是“問(wèn)題 推理過(guò)程 最終答案”。第二步做偏好優(yōu)化讓模型學(xué)會(huì)“好的推理路徑長(zhǎng)什么樣”。這里我選擇了GRPO這類不需要訓(xùn)練額外獎(jiǎng)勵(lì)模型的方案。GRPO的工程實(shí)現(xiàn)需要三個(gè)模塊采樣模塊對(duì)同一個(gè)問(wèn)題采樣N條回答獎(jiǎng)勵(lì)模塊用規(guī)則或模型判斷回答好壞策略更新模塊根據(jù)獎(jiǎng)勵(lì)信號(hào)調(diào)整策略。我當(dāng)時(shí)用的是簡(jiǎn)化版規(guī)則獎(jiǎng)勵(lì)數(shù)學(xué)題直接比對(duì)最終答案是否正確格式上要求包含“思考過(guò)程”和“最終答案”兩段符合格式加1分答案正確再加1分。獎(jiǎng)勵(lì)信號(hào)非常干凈工程實(shí)現(xiàn)也不復(fù)雜。def compute_reward(prompt, response, answer): if 思考過(guò)程 not in response or 最終答案 not in response: return 0.0 score 1.0 if extract_answer(response) answer: score 2.0 return score這整條路線對(duì)工程側(cè)的啟示是推理模型不是單一的模型結(jié)構(gòu)而是“基座模型 采樣器 獎(jiǎng)勵(lì)函數(shù) 優(yōu)化器 數(shù)據(jù)流水線”的組合。需要自建的環(huán)節(jié)越多ai-engineering-from-scratch的路線就越有價(jià)值因?yàn)槟銦o(wú)法靠一個(gè)API解決全部問(wèn)題。4. 評(píng)估、部署與迭代把模型從實(shí)驗(yàn)品變成產(chǎn)品4.1 先定義“效果好”離線評(píng)估體系訓(xùn)練之前先寫評(píng)估腳本聽(tīng)起來(lái)有點(diǎn)反直覺(jué)但這是我整個(gè)項(xiàng)目里最受益的決定。沒(méi)有評(píng)估標(biāo)準(zhǔn)前模型迭代就是無(wú)頭蒼蠅有了評(píng)估集后每次改動(dòng)都能被量化。我建立了三層評(píng)估第一層是基礎(chǔ)指標(biāo)loss曲線、困惑度。這層只能反映模型有沒(méi)有正常學(xué)習(xí)不能反映任務(wù)能力。第二層是任務(wù)集指標(biāo)針對(duì)實(shí)際業(yè)務(wù)場(chǎng)景準(zhǔn)備的幾百道測(cè)試題每次跑完都得到準(zhǔn)確率。比如常見(jiàn)指令遵循測(cè)試、數(shù)學(xué)計(jì)算測(cè)試、知識(shí)問(wèn)答測(cè)試。第三層是人工bad case評(píng)審自動(dòng)指標(biāo)之外的抽檢和長(zhǎng)期記錄。評(píng)估維度具體方法用途與注意點(diǎn)基礎(chǔ)指標(biāo)訓(xùn)練loss、驗(yàn)證集loss判斷是否欠擬合/過(guò)擬合不能只看它任務(wù)集指標(biāo)固定數(shù)據(jù)集的準(zhǔn)確率、匹配率版本對(duì)比的核心依據(jù)集子必須固定人工評(píng)審抽樣看模型輸出記錄bad case發(fā)現(xiàn)自動(dòng)指標(biāo)看不到的質(zhì)量問(wèn)題穩(wěn)定性檢查同題多次輸出的方差推理類任務(wù)要關(guān)注隨機(jī)性輸出是否漂移評(píng)估集一經(jīng)確定絕不輕易改題目。我吃過(guò)虧評(píng)估中途發(fā)現(xiàn)幾道題本身表述不清晰直接換了題結(jié)果新版模型分?jǐn)?shù)提升其實(shí)是題目變動(dòng)帶來(lái)的誤導(dǎo)了我整整兩天。4.2 部署與推理優(yōu)化從訓(xùn)練權(quán)重到可用服務(wù)模型訓(xùn)練完要變成一個(gè)能抗住請(qǐng)求的在線服務(wù)還有一段路要走。我的部署流程大致是模型導(dǎo)出 → 格式轉(zhuǎn)換 → 量化 → 單請(qǐng)求測(cè)試 → 并發(fā)壓測(cè) → 上線。量化在消費(fèi)級(jí)顯卡上幾乎是必須的。7B模型BF16推理大約占14GB顯存而4bit量化后只剩約6GB調(diào)用成本和顯存壓力都大幅下降。代價(jià)是極端復(fù)雜推理任務(wù)上質(zhì)量可能有輕微波動(dòng)。服務(wù)框架我用了專門的推理引擎用起來(lái)很簡(jiǎn)單核心參數(shù)包括最大輸入長(zhǎng)度、最大輸出長(zhǎng)度、并發(fā)批大小。下面是一個(gè)部署腳本的簡(jiǎn)化示意from vllm import LLM, SamplingParams llm LLM(model./qwen2.5-7b-instruct-awq, tensor_parallel_size1, gpu_memory_utilization0.9) params SamplingParams(temperature0.7, top_p0.8, max_tokens1024) def generate(prompt): outputs llm.generate([prompt], params) return outputs[0].outputs[0].text上線前的單請(qǐng)求耗時(shí)和并發(fā)壓測(cè)也很有必要。我當(dāng)時(shí)的經(jīng)驗(yàn)參考單張24GB顯卡部署7B量化模型單請(qǐng)求首token延遲約幾十毫秒到上百毫秒具體受輸入長(zhǎng)度影響并發(fā)8路時(shí)能維持穩(wěn)定的吞吐但顯存占用會(huì)明顯上升。這組數(shù)據(jù)在真實(shí)場(chǎng)景上應(yīng)該重新測(cè)不能直接搬。4.3 監(jiān)控、回滾與數(shù)據(jù)回流上線不是終點(diǎn)模型上線后我見(jiàn)過(guò)太多人從此不管了直到用戶開始吐槽才發(fā)現(xiàn)輸出質(zhì)量崩了。工程鏈路必須包含監(jiān)控和迭代閉環(huán)。我在線上至少盯著這幾類指標(biāo)請(qǐng)求量、推理延遲、錯(cuò)誤率、輸入輸出的平均長(zhǎng)度、用戶反饋的打分分布。其中打分分布最有價(jià)值如果連續(xù)幾天發(fā)現(xiàn)低分比例上升就該跑一輪測(cè)試集對(duì)比決定是否需要回滾。數(shù)據(jù)回流是整個(gè)閉環(huán)的發(fā)動(dòng)機(jī)。線上出現(xiàn)的bad case不能只是看一眼就刪掉我會(huì)把脫敏后存成新的訓(xùn)練樣本等積累到一定量后觸發(fā)新一輪微調(diào)。這就像滾雪球模型會(huì)隨著真實(shí)使用越來(lái)越貼合場(chǎng)景。這里的關(guān)鍵是建立“可回滾”機(jī)制。每次上線都留上一個(gè)版本的權(quán)重切流量時(shí)做灰度先放少量請(qǐng)求等指標(biāo)平穩(wěn)后再放量。AI項(xiàng)目的回滾不丟人模型迭代本來(lái)就是試錯(cuò)過(guò)程。4.4 消費(fèi)級(jí)硬件怎么跑完整流程我的整個(gè)項(xiàng)目并不全在集群上完成很多實(shí)驗(yàn)是在一張24GB顯卡上跑的。如果你手里的設(shè)備更有限可以參考這個(gè)思路優(yōu)先選小模型3B/7B級(jí)別足夠覆蓋大量真實(shí)任務(wù)。不是所有需求都要70B。用LoRA微調(diào)訓(xùn)練顯存減少到原先的1/5左右效果在多數(shù)任務(wù)上和有損微調(diào)差距不大。開gradient checkpointing犧牲一點(diǎn)點(diǎn)訓(xùn)練速度換來(lái)顯存大幅降低?;P陀?bit量化推理顯存壓到6GB左右普通工作站就能跑。如果你的目標(biāo)場(chǎng)景是數(shù)學(xué)推理這類高難度任務(wù)還可以考慮用小模型加蒸餾策略。讓大模型生成推理軌跡再用這些軌跡訓(xùn)練小模型。這條路線不需要大顯存只需要你愿意花時(shí)間做數(shù)據(jù)性價(jià)比非常高。5. 實(shí)戰(zhàn)排坑與經(jīng)驗(yàn)心得5.1 高發(fā)問(wèn)題速查表六個(gè)崩潰現(xiàn)場(chǎng)項(xiàng)目過(guò)程中我記錄了不少問(wèn)題這里挑最典型的六個(gè)整理成表遇到時(shí)可以直接對(duì)號(hào)入座。問(wèn)題現(xiàn)象常見(jiàn)原因排查手段解決方向訓(xùn)練剛開始就OOM顯存估算不足batch過(guò)大用profiler看峰值顯存降低batch開gradient checkpointing切LoRAloss前期正常后期發(fā)散或nan學(xué)習(xí)率過(guò)高、數(shù)據(jù)里有異常樣本、loss scaling問(wèn)題查看前幾步梯度范數(shù)定位異常batch降低學(xué)習(xí)率、加強(qiáng)grad clip、清理臟數(shù)據(jù)驗(yàn)證集指標(biāo)虛高線上效果差數(shù)據(jù)泄漏、train和eval有重疊檢查樣本hash是否有交集重新劃分?jǐn)?shù)據(jù)去重后再切分訓(xùn)練中斷恢復(fù)后指標(biāo)驟變checkpoint沒(méi)保存optimizer和RNG狀態(tài)檢查checkpoint內(nèi)容完整保存optimizer、scheduler、dataloader狀態(tài)同樣的代碼別人能跑自己就報(bào)錯(cuò)依賴版本不一致、cuda版本錯(cuò)配對(duì)比環(huán)境列表用conda導(dǎo)出鎖定依賴版本復(fù)制完整環(huán)境模型輸出大量重復(fù)內(nèi)容訓(xùn)練數(shù)據(jù)重復(fù)、推理參數(shù)不當(dāng)檢查數(shù)據(jù)去重情況看重復(fù)率數(shù)據(jù)去重調(diào)高temperature加重復(fù)懲罰5.2 斷點(diǎn)續(xù)訓(xùn)的正確姿勢(shì)不只是存模型權(quán)重?cái)帱c(diǎn)續(xù)訓(xùn)是我在這個(gè)項(xiàng)目里學(xué)到教訓(xùn)最深的一課。第一次做時(shí)我只保存了model.state_dict()恢復(fù)訓(xùn)練后不僅指標(biāo)對(duì)不上模型輸出明顯變差等于白跑。后來(lái)才明白一個(gè)可用的checkpoint必須包含模型權(quán)重optimizer狀態(tài)尤其是AdamW的動(dòng)量項(xiàng)和方差項(xiàng)scheduler狀態(tài)確認(rèn)當(dāng)前學(xué)習(xí)率位置隨機(jī)數(shù)生成器狀態(tài)包括PyTorch、Python、CUDA的隨機(jī)狀態(tài)dataloader的當(dāng)前位置或者至少是step數(shù)簡(jiǎn)化實(shí)現(xiàn)的思路如下checkpoint { model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), rng_state: torch.get_rng_state(), cuda_rng_state: torch.cuda.get_rng_state(), step: step, } torch.save(checkpoint, fcheckpoint_step_{step}.pt)實(shí)測(cè)下來(lái)完整保存這些狀態(tài)后恢復(fù)訓(xùn)練loss曲線能夠和中斷前無(wú)縫銜接。省下的時(shí)間和避免的返工比寫這些代碼花的時(shí)間多得多。5.3 復(fù)現(xiàn)性把變量鎖死模型實(shí)驗(yàn)經(jīng)常遇到“這次跑的和上次不一樣”的情況。很多同學(xué)第一個(gè)念頭是模型隨機(jī)性其實(shí)問(wèn)題往往出在變量沒(méi)鎖住。我后來(lái)固定了這些因素全局隨機(jī)種子、PyTorch的use_deterministic_algorithms、cudnn的benchmark開關(guān)、數(shù)據(jù)加載順序、tokenizer版本、依賴版本。其中數(shù)據(jù)加載順序最容易被忽略如果dataloader里用了多進(jìn)程shuffle理論上即使固定了seed每次運(yùn)行的數(shù)據(jù)順序也可能不同。def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False這套配置不能保證跨框架、跨GPU型號(hào)完全一比一復(fù)現(xiàn)但至少能讓同一臺(tái)機(jī)器上的實(shí)驗(yàn)具備可對(duì)比性。復(fù)現(xiàn)性是工程項(xiàng)目的底線沒(méi)有這個(gè)底線所有優(yōu)化都無(wú)從談起。5.4 復(fù)盤后的三條核心心法整個(gè)項(xiàng)目結(jié)束之后我復(fù)盤了很久最后留在腦子里的不是哪個(gè)具體技術(shù)點(diǎn)而是三條做事的順序和原則。第一先跑通1%的數(shù)據(jù)再放量到100%。我見(jiàn)過(guò)太多人一開始就把全部數(shù)據(jù)灌進(jìn)訓(xùn)練腳本然后花三周去找數(shù)據(jù)格式和加載邏輯的bug。用小規(guī)模數(shù)據(jù)先跑通一兩個(gè)step能最快暴露鏈路問(wèn)題哪怕只訓(xùn)練5個(gè)step也能驗(yàn)證各個(gè)模塊是通的。第二評(píng)估先于訓(xùn)練。第一天就把評(píng)估腳本寫出來(lái)哪怕還沒(méi)有任何模型可評(píng)。這會(huì)讓每一次實(shí)驗(yàn)變得“有錨點(diǎn)”不會(huì)在反復(fù)訓(xùn)練中迷失方向。我在項(xiàng)目中最浪費(fèi)時(shí)間的階段恰恰是對(duì)評(píng)估標(biāo)準(zhǔn)還模糊的那些天。第三環(huán)境變更要有記錄。每次升級(jí)Python包、切換CUDA版本、換GPU驅(qū)動(dòng)都可能是實(shí)驗(yàn)結(jié)果漂移的元兇。養(yǎng)成把環(huán)境變更寫進(jìn)實(shí)驗(yàn)筆記的習(xí)慣遇到奇怪的復(fù)現(xiàn)問(wèn)題時(shí)能少掉一大半頭發(fā)。這三條心法聽(tīng)起來(lái)樸素但實(shí)際操作中幫我把大量“玄學(xué)問(wèn)題”變成了可以定位的系統(tǒng)問(wèn)題。工程能力的提升很多時(shí)候不是學(xué)會(huì)更高級(jí)的技巧而是把基本功做到讓人放心。結(jié)尾如果重新來(lái)一次我會(huì)怎么做現(xiàn)在再回頭看這個(gè)項(xiàng)目我最大的體會(huì)是ai-engineering-from-scratch的價(jià)值不只是產(chǎn)出了一個(gè)可用的AI模型而是讓我對(duì)整個(gè)鏈條的每個(gè)環(huán)節(jié)都有了真實(shí)手感。數(shù)據(jù)清洗有多臟訓(xùn)練中斷有多痛評(píng)估指標(biāo)有多容易騙人上線后監(jiān)控有多必要這些體驗(yàn)不是靠看文檔能得到的。如果現(xiàn)在再給我一臺(tái)裸機(jī)重新開始我會(huì)比第一次快三倍不是因?yàn)榧夹g(shù)突飛猛進(jìn)而是因?yàn)槲抑懒恕鞍讶罩尽⒃u(píng)估、回滾三條線放在訓(xùn)練之前”這件事有多重要。最后想給你的建議只有一句話別怕從零開始也別急著追求大模型先把最小閉環(huán)跑通每一段鏈路親手接一遍你會(huì)獲得任何現(xiàn)成方案都給不了的掌控感。