
先說個我自己踩過的場景導師丟過來一臺 32GB 顯存的卡讓你把手頭 7B 模型微調一下給學生們演示效果。你高高興興把training_args一填batch_size2一跑五分鐘不到終端刷出一行CUDA out of memory。換batch_size1還是 OOM。網上搜了一圈人人都說“用 LoRA 啊”結果你換完 LoRA 照樣溢出于是開始懷疑人生。這篇博文就是來解決這個問題的。我會從顯存到底被誰吃掉了講起把 LoRA / QLoRA 為什么省顯存講透再給出一套我在 32GB GPU 上實測不 OOM 的配置模板最后整理一份排查 OOM 的速查表和踩坑記錄。適合手里只有一張 3090 / 4090 / A6000 這種“大單卡”的在校學生、算法工程師以及要給實驗室搭微調演示環境的朋友。看完你能明白一個核心事實OOM 不是因為你顯存小而是因為你把顯存花在了不該花的地方。1. 32GB 顯存還 OOM先搞明白訓練時顯存究竟花在哪很多人第一次做微調就把 OOM 歸咎于“模型太大了”這個判斷其實只對了一半。推理和訓練完全是兩種內存模型用 7B 模型的例子算一筆賬你會立刻明白訓練為什么顯得這么“重”。1.1 一套 7B 模型在訓練狀態下的真實顯存開銷先明確一個基礎數字以最常見的 LLaMA-2 結構為例7B 參數hidden size 4096層數 32。很多人會直接拿模型文件大小算顯存比如“模型 14GB那我 32GB 顯存肯定夠啊”——這個直覺丟掉了三個大頭。訓練時顯存里至少躺著四樣東西模型權重本身。BF16 精度下7B 參數 7 × 10^9 × 2 bytes ≈ 14GB。梯度。每個參數都有對應梯度同樣精度又是 14GB。優化器狀態。用的最多的 AdamW每個參數要維護一階動量fp32和二階動量fp32也就是每個參數 8 字節算下來 7B 參數要 56GB 的 fp32 狀態。哪怕你用 8bit 版優化器也需要 14GB。中間激活值。這是“隱形刺客”取決于 batch size 和序列長度7B 模型上一口氣吃掉十幾到幾十 GB 都不稀奇。把這三項加起來再保守估算權重 14GB 梯度 14GB 優化器狀態 56GBfp32 AdamW還沒算激活就已經逼近 84GB。這也就是為什么全參數微調 7B 模型通常要 4×A10080GB跑張量并行一張 32GB 的卡連門檻都夠不著。推理就完全不同了。推理只做前向傳播沒有梯度和優化器狀態只有權重 14GB KV cache 幾個 GB32GB 帶寬寬裕得很。所以我常說一句話你訓練時 OOM不是因為模型太大是因為你試圖在訓練狀態下背起“推理狀態下不需要的那幾十 GB 包袱”。1.2 為什么反復調低 batch size 治標不治本不少人遇到 OOM 的第一反應是減 batch size2不行換11不行就換0.5沒有 0.5。調低 batch size 確實有效因為激活值會線性下降但它動不了權重、梯度和優化器狀態這三塊硬性開銷。這三樣加起來7B 全參數微調少說 56GBbatch size 再怎么降也騰不出這 50 多 GB。這就是為什么你在 32GB 顯卡上把 batch size 調到 1 照樣 OOM。真正該做的是降低“每參數開銷”和“動不起來的那部分內存”而不是一味壓縮 batch size。理解了這一點再回頭看 LoRA / QLoRA思路就通了。1.3 一個判斷顯存瓶頸的快速心算法不用等訓練報錯先按下面幾步粗算模型參數量 N精度位數 b權重顯存 W N × b bytes。是否全參微調是再額外加至少 2N × 4 bytes梯度 fp32 優化器越估越大。是否只訓練部分參數LoRA是基礎模型權重顯存 W能凍結保存保持梯度換成 LoRA 那幾百萬參數優化器狀態也可以忽略。預留激活顯存7B 模型配 batch_size1、序列 512 時開啟梯度檢查點后大概 2~4GB沒開的話 10~20GB 起步。只要第 2 條直接命中32GB 卡上 7B 模型必然 OOM。所以接下來進入正題LoRA 和 QLoRA 分別怎樣做到“省掉那 50 多 GB”。2. LoRA 和 QLoRA 的核心原理到底幫你省了哪部分顯存LoRA 的論文標題已經說明一切Low-Rank Adaptation。它不改變預訓練模型的權重而是在線性層旁邊插入低秩分解矩陣只訓練這些旁路參數。QLoRA 更進一步先把基礎模型量化成 4bit再做 LoRA。兩者本質都是為了“砍掉前面說的硬性開銷”。2.1 LoRA 為什么省顯存因為優化器狀態從 56GB 縮到不到 100MB全參數微調時7B 模型的每個參數都要被 AdamW“盯”著優化器狀態 56GB這是最大的坑。LoRA 的做法是把要更新的參數數量壓到極致。假設你在每個q_proj、k_proj、v_proj、o_proj上掛 LoRArank8按 32 層、每層 4 個模塊算可訓練參數量 (4096 × 8 8 × 4096) × 4 × 32 ≈ 734 萬這約等于 7B 的千分之一。這 734 萬參數在 BF16 下只占約 15MBAdamW 的 fp32 狀態約 30MB梯度約 15MB——整個優化器相關開銷不到 60MB。相比全參微調的 56GB這就是天壤之別?;A模型本身呢LoRA 仍然加載完整 BF16 權重14GB 還是要的。再加上激活值只要批次長度控制好32GB 完全能裝下。所以 LoRA 省掉的不是“模型權重的顯存”而是省掉了梯度 優化器狀態這兩個大頭。2.2 QLoRA 靠什么再擠出 10GB 空間NF4 量化與雙重量化QLoRA 的思路很直白既然基礎模型權重只是作為特征提取器被凍結那能不能別用 2 字節存它換成 4bit答案是可以。4bit 意味著每個參數只需 0.5 字節7B 模型權重從 14GB 直接降到約 3.5GB。這個降幅非??捎^直接從顯存賬單里砍掉 10GB 左右。QLoRA 在實現上有兩個關鍵組件NF44-bit NormalFloat量化針對正態分布權重設計的數據類型4bit 有 16 個量化級別分布上更貼近神經網絡權重的真實分布精度損失明顯小于普通的均勻 4bit 量化。雙重量化Double Quantization把量化縮放因子也做一次量化繼續吃掉幾十 MB 的顯存尾部。實測中QLoRA 相比 LoRA 在 7B 模型上顯存峰值大約低 8~10GB。這意味著原來 LoRA 跑 batch_size2 勉強會 OOM 的配置換成 QLoRA 后 batch_size4 也能穩住。我補充一個容易混淆的點QLoRA 訓練時計算精度是 BF16更新的 LoRA 參數也是 BF164bit 只是“存儲精度”。也就是說你訓練出來的 LoRA 權重跟正常 LoRA 一樣用起來、合并回原模型都不需要額外處理 4bit 權重。2.3 一張表看清全參 / LoRA / QLoRA 在 7B 模型上的顯存構成開銷項全參微調LoRAQLoRA基礎模型權重BF16/4bit14GBBF1614GBBF163.5GBNF4可訓練參數權重14GB~15MB~15MB梯度14GB~15MB~15MB優化器狀態fp32 AdamW56GB~30MB~30MB激活開啟梯度檢查點數 GB~十幾 GB數 GB數 GB32GB 卡跑 batch1必 OOM勉強能跑余量充足所以結論很明確在 32GB 單卡上全參微調 7B 是硬傷LoRA 是及格線QLoRA 才是能讓你“跑得舒服、還能加大 batch”的選項。接下來給一套我實際驗證過的 QLoRA 配置模板。3. 32GB GPU 上可復現的 QLoRA 微調配置實戰這一節直接給做法。我自己長期在 RTX 309024GB和 A600048GB上做 7B 級模型的 QLoRA 訓練這里的配置同樣適用 32GB 卡。整套配置的核心思路就四個字按量給顯存。3.1 模型加載用 BitsAndBytes 把基礎權重壓到 3.5GBimport torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig model_name meta-llama/Llama-2-7b-chat-hf bnb_config BitsAndBytesConfig( load_in_4bitTrue, # 開啟 4bit 量化加載 bnb_4bit_quant_typenf4, # NF4 類型 bnb_4bit_use_double_quantTrue, # 雙重量化省更多顯存 bnb_4bit_compute_dtypetorch.bfloat16, # 計算精度保持 BF16 ) tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, quantization_configbnb_config, device_mapauto, torch_dtypetorch.bfloat16, )這里有幾個細節值得解釋。第一bnb_4bit_compute_dtype建議固定為 bfloat164bit 權重在計算時會反量化成這個精度BF16 在 AMP 場景下顯存和精度比較均衡FP16 在部分顯卡上容易出現梯度溢出BF16 穩得多。第二device_mapauto在單卡上沒壞處它會幫你處理好模型分布。第三加載完立刻確認一下顯存狀態理論上剛加載完應該在 4GB 以內nvidia-smi如果你看到MiB已經飆到 10GB八成是torch_dtype沒設置成 BF16或者量化沒有真正生效。3.2 訓練前關鍵動作關緩存、開梯度檢查點、掛 LoRA很多人到這一步就直接建Trainer了結果訓練一開始就 OOM。真正穩妥的做法是先做三件事# 1. 關閉 KV cache。訓練時不需要緩存歷史 token 的鍵值對開著白占顯存 model.config.use_cache False # 2. 開啟梯度檢查點。犧牲約 20%~30% 訓練速度換來激活顯存從幾十 GB 降到幾 GB model.gradient_checkpointing_enable() # 3. 4bit 量化模型反向傳播需要輸入梯度標識 from peft import prepare_model_for_kbit_training model prepare_model_for_kbit_training(model)梯度檢查點值得專門說一下。它的原理是不保存每一層 Transformer 的中間激活值反向傳播時再臨時重算一次。相當于拿“多跑一遍前向”的時間換顯存效果立竿見影。實測在 7B QLoRA 配置下開啟后顯存峰值能降 4~8GB。我見過有人不開它硬調 batch size結果 batch1 都能 OOM開了之后 batch4 穩如老狗。這三步做完再掛 LoRAfrom peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha16, target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 輸出trainable params: 7,340,032 || all params: 6,745,904,128 || trainable%: 0.1088r8意味著每個旁路矩陣的秩是 8這是省顯存的核心參數。理論上 r 可以調成 16、32模型表達能力會強一些但可訓練參數、顯存和訓練時間都會跟著漲。在 7B 模型上我個人的經驗是 r8 到 r16 效果好繼續往上性價比下降明顯更容易過擬合。3.3 TrainingArguments 參數組合batch1 配梯度累積32GB 穩妥不 OOM來看最關鍵的部分TrainingArguments 怎么配。我直接貼一份在 32GB 卡上驗證過的參數from transformers import TrainingArguments training_args TrainingArguments( output_dir./lora-7b, per_device_train_batch_size1, gradient_accumulation_steps16, # 梯度累積等效 batch size 16 gradient_checkpointingTrue, # 保持開啟 optimpaged_adamw_8bit, # 8bit 優化器 分頁機制 learning_rate2e-4, lr_scheduler_typecosine, warmup_ratio0.03, bf16True, # 32GB 卡基本都支持 BF16 logging_steps1, save_steps200, max_steps1000, report_tonone, )為什么要per_device_train_batch_size1在 QLoRA 下batch1 和 batch4 的模型權重、優化器狀態完全一樣區別只在激活值和 KV cache。per_device_train_batch_size1gradient_accumulation_steps16能讓激活值最小同時等效 batch size 依然是 16訓練穩定性和收斂質量都有保障。optimpaged_adamw_8bit是另一個隱藏加分項。8bit 優化器把每個參數的 fp32 狀態壓縮到 1 字節對 LoRA 這種本來就只有幾百萬可訓練參數的場景來說省顯存效果有限但幾乎不損失精度。paged 機制還能在顯存不夠時把優化器狀態臨時換到 CPU相當于一層保險絲。這里我提醒一個容易踩的坑當gradient_checkpointingTrue時per_device_train_batch_size建議不要先拉高。你可以在第一次運行時把 batch size 從 1 往上加每加一次看一次nvidia-smi的顯存峰值。加到出現 OOM再退回一檔。這套“漸進試探法”比直接抄別人的 batch size 靠譜得多。3.4 訓練腳本還有一個“顯存黑洞”模型合并前的重載很多人在微調完成后做 LoRA 權重合并merge莫名其妙又 OOM。原因是你用 4bit 加載的基礎模型權重只有 3.5GB但合并以后轉成 BF16 保存全量權重時又要一次性在顯存里組裝出一個 14GB 的全精度模型。如果同時加載了 tokenizer、原有模型和 adapter32GB 很容易再次爆掉。我的做法是訓練完不直接在 4bit 模型上調merge_and_unload()而是先save_pretrained保存 LoRA adapter然后單獨用一段瘦身腳本重新加載原模型再做合并。顯存壓力小很多過程也更可控。細節會在下一節的排查表里再提。4. 常見 OOM 問題與排查技巧實錄訓練腳本跑起來只是開始真正讓人血壓飆升的是那些奇奇怪怪的 OOM。這一節整理我在調試過程中遇到的高頻問題每條都有對應的排查思路。4.1 “CUDA out of memory”報錯信息怎么看最常見的報錯長這樣torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB (GPU 0; 31.75 GiB total capacity; 28.47 GiB already allocated; 947.55 MiB free; 1.12 GiB reserved in total by PyTorch)關鍵信息不是第一行“Tried to allocate”而是那串數字比例。如果你看到“28.47 GiB already allocated”說明顯存已經快滿了此時果斷降 batch size 或者開梯度檢查點不用猶豫。但還有一種更迷惑的情況nvidia-smi里顯存占用并不高PyTorch 卻報 OOM。這通常是顯存碎片化——模型在不同階段反復申請和釋放顯存導致物理上有空余但是不連續分配不出一塊足夠大的連續塊。遇到這種碎片化問題我建議先設置一個環境變量再跑export PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True實測這個選項能顯著緩解顯存碎片問題讓 PyTorch 更靈活地分段分配顯存。代價是顯存使用率會稍微上升整體更接近物理上限但對于 32GB 單卡場景利遠大于弊。4.2 調試期的顯存觀測三板斧排查 OOM 不能只靠猜。我習慣在訓練前、訓練中、報錯后分別做三件事剛加載完模型先看一眼nvidia-smi確認量化是否生效。如果模型加載完顯存就超過 8GB大概率是 BitsAndBytes 沒有正確開啟。訓練中每幾步記錄一次顯存峰值。可以用torch.cuda.max_memory_allocated()在訓練循環里打印或者直接用nvidia-smi dmon -f 1實時盯顯存曲線。一旦 OOM先從報錯堆棧定位是哪個 stage 出的問題。是前向傳播時炸還是反向傳播時炸這兩者的解決思路完全不同。這里我提供一個簡單實用的監控小工具函數def print_gpu_usage(): allocated torch.cuda.memory_allocated() / 1024**3 reserved torch.cuda.memory_reserved() / 1024**3 print(f[GPU] allocated{allocated:.2f} GiB, reserved{reserved:.2f} GiB)在訓練循環里每個 step 后調一次就能看出顯存是在緩慢爬升還是突發上漲。如果是緩爬通常是有張量沒被釋放檢查是不是在循環里把某些張量不小心保留了下來如果是突漲那大概率是 batch 或序列長度觸頂了。4.3 常見故障速查表現象核心原因排查順序模型加載完就 OOM4bit 量化沒生效 / 隱式轉成 fp32檢查 BitsAndBytesConfig檢查nvidia-smi模型占用訓練一開始前向就 OOM激活值爆炸 / 序列過長開梯度檢查點降 batch size截斷序列長度訓練中途反向OOM反向重新計算的顯存峰值超過上限換paged_adamw_8bit繼續降 batch減少 LoRA rank顯存看著充足但報 OOM顯存碎片化設置PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True合并 LoRA 權重時 OOM重建全量 BF16 權重開銷太大單獨寫合并腳本先釋放訓練狀態多卡微調時某張卡 OOM單卡批次不均 / 梯度同步緩沖檢查device_map確認每張卡的實際 batch 分配我遇到過最典型的一個場景是“加載模型只有 4GB一進訓練直接 OOM”。排查下來發現是use_cacheTrue沒關——訓練階段的 KV cache 和推理不同隨著序列長度增長會持續膨脹把顯存活活拖垮。這提醒我們訓練前model.config.use_cache False幾乎是個必做動作。4.4 終極兜底不是所有任務都非得上 7B 不可如果在 32GB 卡上反復優化還是溢出我的最后建議是先換小模型跑通全流程。比如拿 1.5B 或 3B 的模型用同一套 QLoRA 配置快速驗證數據質量、LoRA rank、學習率等關鍵參數確認沒問題后再切回 7B 正式訓練。這個習慣能幫你省下大量調試時間也避免“OOM→改配置→重跑→再 OOM”的循環。給學生們做演示也是同理。實際教學場景里“把流程跑通、把效果做出來”比“硬塞進一個更大的模型”重要得多。用 3B 模型 QLoRA 在單卡上跑教學 demo速度和成功率都會高很多等到演示穩定了再往 7B 上遷移體驗完全不一樣。寫在最后的個人體會我在多次給實驗室配置微調環境時總結下來一句話QLoRA 梯度檢查點 batch1 梯度累積是 32GB 單卡上最不容易出錯的組合。如果你還想把顯存余量再拉開一些優先考慮把基礎模型換成更小的版本而不是繼續堆 batch size。另外強烈建議做幾組對照實驗——分別不開梯度檢查點、不開 4bit、開 4bit 不疊梯度累積用顯存監控腳本對比峰值你會對顯存分配的直覺越來越準確。這個過程本身就是最好的學習材料比背任何調參公式都有用。