

当遇到库中缺少所需功能的情况时(这种情况可能占开发时间的10%甚至1%),或者需要将不同代码模块粘合时,我们提供了多个解决方案。
其中一个方案叫做cuda.parallel,如果你熟悉C语言中的Thrust库,就会明白它的作用。这些是基础并行算法,你可以直接从CPU端调用排序、直方图统计等功能。这些算法库都经过加速优化,你可以将它们组合成完整的并行算法。另一个方案是cuda.cooperative,当你在编写Python GPU连接代码时,仍然可以调用这些经过极致调优的高性能加速库。

就像你不应该自己编写FFT算法一样,你也不应该自己实现排序算法。应该依赖该领域的专家,他们已经为GPU完美调优了这些算法。由于这些库构建在我们多年开发的底层基础设施之上——也就是最开始展示的那个庞大的技术栈——我们并没有用Python重写这些功能,而是确保它们能链接到经过精细优化的C++底层代码。
因此,Python与C++的性能差异可以忽略不计,你无需做出任何牺牲。这是CUDA的核心要求:我们必须让你能完全访问GPU的性能。毕竟你使用GPU就是为了追求速度,我们必须赋予你这种能力。你将获得Python的生产力,同时又能直接调用与原生CUDA代码相同的性能。

最后要讲的是内核编写,我之前已经承诺过要深入这个主题。但在具体讲解内核编写之前,我想先带大家回顾或了解一下GPU内核的运行机制。

熟悉CUDA的观众可能已经了解这个流程,但我会带大家探索一些未曾见过的细节。通常处理GPU内核时,你会从一个数据数组开始——需要批量处理的数据。你几乎不会只用GPU计算单个元素,多数情况下,你会批量处理数据数组。
你可以在PyTorch或NVIDIA的Warp中编写内核,这类编译系统会自动将其映射到GPU上运行。大多数情况下这已经足够,能显著加速你的工作,但很多时候,你需要更精细的控制。
因此,很多人遵循的原则是:绝不要多做工作。能用一行代码解决的问题,就绝不要用100行。我们都是程序员,本质上都是追求效率的。网格级并行确实是一种简单易行的加速方式,但它并不能完全解决问题。有时候只需多付出一点努力,就能获得更高的性能。

CUDA的分解方式是这样的:你把程序拆分成独立的模块。这在多核或多节点开发中同样适用——你需要找到可以并行处理的独立工作单元。对于Cuda来说,你要做的是启动一个网格(grid)的线程块(block)到GPU上,处理这些被我称为"数据块"的问题单元。每个线程块会处理一个或多个数据块,而每个线程块内部最多可以有1000个线程。每个线程会挑选一个数据元素进行处理,通过这种线程协作的方式,我们实现了并行计算。在CUDA的GPU架构中,通过将线程组合起来,每个线程执行单个元素的运算,我们既获得了粗粒度的并行,也保留了细粒度的控制。

在我看来,这正是GPU的真正超能力,也是它与传统的SIMD向量计算架构的主要区别。我们称之为单线程执行模型的结构,当它们被组合使用时,就能产生强大的并行能力,但你又不必完全依赖这种组合。你仍然可以控制细粒度的并行,这种灵活性使得GPU能够适用于极其广泛的应用场景。
但我们也未必需要深入到线程级并行。我从整个数据数组分解到细粒度的线程数组,但也可以中途停止。如果我在第一步将数据分解成数据块,然后直接处理这些数据块,往往会发现:我的应用程序的问题单元往往不是随机分散的数据,而是结构化的数据。

这些数据块本质上是向量、张量,或者是某种形式的数组。因此我可以自动应用整个数组的运算,只需取其中一个线程块,让编译器替我完成操作。你只需定义好线程映射关系,问题就变得简单了。我只需要做第一步,不需要深入第二步。实际上,编译器往往能比我做得更好,因为编译器深刻理解GPU运行的底层细节。

我们拥有三种不同层次的并行模型:左边的网格级模型,右边的传统线程级模型,中间的块级模型(这正是我想重点介绍的)
这三种模型都有其价值。记住,CUDA的优势在于提供多种选项的菜单——没有放之四海而皆准的方案,没有单一层级能统治所有场景。

