前言
你是否遇到过这样的情况:在昇腾NPU上写了一个 PyTorch 模型,前向传播跑得飞快,结果换了一个自定义 C++ 算子之后,整个推理管道突然慢了一截?明明硬件还是那块硬件,代码逻辑也没变,怎么就卡了?这不是你的错觉,这是 Python 和 C++ 之间的那道"墙"在作祟。
昇腾异构计算架构(CANN)上运行的大模型推理系统,天然就是多语言混合体------Python 负责调度和控制流,C++ 负责算子的核心计算逻辑,底层还有 C 驱动与硬件寄存器打交道。这套分层本身是合理的,但不同语言之间的数据传递从来都不是免费的。当数据从一个语言的内存空间流向另一个语言的内存空间时,涉及的开销往往超出开发者的直觉预期。
CANN 生态下的 hixl 仓库,正是为了解决这类异构跨语言通信问题而诞生的。从能力定位来看,它是一个面向昇腾芯片的高效单边通信库,核心能力是提供零拷贝的数据传输通道,同时在 API 层面做了大量简化,让 Python 和 C++ 之间的互调不再成为性能瓶颈。这个仓库支持 D2D、D2H、H2D 等多种内存类型之间的传输,兼容 HCCS 和 RDMA 等高速互联协议,最高带宽可达 119GB/s(基于昇腾 A3 芯片和 HCCS 链路)。
接下来的内容,我会把"异构交叉调用"这个听起来抽象的概念拆开来讲,接着用生活里的例子来类比 hixl 的几个核心设计思路,再给出接入指南和代码示例。如果你在做 PyTorch 推理加速、大模型 PD 分离、或者任何涉及多语言协作的项目,这篇文章可能会帮你省下不少调优时间。
什么是异构交叉调用问题
从一道家常菜说起
想象你在一间开放式厨房里做菜。灶台是 C++ 算子(高温、精准、快速),而你本人是 Python 调度层(规划流程、判断火候、决定下什么料)。问题来了:当你从案板(Python 内存)把切好的菜端到锅里(C++ 内存)的时候,你需要用一个盘子作为中转。每次端菜都要洗盘子、拿盘子、装盘子、递盘子,菜本身并没有变,但中转这个动作本身消耗了大量时间。
在软件层面,这个"中转盘子"的问题体现在三个地方。
第一层是 Python GIL(全局解释器锁)。Python 的多线程机制并不能真正并行执行字节码,同一时刻只有一个线程在执行 Python 解释器字节码。当 Python 代码尝试调用一个 C++ 扩展时,GIL 需要被释放,控制权随之切换到 C++ 代码。这个获取和释放的过程并非零开销,在高频调用的场景下(比如每秒调用数万次小算子),GIL 切换本身会累积成可观的延迟。
第二层是 ABI 调用约定。Python 的数据对象(比如 PyObject指针、PyArrayObject)和 C++ 内部数据结构之间的格式并不兼容。PyTorch 的 tensor 在 Python 侧是一个 Python 对象,但到了 C++ 侧需要转换成 ATen 张量格式。这个转换涉及指针解引用、shape 解析、dtype 映射、stride 计算等步骤。数据本身可能只需要搬运一次,但格式适配的过程往往是多层函数调用栈,每一层都在做类型检查和内存分配。
第三层是数据格式转换产生的临时拷贝。Python 侧的 NumPy 数组或者 PyTorch tensor,其内存布局与 C++ 侧期望的布局往往存在差异。比如 row-major 和 column-major 的差异,比如连续内存和分段内存的差异,这些差异迫使系统至少要创建一份中间 buffer 来做格式适配。即使框架层做了原地操作(in-place)的优化,格式转换逻辑本身也必须在运行时执行。
把这三层叠加起来,结果就是:当你在 Python 里写一个简单的 model(input_tensor) 时,如果 model 内部包含一个 C++ 自定义算子,那么从 Python 的 tensor 到 C++ 的数据结构的转换过程,会在每次推理迭代中重复执行。如果这个算子在一个 batch 的每条数据上都要被调用一次,那么转换开销就会直接叠加到整体延迟上。
PyTorch 推理延迟为什么会增加
在纯 PyTorch 的场景下,数据一直在 PyTorch 的内部世界里流转,格式高度统一,不需要频繁跨越语言边界。但当你引入自定义 C++ 算子时,情况就变了。
考虑一个典型的推理场景:Prefill 阶段由 Python 调度层驱动,输入 token 先经过 embedding 层(在 Python/PyTorch 中),紧接着需要调用一个 attention kernel(可能是 C++ 实现)。这个 kernel 期望的输入格式是某种特定的数据排布,但 PyTorch 传过来的 tensor 在 layout 上不完全一致。于是,数据在进入 kernel 之前要先做一次格式调整。Prefill 完成后,进入 Decode 阶段,每个新 token 都要重复这个过程。由于 Decode 是自回归的,每个 token 都会触发一次前向传播,每次前向传播都会遇到同样的数据转换开销。这就是为什么 PyTorch 推理在引入自定义算子后延迟会明显增加------不是因为计算本身变重了,而是因为"搬运"的次数变多了。
此外,在大模型推理的 PD 分离架构(Prefill-Decoder 分离)中,Prompt 侧的 Prefill 结果(比如 KV Cache)需要从 Prefill 节点传输到 Decoder 节点。这个跨进程甚至跨节点的数据传输,如果用传统的序列化+Socket 方式来做,数据会在内存中经历从 NPU 到 Host、再从 Host 到网络的多次拷贝,每一跳都是一次完整的格式序列化和反序列化。hixl 解决的正是这个层面的问题------它提供了跨节点零拷贝的直接内存访问能力,数据不需要经过中间缓冲区,直接从一块 NPU 的显存传输到另一块 NPU 的显存。
hixl 的核心设计思路
零拷贝数据通道
hixl 的"零拷贝"并不是说数据完全不需要搬运------数据从节点 A 的显存到节点 B 的显存物理上肯定是要移动的------而是说在整个传输路径中,数据不会因为格式转换或中间缓冲而产生额外的内存拷贝操作。
在传统的 RPC 方式中,数据从 A 节点的 Python 进程到 C++ 运行时,再到网络缓冲区,一路到达 B 节点的接收端,整个链路中数据可能被序列化了多次、拷贝了多次。每一层都需要把数据放到自己的缓冲区里,再由下一层来读取。零拷贝的思路是:跳过中间缓冲区,让发送端和接收端的内存区域建立直接的映射关系。hixl 底层依赖 RDMA 或 HCCS 这样的高速互联硬件,这些硬件支持"注册内存"机制------把一块用户空间的虚拟内存地址注册到硬件,硬件可以直接绕过操作系统内核去访问这块物理内存,从而实现跨节点的高速数据传输而不需要经过内核协议栈。
具体来说,hixl 的零拷贝路径是这样的:用户在 Python 或 C++ 侧准备好数据(已经在 NPU 显存中),调用 hixl 的传输接口,hixl 将这块已注册的内存地址告知硬件,硬件直接执行 DMA 传输,数据从源头到目的地,中间不经过任何中间 buffer。这条路径的设计取舍在于:省掉了中间 buffer 意味着省掉了两次内存读写(写入 buffer 和从 buffer 读取),但代价是需要提前注册内存区域,让硬件知道如何访问这块物理地址。注册过程本身有一次性开销,所以零拷贝更适合大块数据的传输场景(比如 KV Cache 的批量传输),对于极小的控制消息,零拷贝的注册开销反而可能得不偿失。
延迟绑定
"延迟绑定"(Lazy Binding)这个概念,可以类比为餐厅里的"先点菜、后做菜"模式。传统方式下,当你走进餐厅,服务员会立刻把所有菜都做好端上来,不管你实际吃不吃得完。这种" eager"(急切)的方式对应到跨语言调用中,就是每次调用时都把所有可能需要的数据都打包好、传输好,哪怕这次调用根本不需要其中某些数据。
延迟绑定则更聪明一些:你先告诉厨房你可能会点什么菜,厨房把准备工作做好(切配、调料预混),等你真正点了某道菜时,厨房只需要执行最终的烹饪步骤就行,不需要再从头开始准备原材料。在 hixl 中,延迟绑定体现在:连接建立、内存注册、传输资源配置这些准备工作在初始化阶段一次性完成,而具体的数据传输操作等到真正需要时才触发。这样,每次传输操作的路径被压缩到了最小------只有 DMA 指令的发起和数据在硬件层面的移动,没有额外的协议握手或资源发现开销。
批量请求合并
假设你要从上海寄一批快递到北京,每件单独寄,每件都要填单子、称重、装车、运单追踪,成本很高。但如果把这批快递打包成一个集装箱一起运,单件的平均成本就大幅下降了。批量请求合并的思路与此类似:当多个小尺寸的数据传输请求同时到达时,hixl 不会立即为每个请求单独发起一次 DMA 操作,而是先把这些请求暂存到内部队列中,等到积累到一定数量或者达到时间窗口阈值后,统一打包执行。
这样做的好处有两方面。一方面,硬件层面一次批量 DMA 的吞吐量远高于多次小尺寸 DMA 的吞吐量之和,因为每次 DMA 都有固定的启动开销(命令构造、硬件队列排队、延迟确认),批量操作可以把启动开销分摊到多个数据块上。另一方面,批量合并减少了对硬件队列的竞争,在高并发场景下避免了频繁的队列切换和锁争用。设计上的权衡在于:批量合并会引入一定的排队延迟(数据要等一下才能发出),所以 hixl 提供了可配置的时间窗口和批次大小参数,让用户根据实际业务对延迟的敏感程度来调整合并策略。
快速接入 hixl
安装方式
hixl 库目前提供两种安装途径:pip 预编译包和 conda-forge 通道。对于大多数用户来说,pip 是最直接的选择。
bash
pip install cann-hixl
如果你是 conda 环境的管理者,也可以通过 conda 来安装:
bash
conda install cann-hixl -c conda-forge
安装完成后,可以验证一下:
python
import hixl
print(hixl.__version__)
hixl 作为 CANN 生态的一部分,预编译包中已经包含了针对昇腾芯片的优化过的 native 库,不需要用户手动编译底层 C++ 代码。conda 通道的提供是为了照顾科学计算场景中广泛使用 conda 环境的研究团队。
环境变量配置
hixl 的行为受多个环境变量控制,在多机多卡场景下需要正确配置。
bash
# 指定使用的昇腾设备 ID
export ASCEND_DEVICE_ID=0
# 启用 HCCS 传输协议(用于单机内或超节点内通信)
export HCCL_INTRA_ROCE_ENABLE=1
# 指定 RDMA 网卡名称(跨节点通信时必须)
export GLOO_SOCKET_IFNAME=enp67s0f5
# 设置日志级别(INFO/WARN/ERROR/DEBUG)
export HIXL_LOG_LEVEL=INFO
HCCL_INTRA_ROCE_ENABLE 看起来名字里有 HCCL,但它控制的是 hixl 在超节点内的传输链路选择------开启后走 HCCS 直连通道,不经过传统网络协议栈,延迟更低。GLOO_SOCKET_IFNAME 则指定了用于跨节点 RDMA 通信的网卡,这在使用 RoCE 协议的超节点间传输时是必需的。
初始化顺序与依赖关系
在使用 hixl 的 Python 接口之前,必须先正确初始化 CANN 的基础层。这个顺序是有约束的,不可以颠倒。
python
import acl
# 第一步:初始化 ACL(昇腾计算语言层)
acl.init() # 必须在 hixl 之前调用
# 第二步:初始化 hixl
import hixl
hixl.init()
# 此后就可以正常使用 hixl 的 API 了
# 使用完毕后按反序销毁
hixl.finalize()
acl.finalize()
acl 是 CANN 生态的最底层运行时抽象,负责管理昇腾 NPU 的上下文、设备内存分配和硬件资源句柄。hixl 的底层传输引擎依赖 ACL 分配的设备内存和句柄,所以在逻辑上必须建立在 ACL 初始化完成之后。finalize 的反序调用也是同样的原因------hixl 需要在 ACL 释放设备资源之前先释放自己持有的传输句柄,否则 ACL 会报资源泄漏错误。
性能对比与适用场景
为什么需要 hixl 的零拷贝方案
在大模型推理的 KV Cache 传输场景中,数据量通常非常大。一次 Decode 请求涉及的 KV Cache 可能达到几百 MB 甚至 GB 级别。如果用传统方式进行传输,数据在发送端要经历 NPU 显存到 Host 内存的拷贝(pinned memory),接着经过序列化后发到网络缓冲区,再到接收端的网络缓冲区反序列化,再拷贝到目标 NPU 显存。这个链路中至少包含了 4 次内存拷贝操作,而且序列化/反序列化本身还要消耗 CPU 周期。
hixl 的零拷贝方案在硬件层面支持直接内存访问,数据只需要经过一次物理层面的 DMA 传输就可以到达对端显存,整个过程不需要经过操作系统的网络协议栈,也不需要额外的中间 buffer。根据官方 README 中的数据,在昇腾 A3 芯片上使用 HCCS 链路传输 128MB 数据时,hixl 的带宽可以达到 119GB/s;使用 RDMA 链路时,带宽可以达到 22GB/s。这个数字意味着传输 1GB 的 KV Cache 数据在 HCCS 链路上只需要不到 10ms,而在传统 TCP 方式下通常需要数十毫秒甚至更高。
以下是从多个维度对比使用 hixl 前后差异的数据表格。
| 对比维度 | 传统方案(序列化+Socket) | hixl 零拷贝方案 | 差异来源分析 |
|---|---|---|---|
| 单次传输延迟 | 数据需经多次内存拷贝和序列化,延迟随数据量线性增长 | 零拷贝直传,延迟主要由硬件带宽决定 | 省掉了中间 buffer 写入和读取,以及序列化 CPU 开销 |
| 内存带宽占用 | 发送端和接收端各需要额外的中间 buffer,内存占用翻倍 | 数据直接在用户内存和硬件之间传输,无需额外 buffer | 消除了中间 buffer 造成的内存空间浪费 |
| 吞吐量(多链路) | 单一 TCP 连接容易成为瓶颈 | 支持多链路聚合传输,带宽可线性扩展 | 底层支持链路池,硬件资源利用率更高 |
| 跨节点通信能力 | 依赖标准网络协议栈,跨 IDC 场景性能受限 | 支持 RDMA/HCCS 跨节点直连 | 硬件直连避免了网络协议栈的额外处理开销 |
hixl 适用的典型场景
第一个典型场景是大模型推理的 PD 分离架构。Prefill 节点和 Decoder 节点各自持有完整的模型副本,Prefill 阶段生成的 KV Cache 需要高效传输到 Decoder 节点继续使用。hixl 的 LLM-DataDist 组件专门为此场景优化,提供了携带 KV Cache 语义的传输接口,可以直接对接 vLLM 和 SGLang 等主流推理引擎,传输延迟比传统方案有显著改善。
第二个典型场景是 RL 后训练中的参数切换。策略模型和价值模型的参数在不同训练步骤之间需要频繁切换,切换过程中涉及大量参数数据的传输。hixl 的批量传输能力可以把多次小尺寸参数更新合并为一次大尺寸 DMA 操作,大幅降低参数同步的总体开销。
第三个典型场景是多卡推理中的 KV Cache 聚合。当单个节点的显存不足以容纳完整上下文时,需要把 KV Cache 分散到多张 NPU 卡上,统一传输到下一阶段。hixl 支持 D2D 直传,可以绕过 Host 内存直接在各卡之间搬运数据。
hixl 不适用的场景
hixl 并不是万能药。有些场景下使用它反而会增加复杂度而得不到预期收益。
单卡、单进程内部的 Python 到 C++ 函数调用,并不适合用 hixl 来优化------hixl 解决的是跨节点、跨进程的传输问题,而不是同进程内语言边界之间的调用开销。这类问题应该通过 PyTorch 的自定义算子注册机制(torch.library)或者 TorchScript 融合来解决。
传输数据量极小的场景(比如几十字节的控制消息)也不适合 hixl。hixl 的零拷贝机制需要提前注册内存区域,注册操作本身有固定开销。如果每次传输的数据量只有几十字节,注册开销会比实际传输开销还大。对于这类场景,直接用 HCCL 的点对点通信原语会更高效。
多语言混合编程示例
Python 调用 C++ 自定义算子的完整流程
这里给出一个完整的示例,演示如何在 Python 侧通过 hixl 来传输 KV Cache 数据到远端节点。示例包含内存注册、集群初始化、传输配置和实际传输四个步骤。
python
import torch
import hixl
# 初始化 hixl(依赖 ACL 环境变量)
hixl.init()
# 从配置构建集群信息
config = {
"device_id": 0,
"cluster_id": 1,
"role": "prompt", # prompt 侧还是 decoder 侧
}
cluster_info = hixl.LLMClusterInfo.from_dict(config)
# 申请一块 PyTorch tensor 并注册为远端可访问的内存块
x = torch.randn(1024, 1024, dtype=torch.float16, device="npu")
registered_blocks = hixl.allocate_cache(x, mem_type="device")
# 配置传输参数
transfer_cfg = hixl.TransferConfig(max_concurrent=4, timeout_ms=5000)
# 从远端 pull 数据到本地
task = hixl.pull_cache(
remote_rank=1,
local_cache=registered_blocks,
transfer_config=transfer_cfg,
)
# 等待传输完成
result = task.wait()
print(f"传输完成,状态码: {result.status_code}")
hixl.finalize()
allocate_cache 的作用是把 PyTorch tensor 的底层 NPU 内存页锁定并注册到 hixl 的传输引擎中。注册后的内存区域硬件可以直接访问,不需要操作系统介入。mem_type="device" 指定注册的是 NPU 设备内存;如果是 Host 内存则传 "host"。这里用 PyTorch 的 tensor 作为数据源是因为推理框架本身就在用 PyTorch 管理数据,直接复用已有的 tensor 可以避免额外的数据拷贝。
推送模式:从 Decoder 侧主动推送数据
在某些架构下,Decoder 节点是数据的持有方,需要主动把 KV Cache 推送给多个 Prompt 节点。hixl 支持 push 模式:
python
import hixl
hixl.init()
# Decoder 侧持有数据并注册
y = torch.randn(2048, 2048, dtype=torch.float16, device="npu")
remote_cache = hixl.allocate_cache(y, mem_type="device")
# 主动推送到远端 prompt 节点
push_task = hixl.push_cache(
remote_ranks=[0, 1, 2], # 推送到多个节点
src=remote_cache,
transfer_config=hixl.TransferConfig(async_enable=True),
)
# 异步模式下可以先去做其他计算
compute_result = do_other_work()
push_result = push_task.wait()
hixl.finalize()
push 模式支持一次操作向多个远端节点同时推送数据,hixl 底层会为每个目标节点发起独立的 DMA 流,但数据源 buffer 是共享的,不需要为每个目标单独拷贝一份数据。async_enable=True 开启了异步传输模式,调用 wait() 之前可以并行执行其他计算任务,实现传输与计算的重叠掩盖。
异步分层传输
对于超大规模的 KV Cache 数据,hixl 还提供了异步分层传输能力,可以在数据传输的同时进行部分解码计算:
python
import hixl
hixl.init()
# 注册大型 KV Cache
big_cache = torch.randn(8192, 8192, dtype=torch.float16, device="npu")
layers = hixl.allocate_cache(big_cache, mem_type="device")
# 配置分层同步器,实现逐层传输和计算重叠
syncer = hixl.LayerSynchronizer(
total_layers=32,
sync_interval=1, # 每传完 1 层就开始下一层的传输
)
async_task = hixl.transfer_cache_async(
remote_rank=1,
local_cache=layers,
synchronizer=syncer,
)
# 在传输过程中可以同时进行其他层级的计算
while not async_task.is_done():
current_layer = syncer.get_current_layer()
decode_layer(current_layer)
syncer.wait_sync()
hixl.finalize()
分层传输的核心设计思想是"传输-计算流水线化"。当第 N 层的数据还在传输时,第 N-1 层的解码计算就可以开始了。这种流水线结构在延迟敏感的场景下非常重要------用户感知的端到端延迟等于"计算时间 + 收尾层的传输时间",而不是"所有层传输时间之和 + 计算时间"。sync_innterval=1 意味着每传完一层就立即触发下一层的传输,这样可以最大化流水线并行的效果。
调试与排查
日志级别配置
hixl 提供了分级的日志输出能力,默认级别是 WARN。如果遇到初始化失败或传输异常,可以通过调整日志级别来获取详细的诊断信息。
python
import os
# 在代码开头设置环境变量
os.environ["HIXL_LOG_LEVEL"] = "DEBUG" # DEBUG 会输出每个传输操作的详细信息
import hixl
hixl.init()
# 也可以在运行时动态设置日志级别
hixl.set_log_level("INFO")
# 运行你的传输逻辑
transfer_and_compute()
hixl.finalize()
DEBUG 级别会打印出每次 DMA 传输的源地址、目标地址、传输大小和耗时,这些信息在排查传输失败或者性能不达预期时非常有用。但 DEBUG 日志量很大,在生产环境中应该切回 INFO 或 WARN 级别,避免日志文件膨胀。
常见错误码解读
hixl 和 LLM-DataDist 的错误码体系分为两层:通用 hixl 层错误码和 LLM-DataDist 业务层错误码。理解错误码的前缀有助于快速定位问题来源。
| 错误码 | 含义 | 常见原因 | 处理建议 |
|---|---|---|---|
| HIXL_001001 | 内存注册失败 | 指定的 tensor 内存区域已被占用或不在 NPU 上 | 检查 tensor 的 device 属性,确保在 npu 上 |
| HIXL_001002 | 连接建立超时 | 远端节点未启动或网络不通 | 确认两端节点都已执行 hixl.init() 且 cluster_id 匹配 |
| HIXL_001003 | 传输句柄无效 | 调用了已 finalize 的 hixl 实例 | 检查 finalize 和 init 的调用顺序,确保没有悬空引用 |
| LLM_002001 | 远端 Cache 区域不存在 | 远端未调用 allocate_cache 或角色配置错误 | 确认两端角色配置一致,prompt 和 decoder 不可互换 |
| LLM_002002 | 传输数据量超限 | 单次传输超过了注册的 Cache 大小 | 检查 registered_blocks 的 shape 和传输 tensor 的 shape 是否匹配 |
hixl 错误码的第一段数字(如 001)标识了错误所属的子系统,第二段数字(如 001、002)是该子系统内的具体错误编号。看到 HIXL 前缀的错误说明问题出在传输引擎层,可能与硬件链路或内存注册有关;看到 LLM 前缀的错误说明问题出在 LLM-DataDist 业务层,可能与集群配置或 Cache 管理有关。
hixl 与 ACL 错误的区分
在实际调试中,hixl 的错误有时会和底层 ACL 的错误混在一起输出。区分两者有助于缩小排查范围。
如果错误信息中包含 "acl" 关键字(如 "ACL error: memory allocate failed"),说明问题出在 CANN 的基础运行时层,与硬件驱动或 NPU 上下文有关。遇到这类错误时,排查的第一步是检查昇腾 NPU 驱动是否正常运行:npu-smi 命令能正常显示设备信息是排查的第一步。同时确认 CANN 软件包版本和驱动版本是否匹配------版本不匹配是导致 ACL 初始化失败的最常见原因。
如果错误信息中没有 acl 关键字,而是以 HIXL 或 LLM 开头,说明问题出在 hixl 库本身,可能是传输配置不正确或者集群成员之间的通信超时。遇到这类错误时,应该优先检查网络连通性(尤其是 RDMA 网卡的 link 状态),紧接着核对集群配置文件中的 cluster_id 和 role 是否在所有参与节点上保持一致。
还有一类容易混淆的错误:Python 侧的 tensor 已经在 NPU 上,但 hixl 报告无法注册该内存。这种情况通常是因为 tensor 的底层内存不是 page-locked 的。PyTorch 在 NPU 上分配的 tensor 默认是 page-locked 的,但如果你手动用 torch.empty() 或其他方式重新创建了 tensor,需要确认 device 设置正确。解决方法是始终使用 torch.empty(device="npu") 或 torch.randn(..., device="npu") 来分配设备内存,而不是先在 CPU 上创建再移动到 NPU。
结尾
hixl 是昇腾生态中专门针对跨节点零拷贝传输场景设计的通信库,它的核心价值在于通过单边 DMA 操作省掉了传统传输方式中的多次内存拷贝和序列化开销。在 KV Cache 传输这类大块数据场景下,它提供的带宽可以达到 119GB/s(HCCS 链路)或 22GB/s(RDMA 链路),远高于传统 RPC 方式。hixl 的 API 设计非常精简,核心调用数量控制在十几个,同时提供了完整的 Python 和 C++ 接口,可以直接集成到 vLLM、SGLang 等主流推理框架中使用。对于大模型推理的 PD 分离架构、RL 后训练的参数切换、以及多卡推理中的 KV Cache 聚合,hixl 都是值得考虑的技术选型。需要注意的是,hixl 主要面向跨节点传输场景,对于同进程内的语言边界调用问题,应该通过 PyTorch 的自定义算子机制来解决而不是引入 hixl。