简介《DeepSeek 本地化部署实现私有化训练医疗数据实战方案》面向医疗信息化团队、深度学习算法工程师及需要处理敏感病历数据的技术人员聚焦在受控本地环境中完成DeepSeek部署与医疗数据训练兼顾隐私合规与模型落地。资料以单份PDF呈现共1个文件、约228KB篇幅紧凑便于按需查阅和随项目传阅。内容从医疗数据私有化部署背景与法规要求切入覆盖服务器、GPU、内存与存储等硬件选型以及Ubuntu、Anaconda、PyTorch、MySQL等软件环境配置随后展开病历数据收集、清洗、标注与训练集验证集测试集划分并说明模型选择、Transformers加载和训练流程中的关键代码片段同时强调数据加密、访问权限控制等合规要点。已有1347人学习适合希望从环境搭建走到训练实施的读者作为实战参考也可用于梳理医疗数据私有化训练的整体框架与实施细节。1. 先把手上的残局说清楚DeepSeek 本地化部署到底装的是什么上周帮一个做医疗信息化的团队收拾残局机器是新的四张 80GB 卡插着DeepSeek 权重也拷进来了结果torch.cuda.is_available()返回 False白洗了三天的医疗数据。问题不在模型而在他们先用 pip 默认源装了 PyTorch再回头补 CUDA 11.8版本早就对不上了。本地化部署这件事顺序错一步就得推倒重来。这套环境适合手里压着敏感病历、必须在受控内网里把深度学习的训练和推理全跑完的团队也适合想在自己机器上把 DeepSeek 私有化跑通一遍的开发者。下面按我实际装过的顺序写从硬件基线到验收脚本一步不省。2. 硬件与系统基线Ubuntu Server 22.04 装到 nvidia-smi 出画面2.1 四条不能省的配置底线私有化训练医疗数据和跑一个在线 demo 是两码事。在线 demo 一张卡、32GB 内存就能转起来本地化训练要同时兼顾数据吞吐、模型权重驻留和检查点落盘任何一项拖后腿表现出来的现象都是“GPU 利用率上不去”然后你会去调各种参数最后发现调错了地方。我一般先把下面这张表拉齐再动手装系统。部件这套环境的取值底线要求低于底线时的表现CPU双路 32 核 64 线程主频 2.3GHz单路 16 核起分词和数据预处理成为瓶颈GPU 长时间空转GPU4 张 80GB 显存 PCIe 卡单卡 24GB 起7B 级微调模型加载阶段就 OOM连推理都起不来内存512GB DDR4 3200MHz不小于显存总和的 2 倍多进程 DataLoader 时进程被系统直接杀掉系统盘2 块 960GB 企业级 SSD 组 RAID 1480GB 起conda 环境和 CUDA 包几个月就撑满数据盘4 块 4TB SAS 组 RAID 5另配 2TB NVMe 作缓存按病历文本量估训练时读样本打满 IOstep 时间忽快忽慢网络万兆内网千兆起多机分发权重和数据集时等到怀疑人生CPU 这一项最容易被忽略。很多人觉得训练全靠 GPUCPU 随便配个 8 核就行实际跑起来才发现分词是在 CPU 上做的尤其是病历这种长文本一条记录几百上千字tokenizer()调用在 DataLoader 的 worker 里跑核数不够就是 GPU 干等着。双路 32 核 64 线程是我试下来比较舒服的档位再低就得靠缓存 tokenize 结果来弥补。内存和显存的关系值得单独说一句。显存决定能不能装下模型内存决定能不能同时开几个 worker 去喂数据。经验值是内存不小于显存总和的两倍四张 80GB 就是 320GB 显存配 512GB 内存刚好留出余量给系统和数据库。低于这个数num_workers只能开到 2训练速度会掉得很难看。存储分层这一条属于“当时嫌麻烦、后面天天庆幸”的类型。系统盘只放操作系统和驱动conda 环境、模型权重、病历数据、训练检查点分别挂在不同目录下。这样做的好处是重装系统时数据不动而且检查点和数据盘分开之后训练时写 checkpoint 不会把读数据的 IO 抢掉。2.2 系统更新与 NVIDIA 驱动安装系统装的是 Ubuntu Server 22.04 LTS安装时勾选 OpenSSH Server 就行桌面组件一概不要省下来的内存都是给训练的。装完之后第一件事是把源更新掉并且顺手把内核锁定避免某次自动升级把驱动内核模块搞挂。# 1. 更新系统先把包索引拉到最新 sudo apt update sudo apt upgrade -y # 2. 装上编译和诊断常用工具 sudo apt install -y build-essential dkms linux-headers-$(uname -r) \ htop iotop nvtop tmux git curl wget # 3. 查看系统识别到的显卡和建议驱动版本 ubuntu-drivers devices # 4. 安装 NVIDIA 驱动服务器版驱动525/535 都可以跟 CUDA 11.8 兼容 sudo apt install -y nvidia-driver-535-server # 5. 重启让内核模块生效 sudo reboot这段脚本里每一步都有它的用途。linux-headers-$(uname -r)必须和当前运行内核严格对应DKMS 编译驱动模块时要用到装错了版本重启后nvidia-smi就会报 “No devices were found”。ubuntu-drivers devices会列出系统推荐的驱动版本一般选recommended那一行就行不必追最新。驱动版本和 CUDA 版本不是一一对应的这点新手很容易搞混。驱动只提供向后兼容的运行时接口535 的驱动既能跑 CUDA 12.2 的包也能跑 CUDA 11.8 的包反过来525 的驱动跑不了 12.x 的部分特性。所以装驱动的原则是“够用且别太老”535 这个版本在我们这套环境里跑了很久没出过问题。重启之后立刻验证别急着往下走# 列出所有显卡确认 4 张卡都被识别 nvidia-smi -L # 看拓扑确认 PCIe 通道和 NUMA 绑定关系 nvidia-smi topo -m # 持续观察显存和利用率训练时开着它比什么都直观 nvtopnvidia-smi -L的输出应该是四行每行一张卡带 UUID。如果只出来一两张先去看lspci | grep -i nvidia是不是四张都在硬件层面就少卡的话多半是 PCIe 供电或者插槽问题跟软件无关。nvidia-smi topo -m输出的矩阵里卡与卡之间是PHB还是SYS决定了你要不要开 NVLink 相关的优化PCIe 卡基本都是PHB跨 NUMA 节点的那几张会在训练时慢一拍后面绑核的时候要用到这份信息。提示服务器 BIOS 里如果有 Secure Boot装第三方驱动时需要走一次 MOK 密钥注册流程中间会弹蓝色界面要求设置密码重启后再输一遍。忘了这一步驱动模块不会加载现象和没装一模一样。2.3 存储分层把模型、数据、检查点分开放数据盘组好 RAID 之后我习惯按用途切成几个挂载点而不是全部塞进/data一个大目录。原因很实际训练时 checkpoint 会瞬间写出几十 GB和正在读的病历数据抢同一块盘的 IOloss 曲线会莫名其妙地抖。# 假设 md0 是数据盘 RAID5nvme0n1 是缓存盘 sudo mkfs.ext4 /dev/md0 sudo mkfs.ext4 /dev/nvme0n1 # 建挂载点 sudo mkdir -p /data/medical /data/models /data/ckpt /data/cache # 写进 fstab开机自动挂载noatime 减少不必要的写 echo /dev/md0 /data/medical ext4 defaults,noatime 0 2 | sudo tee -a /etc/fstab echo /dev/nvme0n1 /data/cache ext4 defaults,noatime 0 2 | sudo tee -a /etc/fstab sudo mount -anoatime这个参数值得单独提。默认的atime会在每次读文件时更新访问时间训练时反复读同一个数据集文件等于凭空多出一堆写操作。换成noatime之后 IO 压力能降一截代价只是ls -lu看不到真实访问时间对我们这种场景完全无所谓。切分完之后建议这样分工/data/medical放原始病历 CSV 和清洗后的中间文件/data/models放 DeepSeek 权重和分词器/data/ckpt放 Trainer 输出的检查点/data/cache放 HuggingFace 的下载缓存和 tokenize 后的缓存。缓存目录指到 NVMe 上是这套配置里性价比最高的一步第二次训练时省下的时间非常可观。3. 软件栈Anaconda 建 deepseek_medical 环境CUDA 11.8 配 PyTorch3.1 Anaconda 安装与虚拟环境的位置Anaconda 装本身没什么难度坑都在“装完之后它默认往哪儿写”。默认路径是~/anaconda3环境目录在~/anaconda3/envs全在系统盘上。一个 PyTorch Transformers 的环境轻松吃掉 15GB再装几个数据集处理库系统盘那 960GB 看着大两三个环境就下去一小半。# 下载并安装2023.09 这个版本对应 Python 3.11 的 base不影响我们建 3.10 环境 wget https://repo.anaconda.com/archive/Anaconda3-2023.09-0-Linux-x86_64.sh bash Anaconda3-2023.09-0-Linux-x86_64.sh -b -p /opt/anaconda3 # 初始化 shell让 conda 命令可用 /opt/anaconda3/bin/conda init bash source ~/.bashrc # 关掉每次开终端自动进 base 环境减少误操作 conda config --set auto_activate_base false # 把环境和包缓存指到大盘上 conda config --add envs_dirs /data/cache/conda/envs conda config --add pkgs_dirs /data/cache/conda/pkgs # 建虚拟环境Python 版本锁 3.10 conda create -n deepseek_medical python3.10 -y conda activate deepseek_medical-b -p /opt/anaconda3这两个参数是把安装路径直接指定死跳过交互式确认。有人喜欢装在~下个人机器无所谓服务器上我还是习惯放/opt多个用户共用一份权限也好管。conda config --add envs_dirs的顺序有讲究它会把新路径插到列表前面所以之后再conda create建的环境都会落到/data/cache/conda/envs下不会再把系统盘填满。auto_activate_base设成 false 是个小习惯。base 环境里那套 Python 版本和依赖一旦被误用去做 pip 安装后面排查起来很痛苦——你会发现自己明明激活了deepseek_medicalwhich python却指向 base。关掉自动激活之后每次都要显式conda activate反而少出错。注意组织内使用 Anaconda 之前先确认一下许可条款是否覆盖你的使用场景有些团队会换成 conda-forge 的 miniforge功能上对我们这套环境没有差别。3.2 CUDA 11.8 与 PyTorch 的版本对应关系这一节是整套环境里最容易出问题的地方。结论先放这儿PyTorch 的 pip wheel 自带 CUDA 运行时你不需要单独apt install cuda只需要保证驱动版本够新。装错了 wheel就会出现开头那个场景——nvidia-smi一切正常torch.cuda.is_available()返回 False。组件取值说明驱动535-server支持 CUDA 11.8 至 12.2CUDA Runtime11.8wheel 内置来自 PyTorch 官方 cu118 源PyTorch2.x 系列 cu118 版本与 Python 3.10 匹配Python3.10Transformers 生态兼容性最好# 激活环境 conda activate deepseek_medical # 关键从 cu118 专用源安装不要用默认源 pip install torch torchvision torchaudio \ --index-url https://download.pytorch.org/whl/cu118 # 装训练侧依赖 pip install transformers datasets accelerate scikit-learn pandas \ openpyxl mysql-connector-python cryptography seaborn matplotlib--index-url这个参数是整段的核心。不加它pip 会去 PyPI 拉一个 CPU 版本的 torch装完能 import、能跑就是不用显卡而且不会有任何报错提示。这种“静默降级”是本地化部署里最费时间的坑之一我第一次遇到时查了两个小时驱动。装完立刻验证三件事一条都不能省python -c import torch print(torch:, torch.__version__) # 期望 2.x.xcu118 print(cuda:, torch.version.cuda) # 期望 11.8 print(可用卡数:, torch.cuda.device_count()) # 期望 4 print(当前卡:, torch.cuda.get_device_name(0)) 输出的版本号后面必须带cu118后缀没有这个后缀就是装成 CPU 版了。device_count()返回 4 才算驱动和框架真正接上了。如果这里只有 1去检查CUDA_VISIBLE_DEVICES环境变量是不是被别人设过这个变量一旦设了程序只能看到指定的那几张卡。3.3 MySQL 装库与最小权限账号病历数据的结构化部分落在 MySQL 里主要用来存清洗后的记录、标注结果和训练产出的预测表。装的时候有两个细节不能想当然字符集和账号的主机名。# 安装并做基础安全设置 sudo apt install -y mysql-server sudo mysql_secure_installation # 建库建表字符集一定要显式指定 utf8mb4 sudo mysql SQL CREATE DATABASE medical_data DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; -- 注意这里写的是 127.0.0.1不是 localhost CREATE USER medical_user127.0.0.1 IDENTIFIED BY 换成你的强口令; -- 只给需要的权限不要 GRANT ALL GRANT SELECT, INSERT, UPDATE ON medical_data.* TO medical_user127.0.0.1; FLUSH PRIVILEGES; SQL # 只监听本机回环病历数据不出这台机器 sudo sed -i s/^bind-address.*/bind-address 127.0.0.1/ /etc/mysql/mysql.conf.d/mysqld.cnf sudo systemctl restart mysqlutf8mb4这一项不能省。病历里的症状描述经常有生僻字、全角符号用默认的latin1或者老的utf8插入时会静默截断你会在训练时发现某些样本的文本莫名其妙少了一截回过头查数据源又一切正常。这种问题排查起来极其难受不如建库时就定死。账号主机名写127.0.0.1而不是localhost是因为 MySQL 把这两个当作不同来源。localhost走 Unix socket127.0.0.1走 TCP。Python 的mysql-connector-python默认走 TCP如果只建了medical_userlocalhost程序连过来会报 Access denied。这个坑我踩过一次日志里那行报错看半天才反应过来。权限上只给SELECT/INSERT/UPDATE不给DROP和DELETE。训练脚本按理说只读数据但谁也不能保证某个清洗脚本不会手滑。少给一个权限就少一次后悔的机会。3.4 依赖锁定把当前能跑的状态存下来环境装完能跑不代表三个月后重装还能跑。Transformers 的版本迭代很快某个小版本改了默认的 padding 行为你的模型输出就会整体偏移。所以装完当天就把环境导出一份。# 导出 conda 层的环境含 python 版本和 channel 信息 conda env export --no-builds /data/models/environment.yml # 单独锁一份 pip 层的精确版本用来兜底 pip freeze /data/models/requirements.lock # 恢复时优先用 lock 文件比 environment.yml 更精确 # pip install -r /data/models/requirements.lock--no-builds是去掉包构建号让同一份 yml 能在不同机器上复用。真正重装时我一般以requirements.lock为准因为 conda 解决依赖时会重新算一遍版本组合算出来的结果可能和当初不一样。这份 lock 文件和模型权重放在一起属于交付物的一部分。4. 数据管道到训练病历 CSV 一路走到 Trainer.train()4.1 清洗dropna 不是一刀切原始病历数据从业务系统导出来缺失、重复、格式混乱是常态。很多人上来就data.dropna()一行代码看着干净利落实际把大量有价值的长文本样本也干掉了——病历里“过敏史”字段空着很正常但那条记录的症状描述和诊断结论可能是完整的。import pandas as pd import numpy as np # 读取原始病历数据 data pd.read_csv(/data/medical/medical_records.csv, dtypestr) # 1. 关键字段缺失才丢症状描述为空这条样本就没意义了 data data.dropna(subset[symptoms, diagnosis]) # 2. 非关键字段按缺失率处理超过 60% 缺失的整列丢掉 threshold len(data) * 0.6 data data.dropna(axis1, threshthreshold) # 3. 剩余缺失值统一填空串避免后续 tokenizer 报类型错误 data data.fillna() # 4. 去重按症状诊断的组合判断而不是整行比对 data data.drop_duplicates(subset[symptoms, diagnosis], keepfirst) # 5. 长度过滤少于 10 个字的症状描述信息量太低 data data[data[symptoms].str.len() 10] # 6. 数值字段做合理性裁剪血压年龄这类超出医学常识的直接置空 data.loc[(data[age].astype(float) 120), age] np.nan data.to_csv(/data/medical/cleaned_medical_records.csv, indexFalse) print(f清洗后样本数: {len(data)})第 4 步按组合去重是这套管道里比较关键的一个取舍。整行去重只能去掉完全一样的记录但实际数据里同一条病历可能因为导出时间不同而多出一个时间戳字段整行比对就失效了。按业务主键组合去重更贴近真实需求。第 6 步属于“看着多余、事后感谢自己”的操作。年龄 999 岁、血压 500 这种脏数据在真实系统里一定有它们会让特征分布严重偏移进而影响模型对数值特征的判断。清洗阶段处理掉比训练完之后对着一堆异常预测结果反推要省事得多。4.2 划分 7:1:2 与标签编码7:1:2 这个比例是从项目里沿用下来的训练集足够大验证集够做 early stopping测试集够撑起一次可信的评估。划分顺序是先切出 20% 做测试集再从剩下的 80% 里切 12.5% 做验证集这样最终比例正好是 70:10:20。from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder import pandas as pd data pd.read_csv(/data/medical/cleaned_medical_records.csv) # 标签必须先编码成 0..N-1 的连续整数 le LabelEncoder() data[label] le.fit_transform(data[diagnosis]) num_labels len(le.classes_) print(类别数:, num_labels) print(映射关系:, dict(zip(le.classes_, range(num_labels)))) # 先切 20% 做测试集stratify 保证各类别比例一致 train_val, test train_test_split( data, test_size0.2, random_state42, stratifydata[label] ) # 从剩余 80% 中切 12.5%等价于全量的 10% train, val train_test_split( train_val, test_size0.125, random_state42, stratifytrain_val[label] )stratify这个参数在医疗数据上几乎是必须的。疾病类别天然不平衡罕见病的样本可能只占千分之几不分层的话随机切一次就有可能把某个类别全部切进测试集训练时模型压根没见过这个标签。random_state42固定住之后任何人重跑都能拿到同一份划分出了问题可以复现。LabelEncoder这一步不能省。AutoModelForSequenceClassification输出的 logits 维度是num_labels索引是 0 到 N-1。如果直接用字符串标签或者不连续的整数训练不会报错但算出来的准确率会全错——因为argmax(-1)出来的是位置索引和你的原始标签对不上号。这个错误非常隐蔽loss 曲线看着正常指标一算全是乱的。编码器对象一定要保存下来推理时反过来用import joblib joblib.dump(le, /data/models/label_encoder.pkl)4.3 分词与 MedicalDataset 封装分词这步有三个参数直接决定训练效果truncation、max_length、padding。默认的truncationTrue会按模型支持的最大长度截断很多模型是 512而一份完整的病历文本轻松超过这个数。截掉的往往正是诊断结论所在的尾部模型学到的东西就残缺了。from transformers import AutoTokenizer import torch tokenizer AutoTokenizer.from_pretrained(/data/models/deepseek-base) # 显式指定 max_length别让默认值替你决定 train_encodings tokenizer( train[symptoms].tolist(), truncationTrue, max_length1024, paddingFalse, # 动态 padding 交给 DataCollator省显存 return_tensorsNone ) val_encodings tokenizer( val[symptoms].tolist(), truncationTrue, max_length1024, paddingFalse ) class MedicalDataset(torch.utils.data.Dataset): 把编码结果包成 PyTorch 能迭代的数据集 def __init__(self, encodings, labels): self.encodings encodings self.labels labels def __getitem__(self, idx): item {k: torch.tensor(v[idx]) for k, v in self.encodings.items()} item[labels] torch.tensor(self.labels[idx]) return item def __len__(self): return len(self.labels) train_dataset MedicalDataset(train_encodings, train[label].tolist()) val_dataset MedicalDataset(val_encodings, val[label].tolist())paddingFalse配合DataCollatorWithPadding是这套配置里比较讲究的一处。如果在这里直接paddingmax_length每一条样本都会被补齐到 1024 长度一个 batch 里 90% 都是填充 token显存浪费得离谱。改成动态 padding 之后每个 batch 只补齐到该 batch 内最长的那条显存占用能降三到四成训练速度也跟着上来。max_length1024也是权衡的结果。调到 2048 会覆盖更多完整病历但显存消耗是平方级增长压到 512 虽然快但会切掉太多信息。1024 在我们这批数据上是个比较合适的点建议你拿到自己的数据后先统计一下 token 长度的 95 分位数按那个数来定。4.4 加载 DeepSeek 权重与 TrainingArguments 逐项取舍模型加载这一步有两个参数需要明确num_labels和权重的本地路径。路径一定要用绝对路径相对路径在trainer.train()里被调用时工作目录可能已经变了报错信息还特别含糊。from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, DataCollatorWithPadding ) MODEL_PATH /data/models/deepseek-base tokenizer AutoTokenizer.from_pretrained(MODEL_PATH) model AutoModelForSequenceClassification.from_pretrained( MODEL_PATH, num_labelsnum_labels, # 必须和 LabelEncoder 的类别数一致 ignore_mismatched_sizesTrue # 分类头尺寸不符时重建不报错 ) training_args TrainingArguments( output_dir/data/ckpt/deepseek-medical, num_train_epochs3, per_device_train_batch_size4, # 单卡 4先保守 per_device_eval_batch_size16, gradient_accumulation_steps4, # 等效 batch 4 * 4卡 * 4 64 learning_rate2e-5, warmup_ratio0.1, # 用比例比用绝对步数更稳 weight_decay0.01, bf16True, # A100 上首选 bf16不用梯度缩放 logging_dir/data/ckpt/logs, logging_steps10, evaluation_strategyepoch, save_strategyepoch, save_total_limit2, # 只留最近两个检查点省磁盘 load_best_model_at_endTrue, metric_for_best_modelf1, dataloader_num_workers8, report_tonone ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, tokenizertokenizer, data_collatorDataCollatorWithPadding(tokenizer) )per_device_train_batch_size4加上gradient_accumulation_steps4再乘上 4 张卡等效 batch size 是 64。这个组合是我在 80GB 卡上跑 7B 级模型微调的常用起点。有人喜欢直接把per_device_train_batch_size拉到 16结果第一步就 OOM然后去怀疑驱动、怀疑卡坏了。实际上大模型微调的显存占用里激活值占大头batch size 每翻一倍显存就多吃一大块梯度累积就是用来换这个空间的。bf16True和fp16True之间40GB 以上的卡优先选 bf16。bf16 的数值范围跟 fp32 一样不需要 loss scaling训练过程中不会出现梯度下溢变成 0 的情况。fp16 在老卡上省显存更多但需要配GradScaler稳定性上差一截。warmup_ratio0.1比源码里的warmup_steps500更通用。500 步在十万级样本上只占很小比例但在几千条病历的小数据集上可能占了一半的训练步数学习率还没升上去训练就结束了。用比例就能自动适配不同规模的数据集。save_total_limit2是个容易被忽略的细节。每轮都存一个 7B 模型的检查点三个 epoch 就是几十 GB磁盘很快就满了然后训练在第 4 个 epoch 挂掉。限制只留最近两个配合load_best_model_at_endTrue加载最优权重够用。4.5 启动训练与量化导出import numpy as np from sklearn.metrics import accuracy_score, recall_score, f1_score def compute_metrics(eval_pred): logits, labels eval_pred preds np.argmax(logits, axis-1) return { accuracy: accuracy_score(labels, preds), recall: recall_score(labels, preds, averagemacro), f1: f1_score(labels, preds, averagemacro) } trainer.compute_metrics compute_metrics trainer.train() # 保存最终模型和分词器推理侧要用同一份 trainer.save_model(/data/models/deepseek-medical-final) tokenizer.save_pretrained(/data/models/deepseek-medical-final)指标用averagemacro而不是默认的binary或weighted。医疗场景下每个疾病类别的重要性接近macro 会对每个类别单独算再平均少数类表现差会立刻反映在数字上weighted 按样本数加权罕见病被预测错几乎不影响总分指标会显得虚高。量化这一步要看清楚它到底作用在哪儿。PyTorch 的动态量化只能把nn.Linear转成 int8而且主要收益在 CPU 推理上import torch # 动态量化把 Linear 层权重转 int8 quantized_model torch.quantization.quantize_dynamic( model.cpu(), {torch.nn.Linear}, dtypetorch.qint8 ) # 保存量化后的模型 torch.save(quantized_model.state_dict(), /data/models/deepseek-medical-int8.pt)注意这里有个model.cpu()。量化后的模型是给 CPU 推理用的想在 GPU 上跑反而会报错。如果目标是降低 GPU 显存占用动态量化帮不上忙应该走 bitsandbytes 的 4bit/8bit 量化加载路线。这两者解决的问题不一样混用只会浪费时间。5. 避坑与排查五个装完跑不起来的现场5.1 nvidia-smi 正常但 torch.cuda.is_available() 为 False现象驱动装好了nvidia-smi能列出四张卡显存信息也有。进 Python 里一执行torch.cuda.is_available()返回 Falsetorch.version.cuda是 None。原因装了 CPU 版本的 PyTorch。pip install torch不加任何 index 参数时默认从 PyPI 拉的是 CPU wheel。这个 wheel 功能完整能 import、能训练就是不用显卡而且不会抛任何异常。解决先pip uninstall -y torch torchvision torchaudio卸干净再用--index-url https://download.pytorch.org/whl/cu118重装装完确认torch.__version__带cu118后缀。顺带检查一下pip config list看是不是有人在全局配了默认 index那种情况下每次装包都会走错源。5.2 新开终端 conda activate 提示 command not found现象当前窗口里conda activate deepseek_medical一切正常关掉重开一个终端报CommandNotFoundError: Your shell has not been properly configured。原因conda init只写了当前用户的.bashrc如果用的是 sudo 切过去的用户、或者脚本里用的是sh而不是bash初始化脚本根本不会加载。解决确认~/.bashrc里有 conda 的初始化块没有就重新跑一次/opt/anaconda3/bin/conda init bash。写自动化脚本时更可靠的做法是直接调绝对路径的解释器/data/cache/conda/envs/deepseek_medical/bin/python train.py绕开 shell 初始化这一层少一个变量就少一种失败可能。5.3 训练第一步就 CUDA out of memory现象trainer.train()刚启动还没打印第一个 loss 就抛 OOM显存监控显示某一瞬间打满。原因per_device_train_batch_size给大了或者max_length设得太长。大模型微调时激活值占用的显存远大于权重本身batch size 翻倍显存基本也翻倍。解决把 batch size 降到 2 或 4用gradient_accumulation_steps把等效 batch 补回来。同时把max_length从 2048 降到 1024。还不行就开gradient_checkpointingTrue用时间换空间速度掉 20% 左右但能跑起来。排查时用torch.cuda.max_memory_allocated()打出峰值比盯着nvidia-smi猜要准。5.4 量化之后显存没降、推理速度也没变现象按文档跑了quantize_dynamic模型文件确实小了但推理时显存占用几乎没变化速度也没提升。原因动态量化只作用于nn.Linear的权重而且优化目标是 CPU 推理。模型还在 GPU 上跑或者瓶颈根本不在 Linear 层而在 attention 计算上量化自然看不到效果。解决先明确目标。要省 GPU 显存改用 bitsandbytes 的 4bit 量化加载要提 CPU 推理速度确保model.cpu()之后再做量化并且用torch.set_num_threads()把线程数调满。量化前先 profile 一下各层耗时别对着不是瓶颈的地方使劲。5.5 分词截断后验证集指标虚高现象训练集 loss 降到很低验证集 F1 冲到 0.95 以上但拿真实新病历去预测效果惨不忍睹。原因训练集和验证集来自同一批数据划分时没有按患者 ID 分组。同一个患者的多次就诊记录同时出现在训练集和验证集里模型实际上是在“背答案”。加上max_length截断短样本保留了完整信息长样本被砍掉一半指标分布被扭曲。解决划分时用GroupShuffleSplit按患者 ID 分组保证同一个人只出现在一个集合里。同时统计 token 长度的分布把max_length定在 95 分位数以上实在超长的样本做分段或者摘要预处理别硬截。6. 装完怎么验证一条从显卡到混淆矩阵的验收链装完不算完得有一条能一次跑通的验收链把硬件、框架、数据、模型四层串起来。我一般按下面这个顺序走任何一环不对就停下来修不带着问题往下走。验收项执行方式期望结果显卡识别nvidia-smi -L四行卡UUID 齐全框架接卡torch.cuda.device_count()返回 4显存可用torch.zeros(1, devicecuda:0)不报错数据通路小样本 DataLoader 迭代一个 batchshape 符合预期训练通路32 条样本过拟合loss 降到 0.1 以下指标通路compute_metrics输出F1 在 0 到 1 之间前四项用一行脚本就能扫完source /opt/anaconda3/etc/profile.d/conda.sh conda activate deepseek_medical python -c import torch assert torch.cuda.is_available(), CUDA 不可用先查 torch 是不是 cu118 版本 assert torch.cuda.device_count() 4, f只识别到 {torch.cuda.device_count()} 张卡 x torch.zeros(1024, 1024, devicecuda:0) torch.zeros(1024, 1024, devicecuda:0) torch.cuda.synchronize() print(硬件与框架层验收通过) 真正有价值的是第五项——32 条样本过拟合测试。取 32 条病历用同样的模型和参数训 30 个 epoch正常情况下 loss 应该一路降到 0.1 以下训练集准确率接近 100%。如果 loss 卡在某个值不动说明数据管道、标签映射或者学习率配置里有问题这时候排查范围就缩小到很小一块比在几万条数据上跑一天再发现问题要划算得多。# 过拟合测试只取 32 条模型应该能完全记住 small train.iloc[:32] small_enc tokenizer(small[symptoms].tolist(), truncationTrue, max_length512, paddingTrue) small_ds MedicalDataset(small_enc, small[label].tolist()) test_args TrainingArguments( output_dir/tmp/overfit, num_train_epochs30, per_device_train_batch_size8, learning_rate5e-5, logging_steps5, report_tonone, save_strategyno ) Trainer(modelmodel, argstest_args, train_datasetsmall_ds).train()这一步跑完链路就算通了。最后再做一次保存-加载一致性检查把save_pretrained出来的模型重新from_pretrained加载用同一条测试样本推理两次argmax出来的标签必须一致。不一致说明保存时有层没写进去多半是自定义 head 没注册到模型里。指标侧别忘了混淆矩阵光看准确率在类别不均衡时极易被误导import seaborn as sns import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix preds trainer.predict(test_dataset).predictions.argmax(-1) cm confusion_matrix(test[label], preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(/data/models/confusion_matrix.png, dpi150)矩阵里非对角线上那些集中的方块才是真正需要回去补数据的地方。准确率 0.9 而某个类别全部预测错这种情况在医疗数据上太常见了。从那以后我每次装完这套环境不管多赶时间都强制先跑一遍 32 条样本的过拟合测试——它花不到五分钟但能挡掉后面至少两天的瞎猜。整套硬件清单、conda 环境导出文件、训练脚本和这份排查清单我都整理在一起了需要按图索骥复现一遍的话可以拿去对照着装。希望帮到你。本文还有配套的精品资源点击获取