我特别想谈谈中间的块级模型,因为我认为它在投入与回报之间达到了很好的平衡。我们开发了一个名为cuTile的工具,这是为CUDA设计的分块编程扩展。与传统的元素级操作不同(如图片底部所示),我们采用数组运算的方式:
数组 + 数组 = 新数组
由于这些数组具有结构化特性,编译器能高效地将它们映射到GPU上。这让问题变得更容易思考、调试和描述。正如我之前所说,这往往能带来更高的性能。

基于张量的模型另一个显著特点是,它能完美映射到张量核心(TensorCores)这类硬件单元。GPU内部包含大量并行硬件引擎,现在我们可以这样描述问题:
"这些是张量,请编译器决定如何高效部署"
编译器会回应:"我明白该怎么处理,知道如何直接把这些张量部署到GPU上"。于是我们可以将这些运算映射到张量核心。
由于cuTile已深度集成到CUDA平台(作为Cuda的基础扩展),所有代码可移植性和跨代兼容性保障都自动生效。这意味着用张量表达的算法可以运行在任何架构上,无论底层硬件如何演进。

cuTile已深度融入CUDA平台架构。如果你观察过Sharphide(可能是指某个开发环境或工具链),会发现它其实一直存在于技术图谱中,只是我直到现在才点明。重要的是要理解:
这不是简单的另一个库,而是作为CUDA语言和平台的核心组件嵌入其中。这意味着它具备Cuda的所有运行特性——CUDA能运行的地方它就能运行,享有相同的稳定性和可移植性保障。

它支持Windows和Linux系统,能与cudagraphs等工具无缝集成。我们配备了完整的调试工具链和其他支持组件,这是CUDA 生态的自然延伸。但它提供了一种全新的编程范式——如果问题能在张量层面或分块层面解决,就无需深入线程级操作。
新的分块编程模型正是你需要的目标架构。我的同事Mark Harris(他在NVIDIA的时间比我还长)已经催了我十五年:"快做个简化版的CUDA 吧!" Mark,我们终于做到了!现在既能保持CUDA的性能优势,又能大幅简化开发流程。

这将极大扩展GPU加速的应用范围。当然,这需要时间积累。虽然今年晚些时候的首个版本就能支持混合使用分块内核和线程内核,但我们的终极目标是让不同粒度的计算单元在同一个内核里协同工作。
因为有些问题适合用规则数组处理,有些(如压缩算法或哈希表)则更适合线程级操作。开发者应该根据需求混合搭配,在单一计算粒度内使用最合适的工具。这正是我们努力的方向。

回到之前的观点,这种设计本质上更符合Python哲学。现代Python开发者不会纠结线程细节,而是直接操作数组。Numpy就是数组,数组相加生成新数组——这种思维方式与cutile完美契合。

我左手边有个NumPy示例,而cutile采用基于分块的编程模型。你需要将数据拆分成合适的片段——因为自动拆分未必最优,你最清楚如何按列、按行或按特定形状拆分数据。记住,优化要针对具体问题,开发者最了解自己的需求。
虽然操作的是分块,但语法层面仍是熟悉的数组运算。我认为这种范式本质上更直观:开发者无需深入线程细节,只需关注数据分块策略,编译器会自动处理并行化和内存优化。
虽然我们的首要任务是推出Python接口,但作为CUDA生态的一部分,同时也会提供C++扩展,支持完整的分块类型编程。当前展示的语法仍在开发中,但可以看出左右两边的代码高度相似。
左边是从网上找的NumPy矩阵乘法示例,使用共享内存等底层线程级操作。右边的代码显然更易读,逻辑更清晰,性能却完全相同。开发者无需深入线程细节,只需关注数据分块策略,编译器会自动处理并行化和内存优化。

我们用cutile实现了Llama 3.1推理引擎,并通过PyTorch部署——毕竟没人想从零造轮子实现KV cache。就像将手工优化的内核注入PyTorch那样,我们利用yTorch工具完成了这项部署。
图表对比显示:标准Torch Egle后端速度约为cuDNN的一半,而cutile的实现竟能惊人地接近PyTorch的cuDNN后端。

要知道cuDNN是用C++编写,且经过多年手工调优的。而我们仅用几周时间,就基于全新编译器和渐进式优化,在Python环境下实现了与手工优化cuDNN相差仅10%的推理性能。