ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GPU加速机器学习:cuML与RAPIDS实战指南

GPU加速机器学习:cuML与RAPIDS实战指南 如果你手头有一张NVIDIA显卡却还在用CPU慢慢跑scikit-learn那这篇教程就是为你准备的。我会全程围绕NVIDIA cuML和RAPIDS这套GPU加速生态带你从环境搭建、数据处理、模型训练一路做到一个完整的机器学习工作流最后还会附上我实际踩过的一些坑和排查记录。这套方案解决的核心痛点很直接当数据量到了几十万行或上百个特征之后CPU上的pandas和sklearn会变得越来越吃力而GPU上的并行计算能把同样一段代码的时间从“喝杯咖啡”缩短到“喝口水”。我会尽量写得像在实验室里手把手带人一样能直接抄作业也能帮你理解每一步背后的逻辑适合刚接触GPU加速机器学习的算法工程师、数据科学方向的学生以及被训练延时折磨的建模选手。1. 项目概述cuML和RAPIDS到底能省多少时间1.1 核心组件与适用场景RAPIDS是NVIDIA开源的一套GPU数据科学套件核心是把原本跑在CPU上的数据分析、数据处理、机器学习算法整体搬迁到CUDA上并行执行。它并不是一个单独的项目而是一组互相配合的库其中最常碰到的几个包括cuDFGPU版pandas负责数据读取、清洗、聚合、连接等DataFrame操作。cuMLGPU版scikit-learn提供分类、回归、聚类、降维、模型选择等机器学习算法。cuGraphGPU版图分析工具处理NetworkX风格的计算。cuSpatial和cuSignal分别对应空间数据和信号处理场景。这套生态的设计思路很像“直接换引擎”API层面尽量向pandas和sklearn对齐让熟悉CPU数据科学栈的人可以少学很多新概念只要把import pandas as pd改成import cudf as pd把from sklearn.ensemble import RandomForestClassifier改成from cuml.ensemble import RandomForestClassifier大部分代码就能跑起来。我个人用下来最适用cuML和RAPIDS的场景有三类。第一类是单机数据量大到pandas开始吃力但又不至于上Spark那么重的场景比如百万行以内的表格数据处理。第二类是模型训练和调参频繁需要快速迭代特征的场景比如特征工程阶段经常要反复训练同一批模型。第三类是深度学习之外的传统机器学习模型需要批量跑批的场景比如在企业离线训练任务中用随机森林、逻辑回归、KMeans等模型做基线。1.2 从Scikit-learn迁移的真实成本很多人担心从sklearn迁移过来要改一大片代码实际并非如此。cuML从设计初期就在刻意模仿sklearn的fit、predict、transform接口所以只要是熟悉sklearn用法的人切到cuML几乎是无痛的。我举个例子sklearn里的随机森林一般这样写from sklearn.ensemble import RandomForestClassifier model RandomForestClassifier(n_estimators100, max_depth10) model.fit(X_train, y_train) pred model.predict(X_test)换成cuML之后是这样from cuml.ensemble import RandomForestClassifier model RandomForestClassifier(n_estimators100, max_depth10) model.fit(X_train, y_train) pred model.predict(X_test)除了import那一行剩下的代码几乎不变。训练数据方面cuML既接受cuDF的DataFrame也接受pandas的DataFrame和numpy的ndarray。如果传入的是pandas数据cuML会自动把它搬到GPU显存中只不过这一步会有一次拷贝开销所以正式场景我更推荐直接用cuDF来读数据让数据从源头就待在GPU上。真实迁移成本主要不在代码而在环境配置和习惯调整。环境方面要处理驱动、CUDA、RAPIDS版本的匹配这个我会在下一章详写。习惯方面则要注意cuDF虽然API很像pandas但毕竟底层实现不同偶尔会遇到个别函数不支持或行为略有差异的情况。整体来说从sklearn迁到cuML的改动成本大约只占整个项目的10%到20%而换来的训练提速常常是一个数量级。2. 环境准备先把GPU计算环境整明白2.1 硬件与驱动检查在动手安装RAPIDS之前建议先确认三件事显卡型号、驱动版本、CUDA工具包版本。你可以用nvidia-smi命令一次性看到这些信息。下面是我在Ubuntu 20.04上的一台工作机上看到的效果$ nvidia-smi ----------------------------------------------------------------------------- | NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0 | ----------------------------------------------------------------------------- | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | ... ... ... ... | ... | -----------------------------------------------------------------------------这里要重点看的不是显卡型号那一行而是右上角的Driver Version和CUDA Version。RAPIDS从23.10版本开始官方支持CUDA 11.8和CUDA 12.0两个分支所以你的驱动版本至少要能兼容其中一个。一般来说Linux驱动版本在520以上基本都能跑CUDA 11.8和12.0。如果你的环境中nvidia-smi报错提示类似“NVIDIA-SMI has failed because it couldnt communicate with the NVIDIA driver”这通常意味着驱动没有正确安装或者系统内核更新之后驱动模块失效了。这种时候建议先用dkms status看一下驱动模块状态然后重新安装一次匹配当前内核的驱动。经验之谈Ubuntu机器上驱动装不上八成是内核头文件缺失或Secure Boot没关。RAPIDS对显卡的算力也有要求官方文档里明确要求NVIDIA GPU的算力至少在7.0及以上也就是说GTX 10系及以下的显卡直接放弃比较稳妥的是从RTX 20系、30系、40系以及Tesla T4、V100、A100这些卡开始。像K80这种老卡就算能装上驱动也没法跑cuML。2.2 用Docker一条命令跑起RAPIDS如果你不想折腾依赖冲突我最推荐的方案是用Docker。RAPIDS官方在Docker Hub上维护了rapidsai/rapidsai-core镜像里面已经把cuDF、cuML、cuGraph等组件和对应版本的CUDA运行库打包好了拉下来就能用。我习惯这样启动一个交互式容器docker run --gpus all -it --rm \ -p 8888:8888 \ -v /home/yourname/data:/workspace/data \ rapidsai/rapidsai-core:23.10-cuda11.8-runtime-ubuntu20.04-py3.10这里逐项解释一下参数的含义。--gpus all是让容器访问宿主机上所有的GPU这要求你提前装好NVIDIA Container Toolkit否则会报“could not select device driver”之类的错误。-p 8888:8888是把容器内的Jupyter端口映射到宿主机因为我有时候喜欢直接在浏览器里写代码。-v参数把宿主机上的数据目录挂载进容器这样容器销毁之后数据还能留在本机避免误删。进入容器后可以用python检查一下关键包是否正常$ python -c import cudf, cuml; print(cudf.__version__, cuml.__version__) 23.10.0 23.10.0如果顺利打印出版本号说明环境已经可用了。这里额外提醒一句Docker镜像在首次拉取时体积比较大一般6到8GB取决于网络状况可能会等一阵。实在拉不动的话可以配置一下docker registry mirror用国内镜像源加速下载。2.3 conda安装方式与注意事项不用Docker也没关系conda也能装而且对那种不想引入容器虚拟化层的人来说更直接。安装前先创建独立环境强烈建议不要装到base环境里因为cuDF、cuML的依赖链很长装到base很容易把其他包搞挂。conda create -n rapids-23.10 -c rapidsai -c conda-forge \ rapids23.10 python3.10 cuda-version11.8 conda activate rapids-23.10如果网络速度不乐观可以加上-c https://mirrors.xxx之类的镜像通道这里就不展开了。conda安装方式比较适合后续要频繁做自定义开发的人因为所有依赖都直接暴露在系统路径下改起包来更方便。但要注意conda方式不会自动帮你装NVIDIA驱动驱动仍然需要先装好。另外conda环境里的cuda-version只是告诉conda去拉对应CUDA版本的依赖包并不等于全量安装了完整的CUDA Toolkit所以如果你后续要自己编译CUDA扩展可能还需要手动补装CUDA工具链。我在Ubuntu 20.04上实际用下来的感受是Docker方案和conda方案都能稳定运行但Docker胜在开箱即用、归一化程度高conda胜在灵活、和宿主机交互自然。如果你只是想快速验证cuML能不能解决自己的问题优先选Docker如果打算在RAPIDS生态上做长期开发conda更顺手。3. 数据处理cuDF让数据操作也在GPU上进行3.1 从pandas到cuDF的API迁移机器学习工作流里数据清洗和特征工程往往占据大半时间如果只把模型训练放到GPU上数据准备阶段还得先把数据从CPU拷贝到GPU整体提速效果会打折扣。所以更合理的方案是让数据操作也留在GPU上cuDF就是干这个的。cuDF的DataFrame在API设计上高度参考pandas常用的read_csv、head、describe、fillna、groupby、merge、apply等操作在cuDF里几乎都能找到同名方法。这意味着很多pandas代码可以直接切换。让我用一段数据清洗的例子来对比。假设我们要读取一个几GB的CSV过滤掉空值过多的行再做一次分组聚合。pandas版本大概长这样import pandas as pd df pd.read_csv(train.csv) df df.dropna(subset[label]) df df[df[age] 0] grouped df.groupby(city)[amount].mean()cuDF版本只要改import和read_csv那一行import cudf df cudf.read_csv(train.csv) df df.dropna(subset[label]) df df[df[age] 0] grouped df.groupby(city)[amount].mean()我在一台配置是RTX 3090的机器上测试过一份大约5GB的CSVpandas读取加处理大概需要100秒左右cuDF的读取和处理时间大约只有15到20秒。差距主要来自PCIe带宽和GPU并行能力的双重加持。如果你的数据源本身就在GPU显存里那差距会更夸张。3.2 实操读取、清洗、切分一次完成实际项目中我们通常会把数据读取、缺失值处理、编码、归一化、训练测试切分全串起来。下面这段代码是一个比较典型的cuDF处理流程我会在注释里说明每一步的目的。import cudf as cf from cuml.preprocessing import StandardScaler from cuml.model_selection import train_test_split # 1. 读取数据 df cf.read_csv(user_behavior.csv) # 2. 删除全空列和全空行 df df.dropna(axis1, howall) df df.dropna(axis0, howall) # 3. 对数值列填充中位数 num_cols [age, income, click_count] for col in num_cols: median_val df[col].median() df[col] df[col].fillna(median_val) # 4. 对类别列填充众数 cat_cols [city, device_type] for col in cat_cols: mode_val df[col].mode()[0] df[col] df[col].fillna(mode_val) # 5. 把类别列转成数值编码 for col in cat_cols: df[col] df[col].astype(category).cat.codes # 6. 特征和标签分离 X df.drop(is_churn, axis1) y df[is_churn].astype(int8) # 7. 数据集切分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )这段代码的核心思路是先处理整列缺失再逐列填充然后做编码最后切分。有几个细节值得注意。dropna(axis1, howall)这一步先干掉全空列能减少后续很多无效计算。填充中位数和众数的选择是根据业务习惯来的数值型用中位数更稳类别型用众数不会引入新类别。astype(category).cat.codes是把类别列转为整数编码的常见做法和pandas的使用方式一致。3.3 显存管理类型、释放与大数据策略GPU显存是宝贵资源用cuDF处理数据最容易踩的坑就是“OOM”也就是Out of Memory。我自己经历过好多次总结下来有几个非常实用的规避方法。第一尽量缩小数据类型。同样的数据用float64会比float32多占一倍显存。如果数据精度要求没那么极端读取时就可以批量转换成float32或int32明显降低显存占用。df df.astype({col: float32 for col in num_cols})第二善用del和garbage collection。cuDF的数据对象如果你用完了还放在内存里它会一直占着显存空间。在长时间运行的脚本里建议每个大型中间变量用完后就执行一次del必要时再调用gc.collect()。虽然cuDF有自己的内存池但主动释放总比被动等待强。第三学会分批处理。如果一份数据整体放不进显存可以考虑用cudf.read_csv(..., chunksize...)或者其他方式分块读取每块处理完就聚合结果最后再把汇总结果合并。这和pandas处理超大文件时的思路几乎一样只是操作发生在GPU上。第四检查显存占用可以用nvidia-smi也可以用cudf运行时自带的df.memory_usage(deepTrue)来观察各个DataFrame占用。我通常是在模型训练前快速扫一眼所有DataFrame的内存总量如果超过显存的80%就先压缩类型或删掉临时列。4. 模型训练体验GPU版Scikit-learn4.1 cuML提供了哪些常用模型cuML的模型覆盖范围已经相当广了基本把sklearn里常用算法都搬到了GPU上。我习惯把它分成几类来看线性模型类LogisticRegression、LinearRegression、Ridge、Lasso、ElasticNet树模型类RandomForestClassifier、RandomForestRegressor、DecisionTreeClassifier、DecisionTreeRegressor近邻与聚类类KNN、KMeans、DBSCAN降维类PCA、TSNE、UMAP指标与工具类accuracy_score、confusion_matrix、train_test_split、KFold这些模型的使用方式基本都遵循fit/predict/predict_proba的模式参数命名也在尽量对齐sklearn。拿KMeans来说sklearn里写n_clusters5、random_state42cuML里也是这样。这种设计带来的好处是你已经掌握的调参经验可以直接迁移不需要重新学一套新概念。不过有一点必须说明cuML的算法虽然接口和sklearn一致但底层实现不同所以训练出来的模型在数值细节上不会和sklearn完全一样。尤其像随机森林这种依赖随机数生成和并行调度策略的模型特征重要性排序可能略有差异这属于正常现象不是bug。4.2 用随机森林做一个分类任务我用一个公开的“用户是否流失”二分类数据来演示完整模型训练流程。这里的数据格式是网上常见的user_behavior.csv特征包含年龄、收入、点击次数、城市、设备类型、使用时长等目标是判断用户是否会流失。先把特征准备好然后直接训练import cudf as cf from cuml.ensemble import RandomForestClassifier from cuml.model_selection import train_test_split from cuml.metrics import accuracy_score # 读取数据 df cf.read_csv(user_behavior.csv) # 一次简单的清理只保留有效样本 df df.dropna() X df.drop(is_churn, axis1) y df[is_churn].astype(int8) # 切分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 训练随机森林 model RandomForestClassifier( n_estimators300, max_depth12, max_featuresauto, random_state42 ) model.fit(X_train, y_train) # 预测与评估 y_pred model.predict(X_test) acc accuracy_score(y_test, y_pred) print(accuracy:, acc) print(feature importances:, model.feature_importances_)这段代码跑起来后能看到准确率大约在0.85到0.9之间具体取决于数据分布。我在这台RTX 3090上用大约30万行、40个特征的数据测试cuML的随机森林训练耗时大约是CPU版sklearn的五分之一到十分之一。如果数据量继续变大这个差距还会进一步拉开。训练过程中有几个参数值得调试。n_estimators控制树的数量越大越稳但越慢一般100到500之间比较合理。max_depth控制树的深度太深容易过拟合太浅精度不够我习惯从10到15起调。max_features控制每个节点考虑的特征数auto是让算法自动选择实际任务里可以试sqrt或log2。还有一个容易被忽略的地方cuML的随机森林在训练时如果遇到非常大的数据集可以设置max_leaves或max_depth来限制单棵树复杂度否则即使显存够大训练时间也会线性上涨。4.3 与其他库搭配XGBoost和LightGBM的GPU模式cuML内置的模型虽然覆盖面广但很多实际项目里大家更偏爱XGBoost或LightGBM这类梯度提升树模型。好消息是这两者也都能跑在GPU上而且支持直接读取cuDF的DataFrame。XGBoost从1.7版本开始推荐使用新的hist方法配合devicecuda参数。举个例子import xgboost as xgb dtrain xgb.DMatrix(X_train, labely_train.to_numpy()) params { objective: binary:logistic, eval_metric: auc, tree_method: hist, device: cuda, max_depth: 6, learning_rate: 0.05, } model xgb.train(params, dtrain, num_boost_round500)LightGBM同样支持GPU训练它的方法是把device_type设为gpu然后指定gpu_platform参数常见的配置是CUDA。两种库配合cuDf的关键点是一样的尽量让数据直接以GPU内存块的形式传入避免从GPU显存拷贝回CPU内存。在实际工作流里我通常会把cuML的随机森林当基线再用XGBoost或LightGBM去冲精度。这样一方面能享受GPU加速带来的快速迭代另一方面也能发挥梯度提升树在表格数据上的传统优势。如果只是做基线测试cuML已经够用如果是最终上线模型XGBoost或LightGBM会更常见。5. 端到端机器学习工作流从CSV到模型部署5.1 业务场景与数据约定前面的章节都是拆开讲单个环节这一节我会把它们连起来跑通一个比较完整的机器学习工作流。假设这样一个业务场景某平台希望根据用户的注册信息和使用行为预测用户在未来一个月内是否流失。我们已经有一份训练数据字段包括用户ID、年龄、注册天数、登录次数、平均使用时长、是否VIP、所属行业、最近一次投诉类型、上个月消费金额等。标签列是is_churn取值为0或11表示流失。这个场景的机器学习工作流从项目落地的角度来说通常包含下面几个阶段数据读取与探索数据清洗与特征工程数据集切分模型训练与评估模型保存与部署。下面我会按这个顺序写一套可以跑的代码。5.2 工作流完整代码实现先写数据准备和特征工程部分。考虑到代码可读性我用函数把每个阶段拆开这样后续替换某个环节的时候不用动其他代码。import cudf as cf import numpy as np import joblib from cuml.model_selection import train_test_split from cuml.preprocessing import StandardScaler from cuml.ensemble import RandomForestClassifier from cuml.metrics import accuracy_score def load_and_clean(path): df cf.read_csv(path) # 删除全空列 df df.dropna(axis1, howall) # 填充关键字段 df[age] df[age].fillna(df[age].median()) df[login_count] df[login_count].fillna(0) df[last_month_spend] df[last_month_spend].fillna(0) # 类别编码 cat_cols [industry, complaint_type] for col in cat_cols: df[col] df[col].astype(category).cat.codes return df def split_and_train(df, label_colis_churn): X df.drop(columns[label_col, user_id], errorsignore) y df[label_col].astype(int8) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model RandomForestClassifier( n_estimators200, max_depth10, random_state42 ) model.fit(X_train_scaled, y_train) pred model.predict(X_test_scaled) acc accuracy_score(y_test, pred) print(ftest accuracy: {acc:.4f}) return model, scaler, acc if __name__ __main__: df load_and_clean(user_behavior.csv) model, scaler, acc split_and_train(df) print(workflow done.)这段代码把整个流程压缩在几十行里核心设计是数据读取清洗和训练评估解耦。load_and_clean只负责返回一份干净的DataFramesplit_and_train专注于训练和评估。我用StandardScaler做特征标准化是因为树模型其实不太在意尺度但换成逻辑回归或KNN时标准化很重要在这里先演示通用做法。如果数据量大到一次放不进显存我会在外层加一个分批处理函数每次读入一部分数据计算完统计量后直接合并结果确保整体内存峰值可控。5.3 模型保存与后续部署模型训练完之后下一步就是持久化保存。cuML模型本质上是Python对象所以你可以直接用joblib或pickle保存这点和sklearn非常像。我习惯用joblib因为它在保存大型numpy数组时效率更高。import joblib joblib.dump(model, churn_model.pkl) joblib.dump(scaler, scaler.pkl)重新加载时也很简单model joblib.load(churn_model.pkl) scaler joblib.load(scaler.pkl)这里要特别提示一个容易踩的坑cuML模型和版本强相关。你在23.10版本下保存的模型最好在相同版本下恢复跨大版本恢复时偶尔会遇到协议不兼容或属性缺失的问题。所以在做工程化交付时最好把运行的conda环境或Docker镜像版本一起锁定避免线上环境升级导致模型加载失败。部署方面如果只是离线批量预测加载模型后对新的cuDF DataFrame调用predict()即可。如果是实时在线服务建议用一个轻量级的模型服务框架包一层接口输入数据先转成cuDF或numpy数组再调用模型推理。由于模型本身跑在GPU上单次推理延迟通常很低比较适合对批量并发要求不高的在线预测。6. 高频问题与排查记录6.1 常见问题速查表我在实际操作中遇到过不少问题也帮同事排查过各种奇怪的报错下面整理成一张表希望能帮你节省时间。现象可能原因解决办法nvidia-smi提示无法与驱动通信驱动未安装或内核更新后模块失效重新安装NVIDIA驱动检查dkms状态运行Docker容器时报缺少GPU错误没有安装NVIDIA Container Toolkit安装nvidia-container-toolkit并配置好runtime导入cudf或cuml时提示包缺失版本与Python或CUDA不匹配使用官方conda环境或Docker镜像建议锁定版本训练时出现GPU out of memory数据或中间变量占用显存过多压缩dtype删除临时变量分批处理cuml预测结果与sklearn的略有不同底层算法实现和随机策略不一致正常现象关注效果而不是逐位一致读取超大CSV时加载过慢或卡死CSV解析瓶颈或内存不足改用chunksize或Parquet等列式格式Docker拉取镜像很慢网络问题或镜像仓库未加速配置registry mirror或使用代理加速但这里不展开这张表里有些问题看起来不起眼但一旦出现就会阻断整个工作流。比如Docker和GPU通信的问题如果你按照我第二章的步骤先装好了nvidia-container-toolkit大概率不会遇到。但如果你是在别人已经配置好的机器上跑这就会变成最常见的障碍。6.2 给初学者的几条经验最后分享几条我做GPU数据科学项目时沉淀下来的经验希望能让新手少走弯路。第一条先跑通再优化。不要一开始就像调参侠一样疯狂搜索超参数先把一个最小化的代码从环境搭建跑到模型评估确认整条链路是通的再考虑扩大数据量和调优。很多新人卡住都是因为在某个环节反复纠结但根本没机会看到模型输出。第二条尽量让数据全程留在GPU上。如果你读取用pandas处理用pandas然后再转成cuML要用的格式每次转换都是一次数据从CPU到GPU或反向拷贝。这些拷贝开销在数据量大时非常可观。合理的方式是从数据入口就用cuDF处理完再训练全程不落地。第三条显存不是越大越好用而是够用就好。不要看显卡是24GB就以为随便折腾实际跑起特征工程来几个大DataFrame加上模型训练缓存很容易就逼近上限。养成定时用nvidia-smi检查显存的习惯对定位OOM问题特别有效。第四条多看官方文档和示例代码。RAPIDS有一套比较完善的官方文档并且每个版本都会提供notebook示例里面的代码质量通常很高比网上某些二手博客靠谱得多。遇到API用法不确定时先去翻docs再去做实验。我在实际操作中还有一个习惯每次搭建新环境时都会保留一份能跑的Docker命令和版本号记录。这样即使过了几个月要复现旧结果也能快速拉起来一个完全一致的环境。这个习惯帮我省掉了非常多“昨天还能跑今天怎么不行了”的烦恼。这套工具链其实是把原来在CPU上串行执行的工作变成了GPU上的并行任务核心思路并不复杂但带来的收益非常可观。顺着环境搭建、cuDF数据处理、cuML模型训练、完整工作流落地、问题排查这条主线走下来你已经可以把大部分传统机器学习项目跑在GPU上了。之后如果再想提升效率可以从自定义CUDA算子、混合使用Dask做多GPU分布式计算这些方向继续深入。
返回列表