资讯动态

cuDF 库设计深度解析:Frame 分层、Column 体系与 Spilling / Copy-on-write 内存管理机制

发布时间:2026/9/25 8:07:52 来源:尧图企业网站定制
数据分析数据工程机器学习【免费下载链接】cudfcuDF - GPU DataFrame Library项目地址https://gitcode.com/gh_mirrors/cu/cudf点击查看免费下载cuDF 是 NVIDIA RAPIDS 生态中面向 GPU 的 DataFrame 库其内部通过Frame 层面向用户的 pandas 兼容对象 Column 层一维 GPU 数据载体两层架构组织所有数据结构。本文以 docs/cudf/source/cudf/developer_guide/library_design.md 为主线结合python/cudf/cudf/core下的真实源码与配置实现系统讲解 Frame 继承体系、Column 类型体系以及支撑超大显存场景的两大内存管理机制——**Spilling溢出到主机内存**与Copy-on-write写时复制。读完本文你将能看懂 cuDF 内部对象的类层次关系理解cudf.set_option(spill, ...)与cudf.set_option(copy_on_write, ...)等选项背后的缓冲层原理并掌握在生产环境中启用、调优与观测这些机制的具体方法。Frame 层用户可见对象的继承骨架cuDF 包含两种主要数据结构Frame框架面向用户的、镜像 pandas 数据结构的对象如DataFrame和SeriesColumn列持有 GPU 数据的一维表示并实现针对特定数据类型的方法。从 python/cudf/cudf/core/frame.py 的源码可以看到Frame的类定义是class Frame(BinaryOperand, Scannable, Serializable): A collection of Column objects. def __init__(self, data: ColumnAccessor | MutableMapping[Any, ColumnBase]): self._data ColumnAccessor(data)也就是说Frame本质上是一个列的集合A collection of Column objects它内部持有一个ColumnAccessor将一列或多列映射到唯一标签。Frame还混入了三个核心 MixinBinaryOperand二元运算、Scannablecumsum等累积扫描运算与Serializable序列化。继承层级与职责划分类图展示了Frame各个子类之间的继承关系最终派生出的公共对象是DataFrame、Series和IndexFrame ├── IndexedFrame # DataFrame、Series 的公共基类含 index └── SingleColumnFrame # Series、Index 的公共基类仅单列 ├── Series └── Index ├── RangeIndex ├── MultiIndex ├── IntervalIndex ├── TimedeltaIndex └── DatetimeIndexFrame |-- IndexedFrame Frame |-- SingleColumnFrame SingleColumnFrame |-- Series SingleColumnFrame |-- Index IndexedFrame |-- Series IndexedFrame |-- DataFrame Index |-- RangeIndex Index |-- MultiIndex Index |-- IntervalIndex Index |-- TimedeltaIndex Index |-- DatetimeIndex以上为文档类图中描述的继承关系实际源码中各类的声明位置如下IndexedFrame定义于 python/cudf/cudf/core/indexed_frame.pySingleColumnFrame定义于 python/cudf/cudf/core/single_column_frame.pyIndex定义于 python/cudf/cudf/core/index.pyMultiIndex定义于 python/cudf/cudf/core/multiindex.py。这个继承体系的设计目标是合并私有方法与公共方法的共享实现让逻辑尽可能下沉到基类Frame实现Series、DataFrame、Index三者通用的基础方法。从源码看它提供了_num_columns、_num_rows、_column_names、_columns、_column_labels_and_values、_dtypes等内部属性以及serialize/deserialize序列化协议frame.py。SingleColumnFrame实现Series与Index共享的方法——它们都只由 1 列数据表示。例如_reduce聚合归约直接委托给self._column上对应的方法single_column_frame.pyname属性取唯一列名第 85-89 行。IndexedFrame实现DataFrame与Series共享的方法——它们都包含一个Index。从 indexed_frame.py 可见其构造函数除data外还接收index、attrs、allows_duplicate_labels等参数并通过self._index index保存索引其_num_rows属性甚至优先使用len(self.index)第 386-389 行因为数据列可能为空。公共方法的分派约定一般而言公共的Series/DataFrame/Index方法只实现类特有的逻辑最终通过super()分派到共享实现同理SingleColumnFrame与IndexedFrame的方法只实现单列/索引相关的逻辑最终通过super()分派到Frame的方法——后者通常涉及处理一个或多个Column。这一约定保证了类特有逻辑与列处理逻辑的解耦也是理解 cuDF 方法调用链的关键线索。索引的两个特例RangeIndex 与 MultiIndexIndex及其子类大部分直接复用SingleColumnFrame的实现唯独以下两个子类例外RangeIndex由 Python 的range对象而非 Column支撑。源码中RangeIndex._range: range是其实例字段index.py构造函数接受start、stop、step、name等参数第 2485-2522 行。RangeIndex的方法在可能的情况下都有特殊实现以避免把range物化为列实在无法避免时才先转换为int64dtype 的Index。这也是为什么DataFrame/Series默认的RangeIndex能保持极低的内存开销。MultiIndex可能由多列数据支撑因此它重写了SingleColumnFrame中所有假定只有 1 列的方法以支持多列场景multiindex.py。Column 层一维 GPU 数据的类型化表示ColumnBase代表Frame中的 1 列是一个包含两个主要组件的对象一个pylibcudf.Column以Apache Arrow 格式表示 GPU 数据一个.dtype是暴露给Frame的合法 pandas 数据类型。在源码中ColumnBase定义于 python/cudf/cudf/core/column/column.pyclass ColumnBase(Serializable, BinaryOperand, Reducible):它同样混入了Serializable、BinaryOperand与Reducible可归约是处理任意数据类型的pylibcudf.Column的共享基类。ColumnBase的方法通常基于 pylibcudf API 实现并包含数据类型解析逻辑以便在需要时返回合适数据类型的新的ColumnBase。列子类体系ColumnBase有多个最终被Frame使用的子类各自整合逻辑并实现与 1 个或多个相关数据类型专属的方法源码中的类定义位置见括号内文件列子类承载数据类型源码位置NumericalColumn整数、浮点、布尔column/numerical.pyStringColumn字符串column/string.pyCategoricalColumncategorycolumn/categorical.pyDatetimeColumntimestampcolumn/datetime.pyDatetimeTZColumn带时区的 timestampcolumn/datetime.pyTimeDeltaColumndurationcolumn/timedelta.pyIntervalColumnintervalcolumn/interval.pyListColumnlistcolumn/lists.pyStructColumnstructcolumn/struct.pyDecimalColumndecimal32 / decimal64 / decimal128column/decimal.py此外两个中间基类值得注意NumericalBaseColumncolumn/numerical_base.py作为NumericalColumn与DecimalColumn的公共基类TemporalBaseColumncolumn/temporal_base.py作为DatetimeColumn、DatetimeTZColumn、TimeDeltaColumn的公共基类统一实现了__contains__、values、element_indexing、to_pandas、ceil/floor/round/strftime等时间语义操作。dtype 约束与例外每个列子类被限制持有与自身数据类型一致的、合法的 pandas 数据类型对象包括 pandas nullable 扩展类型例外如下CategoricalColumn持有cudf.CategoricalDtype而非pandas.CategoricalDtypeIntervalColumn可以持有cudf.IntervalDtype而非pandas.IntervalDtypeListColumn可以持有cudf.ListDtypeStructColumn可以持有cudf.StructDtypeDecimalColumn可以分别持有cudf.Decimal32Dtype、cudf.Decimal64Dtype、cudf.Decimal128Dtype。此外还有两点限制没有pandas.PeriodDtype或pandas.SparseDtype的表示objectdtype 可以作为StringColumn的.dtype但严格表示字符串类型——在 pandas 中object可以表示任意 PyObject 数据而 cuDF 不支持这种任意 Python 对象数据。ColumnBase及其子类的实现全部位于python/cudf/cudf/core/column目录。Frame 对 Column 的两种操作模式实现 cuDF API 时存在两种常见模式模式一逐列操作操作作用于 Frame 的各列遍历Frame的ColumnAccessor中存储的每个ColumnBase调用ColumnBase上的相关方法如果返回另一个Frame则用每个新的ColumnBase结果构造新的ColumnAccessor。模式二多列批量操作一次作用于多列收集Frame的ColumnAccessor中存储的所有ColumnBase将底层的pylibcudf.Column传给某个 pylibcudf API如果返回另一个Frame则用每个新的ColumnBase结果构造新的ColumnAccessor。Frame子类以及GroupBy、Rolling、Resampler等中间对象的方法都模仿这一模式——通过访问Frame的私有 API 来实现。这两种模式概括了 cuDF 中大多数操作但部分公共 API 可能采用更定制化的模式。从 column_accessor.py 可以看到ColumnAccessor是一个MutableMapping支持带multiindex标志的嵌套标签访问而 column.py 中的PylibcudfFunction封装了将ColumnBase包装为pylibcudf.Column后调用 pylibcudf 函数的细节——ColumnList标签类型则用于把一组列打包成plc.Table传给 pylibcudf正是模式二的底层实现载体。Spilling将设备内存溢出到主机内存当数据对象占用的内存超过 GPU 可用显存时cuDF 支持自动溢出spilling与回溢unspilling实现超出显存容量的计算。Spilling 默认关闭可通过两种方式开启设置环境变量CUDF_SPILLon或在代码中执行cudf.set_option(spill, True)。在源码 options.py 中spill选项的默认值正是_env_get_bool(CUDF_SPILL, False)即默认False。参数与选项除spill开关外还有三个配套参数参数环境变量set_option等价形式默认值说明按需溢出CUDF_SPILL_ON_DEMANDONcudf.set_option(spill_on_demand, True)开启 spilling 后默认开启注册 RMM 内存不足错误处理器在显存不足时先溢出缓冲区以腾出空间设备内存上限CUDF_SPILL_DEVICE_LIMITXcudf.set_option(spill_device_limit, X)关闭None将设备内存限制设为X字节。会引入适度开销且是软限制——若不可溢出的缓冲区过多实际用量可能超过该限制溢出统计CUDF_SPILL_STATSlevelcudf.set_option(spill_stats, level)0关闭按级别收集溢出统计信息见下文从 options.py 的实现可以看到这些选项的默认值与校验规则spill_on_demand默认_env_get_bool(CUDF_SPILL_ON_DEMAND, True)即默认True若 spilling 未启用则无效果spill_device_limit默认_env_get_int(CUDF_SPILL_DEVICE_LIMIT, None)合法值为任意正整数或None禁用spill_stats默认_env_get_int(CUDF_SPILL_STATS, 0)合法值为任意正整数0 表示禁用。设计SpillableBuffer 与全局 Spill ManagerSpilling 由两个组件构成SpillableBuffer——一个新的缓冲区子类实现数据在主机内存与设备内存之间的原地移动Spill Manager溢出管理器——跟踪所有SpillableBuffer实例并按需触发溢出。启用 spilling 后cuDF 全局使用一个全局 spill manager使as_buffer()返回SpillableBuffer而非默认的Buffer实例。源码中 spill_manager.py 定义了SpillManager类其关键结构包括_buffers: weakref.WeakValueDictionary——用弱引用跟踪所有受管缓冲区第 225 行、第 235 行add()方法在加入新缓冲区时立即调用spill_to_device_limit()从而在设置设备内存上限时持续将超额缓冲溢出到主机第 284-298 行_out_of_memory_handle()可作为RMMFailureCallbackResourceAdaptor的回调当 RMM 分配失败时尝试溢出 nbytes 字节成功则让 RMM 重试分配失败则执行一次gc.collect()后再试第 240-282 行——这正是按需溢出的底层实现。SpillableBufferspillable_buffer.py与SpillableBufferOwner第 101 行共同完成实际的搬移spill()通过rmm.pylibrmm.device_buffer.copy_ptr_to_host将设备内存复制到主机第 243-249 行unspill()则通过rmm.DeviceBuffer.to_device将主机内存复制回设备第 250-266 行并以nvtx.annotate标注SpillDtoH/SpillHtoD区间便于性能剖析。Exposed暴露语义与 acquire_spill_lock访问Buffer.get_ptr(...)可以得到缓冲区的设备内存指针。对普通Buffer这没有问题但对可能已溢出设备内存的SpillableBuffer来说必须在返回设备指针之前先把内存回溢unspill到设备。而且只要这个设备指针正在或可能被使用SpillableBuffer就不能再把内存溢回主机——否则会使该指针失效。为此cuDF 将该缓冲区标记为不可溢出unspillable即所谓exposed已暴露。这种暴露既可以是永久的例如设备指针被暴露给外部项目也可以是临时的例如libcudf正在访问该设备内存期间。永久暴露SpillableBufferOwner.mark_exposed()会先把缓冲区回溢到设备再设置_exposed Truespillable_buffer.py临时暴露SpillableBuffer.get_ptr(...)若在acquire_spill_lock装饰器/上下文管理器内被调用则只在运行于该上下文期间把缓冲区标记为不可溢出源码中通过spill_lock()方法把SpillLock加入_spill_locks集合弱引用监控其存活周期第 293-307 行。在 spillable_buffer.py 的ptr属性实现中可以看到若在access(scopeinternal)上下文内调用则按需回溢并临时禁止溢出若不在任何访问上下文内调用则该缓冲区被永久标记为不可溢出。溢出统计cuDF 支持溢出统计对性能剖析以及定位哪些代码让缓冲区变得不可溢出非常有用。统计分三个信息收集级别级别收集内容开销0关闭无开销1溢出的时长与字节数极低2每次缓冲区被永久暴露exposed的记录可能很高统计默认关闭可通过两种方式开启设置环境变量CUDF_SPILL_STATSstatistics-level或执行cudf.set_option(spill_stats, statistics-level)。通过全局 spill manager 可以访问统计信息 import cudf from cudf.core.buffer.spill_manager import get_global_manager stats get_global_manager().statistics print(stats) Spill Statistics (level1): Spilling (level 1): gpu cpu: 24B in 0.0033若要每个 dask worker 都打印溢出统计可以这样操作def spill_info(): from cudf.core.buffer.spill_manager import get_global_manager print(get_global_manager().statistics) client.submit(spill_info)源码中get_global_manager()定义于 spill_manager.pySpillStatistics.__str__则负责上述可读输出第 167 行统计的每次记录由SpillableBuffer.spill()在搬移完成后通过self._manager.statistics.log_spill(...)写入第 272-278 行。Copy-on-write写时复制本节描述 copy-on-write 特性的内部实现细节。该特性的核心实现依赖ExposureTrackedBuffer与BufferOwner的跟踪能力。注BufferOwner与Buffer均定义于 python/cudf/cudf/core/buffer/buffer.py分别在第 43 行与第 215 行而Buffer.copy(deepFalse)的浅拷贝实现第 313-337 行与make_single_owner_inplace()第 351-376 行正是 copy-on-write 的核心执行路径阅读时可相互对照。核心机制BufferOwner 与 ExposureTrackedBufferBufferOwner跟踪其底层内存的内部引用与外部引用内部引用通过维护底层内存的每个ExposureTrackedBuffer的弱引用来跟踪源码中体现为BufferOwner._slices: weakref.WeakSet[Buffer]见 buffer.pyBuffer.__init__会执行self._owner._slices.add(self)登记切片第 256 行外部引用通过底层内存的 exposure暴露状态跟踪。当缓冲区持有者把设备指针整数或void*交给 cuDF 之外的库时该缓冲区即被视为 exposed——此时 cuDF 无法得知第三方是否修改了数据。ExposureTrackedBuffer是Buffer的子类表示受暴露跟踪的缓冲区所支撑内存的一个切片slice。当 cuDF 选项copy_on_write为True时as_buffer返回ExposureTrackedBuffer。正是这个类决定当对Column执行写操作时是否要复制见下文。如果多个切片指向同一底层内存那么一旦试图修改就必须复制。向第三方库暴露时的 eager 复制如果Column/ExposureTrackedBuffer通过__cuda_array_interface__暴露给第三方库cuDF 就再也无法跟踪缓冲区是否被修改。因此只要有人通过__cuda_array_interface__访问数据cuDF 就立即eagerly触发复制调用.make_single_owner_inplace确保对底层数据做一次真复制并让当前切片成为唯一所有者。此后任何未来的复制请求也都必须触发真正的物理复制因为无法跟踪第三方对象的生命周期。为此cuDF 还会把该Column/ExposureTrackedBuffer标记为 exposed表示今后任何浅拷贝请求都会触发真正的物理复制而不是 copy-on-write 式的浅拷贝。在源码中Buffer.__cuda_array_interface__属性buffer.py正是以with self.access(modewrite)方式返回指针的——即任何 CAI 访问都被视为写访问并触发make_single_owner_inplace而make_single_owner_inplace在检测到len(self._owner._slices) 1即存在多个共享切片时会把自己从切片集合移除、执行self.copy(deepTrue)并接管新的 owner第 351-376 行。获取只读对象对不会修改数据的操作来说只读对象非常有用。cuDF 的做法是围绕ExposureTrackedBuffer创建简单的包装类构造一个等价的 CAI但通过调用.get_ptr(moderead)获取指针——不会像可写指针访问那样把指针标记为 exposed。这样即使多个ExposureTrackedBuffer指向同一个ExposureTrackedBufferOwner也不会触发深复制。这种方式只能在代理对象生命周期被限制在 cuDF 内部代码执行范围内时使用若把这种只读代理交给外部库或用户 API会导致未跟踪的引用和未定义的 copy-on-write 行为。内部原始数据指针访问由于启用 copy-on-write 后直接访问缓冲区关联的原始指针是不安全的除上述只读代理对象外指针访问统一通过Buffer.get_ptr进行。该方法接受一个mode参数调用者通过它表明将如何访问缓冲区关联的数据moderead调用者只读访问不会通过该指针修改缓冲区此时浅拷贝不会被解除链接unlinkmodewrite调用者需要修改会触发所有浅拷贝的解除链接。从 buffer.py 可以看到Buffer.ptr的实现若访问模式栈顶为write则先调用make_single_owner_inplace()再返回指针而access(mode...)上下文管理器第 286-294 行负责设定访问模式。变宽数据类型Variable width data types弱引用只对定宽数据类型实现因为它们是唯一可以在原地修改的列类型。对变宽数据类型的深复制请求总是返回列的浅拷贝——这些类型不支持对数据的真实原地修改cuDF 内部用_mimic_inplace模拟原地修改但产生的数据始终是底层数据的深复制。示例浅拷贝的延迟复制行为启用 copy-on-write 后对Series或DataFrame做浅拷贝不会立即复制数据而是生成一个视图直到对其任一拷贝执行写操作时才惰性复制。先创建一个 Series import cudf cudf.set_option(copy_on_write, True) s1 cudf.Series([1, 2, 3, 4])复制s1 s2 s1.copy(deepFalse)再复制s2 s3 s2.copy(deepFalse)查看数据与内存地址三者指向同一块设备内存 s1 0 1 1 2 2 3 3 4 dtype: int64 s2 0 1 1 2 2 3 3 4 dtype: int64 s3 0 1 1 2 2 3 3 4 dtype: int64 s1.data._ptr 139796315897856 s2.data._ptr 139796315897856 s3.data._ptr 139796315897856现在对其中一个例如s2执行写操作s2会在设备上创建新副本后再修改 s2[0:2] 10 s2 0 10 1 10 2 3 3 4 dtype: int64 s1 0 1 1 2 2 3 3 4 dtype: int64 s3 0 1 1 2 2 3 3 4 dtype: int64检查内存地址s1与s3仍共享同一地址而s2有了新地址 s1.data._ptr 139796315897856 s3.data._ptr 139796315897856 s2.data._ptr 139796315899392现在对s1执行写操作因为s3中仍存在共享的弱引用会触发设备内存上的新复制 s1[0:2] 11 s1 0 11 1 11 2 3 3 4 dtype: int64 s2 0 10 1 10 2 3 3 4 dtype: int64 s3 0 1 1 2 2 3 3 4 dtype: int64检查内存地址s2、s3的地址保持不变而s1因写入期间执行了复制操作而改变 s2.data._ptr 139796315899392 s3.data._ptr 139796315897856 s1.data._ptr 139796315879723cuDF 的 copy-on-write 实现受 pandas 相关提案启发Google Doc 提案与 pandas GitHub issue #36195 所讨论的方向设计动机是让浅拷贝零成本化、仅在写入时才发生真实复制从而显著降低大 DataFrame 场景下的显存浪费。小结与进一步阅读cuDF 的库设计可以概括为一条主线面向用户的DataFrame/Series/Index通过IndexedFrame/SingleColumnFrame共享Frame基类逻辑而所有数据最终落到按数据类型分派的ColumnBase子类之上在此基础上SpillableBuffer 全局 spill manager 支撑超显存计算ExposureTrackedBufferBufferOwner弱引用跟踪支撑惰性写时复制。二者共同构成了 cuDF 在大规模 GPU 数据分析场景下的内存管理基石。如果希望深入源码建议按以下路径阅读Frame 继承体系frame.py、indexed_frame.py、single_column_frame.py、index.py、multiindex.py列类型体系python/cudf/cudf/core/columncolumn.py定义ColumnBase各子类文件见上文表格Spilling 实现python/cudf/cudf/core/buffer/spillable_buffer.py 与 spill_manager.pyCopy-on-write 实现python/cudf/cudf/core/buffer/buffer.py选项注册与默认值python/cudf/cudf/options.py。此外所有相关选项spill、spill_on_demand、spill_device_limit、spill_stats、copy_on_write的完整说明、合法取值与默认值都可以通过cudf.get_option(...)与cudf.describe_option(...)在运行时查询。赞分享数据分析数据工程机器学习【免费下载链接】cudfcuDF - GPU DataFrame Library项目地址https://gitcode.com/gh_mirrors/cu/cudf点击查看免费下载相关推荐Caffe2内存池管理底层内存分配机制深度解析Caffe2内存池管理底层内存分配机制深度解析 你是否在训练深度学习模型时遇到过内存碎片化导致的训练中断是否因频繁内存分配拖慢了模型推理速度本文将深入解析Apache Hudi核心概念解析深入理解Copy-on-Write与Merge-on-ReadApache Hudi核心概念解析深入理解Copy on Write与Merge on Read Apache Hudi作为 开源分布式列存储系统 为大数据数据湖湖仓一体大数据数据存储从零到一如何用HiGHS线性优化求解器解决你的第一个实际问题从零到一如何用HiGHS线性优化求解器解决你的第一个实际问题 你是否曾面对复杂的资源分配、生产调度或投资组合优化问题却不知从何下手线性规划、二次规划、混合科学计算高性能计算上一篇终极Android Auto解锁指南AAAD如何让你的车载体验全面升级 ✨下一篇SpeexDSP终极指南5个让音频处理效率翻倍的实用功能创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