跳转到主要内容
  • Daily Report
  • eBPF
  • Heterogeneous Systems
  • SmartNIC
  • GPU
  • Runtime Systems
  • Offload

异构系统里的 eBPF 到底应该运行在哪里?

eBPF 已能运行在内核、用户态、SmartNIC 与 GPU 上。真正缺少的是执行放置机制:跨目标选择和迁移时,状态、安全、权限与效果语义必须保持可解释且可验证。

设想一个服务:数据包先到 SmartNIC,应用在用户态解析请求,张量随后进入 GPU,其中一部分工作集又放在 CXL fabric 后面。现在希望部署一条统一策略:入口处拒绝异常流量,运行过程中统计昂贵的数据搬运,并在 GPU 队列拥塞时调整执行方式。

今天已经不止一个位置可以运行 BPF 逻辑。网络策略可以留在 Linux 内核,也可以下沉到可编程 NIC;高频应用函数可以由 bpftime 一类用户态运行时直接插桩;GPU 策略既可以放在 host driver 一侧,也可以像 gpu_ext 那样进入设备侧运行时;更近期的 fabric_ext 甚至把一条策略拆到 GPU、driver/runtime、DPU/NIC 与 CXL 侧执行。

一个很自然的答案是:离事件越近越好。

问题是,策略不只依赖事件。

某个 packet 在 NIC 上最早可见,但策略表可能由 host 统一维护;GPU 侧执行可以省掉一次 host 往返,却未必拥有内核 helper、map 或系统安全权限;程序一旦移动,pointer 的含义、map 更新何时可见、side effect 的时序,以及故障后能留下多少证据,都可能一起改变。

本文的结论是:异构 eBPF 需要一个独立于 BPF ISA、也独立于单个 backend runtime contract 的执行放置层。 第一层问题是“这个目标能不能按要求运行程序”;放置层要回答的是“多个目标都能运行时,应该由谁执行、由谁持有状态和效果,以及位置变化时怎样避免语义悄悄变化”。

第一版不应该直接追求 CPU、NIC、GPU 之间的透明动态迁移。更稳妥的实现是静态或低频变化的 planner:先把约束写清楚,只在语义有效的目标中选择,并记录为什么这么选。只有 benchmark 证明 workload phase 会让静态放置长期吃亏,才值得加入运行时迁移。

这是当前 eBPF Runtime, Extensibility, and Composition 系列的第六篇。系列第一篇讨论了用户态 eBPF 的可移植运行时契约;那篇回答一个 backend 是否提供程序依赖的 attach、capability、state 与 lifecycle 语义。本文向上再走一层:如果好几个 backend 都满足契约,今天到底应该让谁来执行?

指令可以兼容,执行环境仍然可能完全不同

BPF ISA 已经比它周围的运行环境更容易移植。RFC 9669 定义了 BPF 指令集与 conformance group。runtime 至少实现基础组,也可以声明更多组,compiler 因此能够知道目标支持哪些指令能力。

但 instruction group 无法告诉我们这个目标能看到哪些事件、ctx 中哪些字段可读、哪些 helper 能调用、哪些内存可访问,也无法回答程序究竟能 deny、修改、调度还是只能观察。

Linux 自身就是很好的反例。内核的 eBPF verifier 文档明确说明,不同程序类型会通过 is_valid_access() 限制不同的 context 访问,并通过 get_func_proto() 暴露不同函数集合。socket program 与 tracing program 可以执行同一套指令,却拥有完全不同的上下文和能力。host environment 本身就是程序语义的一部分。

硬件 offload 更能说明这个边界。当前 Linux kernel/bpf/offload.c 把 offloaded program 与 map 绑定到注册的 network device 和 device ops;device-bound 初始化路径围绕 SCHED_CLSXDP 工作,而不是接受任意 BPF 程序和任意异构设备。Linux 已经有真正的 hardware offload,但这个模型有明确的网络子系统边界。

所以,一个通用 placement layer 不能只问“设备会不会跑 eBPF”。它至少还要回答下面这些问题:

问题ISA 能回答吗?放置决策需要吗?
目标能否执行所需指令需要
能否看到所需事件与 context不能需要
是否提供所需 helper / kfunc 类能力不能需要
策略状态实际放在哪里不能需要
哪些 memory domain 可直接访问不能需要
程序能 deny、修改、调度还是只能观察不能需要
哪个主体授权这些 effect不能需要
这个位置会引入多少 event/state crossing不能需要

前一篇 runtime-contract 报告强调 backend capability 应该可以被机器验证。放置问题则是在这些 capability 之上再做一次选择:一个 backend 即使兼容,也不一定是这条策略最合适的执行位置。

现有系统已经在用不同的答案

异构 placement 并不是未来才会出现的问题。已有系统之所以选择不同位置,正是因为 event locality 会直接决定什么机制做得出来。

hXDP 把 XDP 放到 FPGA NIC 上

hXDP 展示了真实 XDP 程序可以直接在 FPGA NIC 上执行,而不是继续占用 host CPU。系统包含优化 compiler、扩展 BPF ISA、soft processor,以及 FPGA 上的 XDP map/helper 支持。OSDI 2020 论文报告其原型约占 15% FPGA 资源,在测试中达到高端 CPU 单核相当的 packet-processing throughput,同时把 packet forwarding latency 降低约 10 倍。

这个结果并不意味着所有 BPF 程序都应该搬到 NIC。它真正说明的是:当 event、state path 与 execution environment 都适合设备侧执行时,specialization 可以带来很大收益。为了做到这一点,hXDP 仍然需要显式重建目标所需的 map、helper 与执行语义。

用户态运行解决的是另一种 locality

用户态 eBPF 处理的是不同的问题。bpftime 可以在应用函数等用户态事件附近执行 BPF,不必让每一个 hot event 都走一次内核 probe 路径。它还包含 attach、map、helper、verification 与兼容层,因此不是一个单纯的 bytecode interpreter。

这种 placement 能直接接触应用自己的控制点,但也改变了程序可以假设的环境。kernel-only helper 与内核对象访问不会因为 ISA 一样就自动出现在用户态。event path 更短,并不意味着 process runtime 在语义上等价于 kernel。

gpu_ext 把策略推进 GPU 边界以内

gpu_ext 的出发点就是 host-only placement 的盲区。论文指出,host-side eBPF 看不到一部分与 GPU memory placement、scheduling 和 observability 直接相关的 device-side event。它因此在 GPU driver 暴露 hook,并加入 device-side eBPF runtime,让经过验证的 policy logic 可以在 GPU kernel 内执行。论文在其 workload 上报告最高 4.8 倍 throughput 提升,以及最高 2 倍 tail-latency 改善。

这里更值得关注的是 event boundary,而不是最大加速数字。host 上的策略可以很安全,也可以很快,但如果它看见事件时已经太晚,就实现不了原本的控制目标。

把这几个系统放在一起看,会发现“一个抽象机器、多个等价 backend”这个模型并不成立。kernel、userspace、NIC 与 GPU 分别提供不同的 event locality、memory visibility、verifier 假设、state cost 与 effect authority。

“靠近数据”只解决了放置目标的一半

把程序放到事件附近可以减少 crossing,但事件只是一次策略决策的一个输入。实际 placement 至少同时面对五种 locality。

事件 locality:触发点在哪里最早可见。packet arrival 更靠近 NIC 或 network stack;GPU allocation 和 queue event 更靠近 driver/device;用户态函数调用显然更靠近 process。

状态 locality:map、counter、sketch、policy table 或 learned state 现在由谁持有。per-queue counter 可以完全 device-local,但同时服务 networking、storage 和 GPU 的 tenant policy table 往往需要 host authority,或者至少需要明确的 replica protocol。

内存 locality:程序能直接看到哪个 address space。host virtual address、GPU global memory、NIC SRAM 与 CXL-attached memory 并没有统一的一致性模型。在一个目标上合法的 pointer,换个目标可能毫无意义。

效果 locality:真正的控制动作在哪里发生。若目标是在 DMA 前拒绝 packet,那么 host 上晚到的 observation 帮助有限;若策略要影响 GPU device scheduling,粗粒度用户态 event 也可能不够。

权限 locality:谁有权产生这个效果。device-local policy 不应该悄悄放宽 host security 已经拒绝的操作;反过来,host 可以把一小段 bounded scheduling choice 委托给设备,却没有必要同步处理每一个 fast-path event。

这几种 locality 经常指向不同位置。

例如一个多租户 GPU service,把共享 quota table 放在 host,却希望根据 device queue event 限流。至少有三种放置方式:

  1. 全部决策留在 host,接受频繁 event crossing;
  2. quota table 复制到 device,再承担 consistency protocol;
  3. host 只维护并发布 budget generation,device 在一个 generation 内执行局部有效的 fast-path 限制。

第三种往往比“所有东西都应该在一个地方”更现实。因此 placement layer 不只决定代码运行在哪里,还要决定authoritative state 留在哪里

最接近这个问题的工作已经能够跨多个设备

讨论 placement 时不能绕开 fabric_ext,也不能声称跨设备 eBPF lowering 还没人研究。

fabric_ext 面向 GPU-CXL fabric。它用 semantic movement graph 描述 source/destination、bytes、stride、reuse、ordering、ownership 和数据变换,再由 compiler 生成多个设备上的 eBPF program、verifier obligation、按 consistency 分类的 BPF map,以及对应 backend artifact。一条策略可以分布到 GPU hook、driver/runtime、DPU/NIC、CXL switch 或 near-memory target。

这已经证明了一个很重要的事实:只要 source policy 暴露足够的语义,compiler 可以把执行拆到不同硬件位置,同时把 state 与 verification 纳入 lowering 过程。

因此,本文剩下的问题必须更具体,而不是重新发明“跨设备 eBPF”。这里关注的是任意 event-driven BPF policy 在多个 eligible target 之间的执行放置,而不只限于 GPU-CXL data movement transformation。与此同时,第一版也不假定 arbitrary program 可以自动 partition。开发者显式标出 component boundary,planner 在 whole-program 或这些 boundary 之间选位置,就已经足够形成一个可验证系统。

两者可以这样区分:

  • fabric_ext 研究一个 semantic data-movement graph 如何根据已知 fabric topology 做 lowering;
  • 本文研究 BPF policy 面对 networking、application、GPU runtime、driver、device 等不同 event source 时,需要什么 target contract、placement objective、state ownership 和 evidence,才能选择执行位置。

如果后续实验发现 fabric_ext 的 graph 很自然地就能覆盖这些 event class 和 effect contract,那么没有必要再造一个 placement abstraction。那会直接否定本文的一部分设计假设,反而是很好的结果。

移动程序很容易顺手改变语义

真正危险的情况发生在部署以后改变 placement。

假设一个程序不断增加 map counter,超过 threshold 后拒绝某类操作。在 host 上,这个 update 也许立刻对其他 host program 可见;到了 NIC 或 GPU,同一个“逻辑 map”可能变成 device-local、mirror、cache,或者每次访问都绕回 host。程序代码完全没变,threshold 何时生效却变了。

时间与 ordering 同样如此。host-side event 可能在设备已经接收 work 之后才被看到;device-side policy 可以更早做决定,但 host trace 未必及时拥有足够 context 来解释原因。每个 target 单独都正确,并不保证跨 target 的 observation 顺序仍然表达同一件事。

因此,透明 migration 不是好的第一目标。系统先要明确哪些属性必须保持不变:

  • 每个 policy generation 明确记录执行 target;
  • 每个 state object 明确记录 authoritative owner 与允许存在的 replica;
  • 新 target 激活前必须证明所需 context、helper、memory 和 effect capability;
  • placement transition 有 generation boundary,避免旧状态和新状态意外混用;
  • 任何对外有意义的 decision 都可以追溯到当时的 placement 与 generation。

这和之前的有状态 eBPF 事务化升级直接相关。placement migration 本质上也是 upgrade,只不过新 generation 同时换了 execution domain。既然单机内替换 program/map 都需要处理 prepare、commit 与 rollback,跨 memory domain 的迁移更不能靠“把 bytecode 再 load 一次”解决。

现有工作还缺什么

缺少一个通用的 BPF placement objective

现有系统通常先选目标,再在目标内部优化。Linux hardware offload 从 network device 上的 XDP/classifier 语义出发;hXDP 从 FPGA NIC 出发;gpu_ext 从 GPU policy 出发;fabric_ext 从 GPU-CXL movement graph 出发。

缺少的是:当一条 policy 同时有两个以上合法执行位置时,如何用同一组约束比较它们。“离数据最近”没有计算 shared-state traffic、authority boundary、verifier difference,也没有计算丢失诊断证据的成本。

这个 objective 也不应该把所有东西硬塞成一个加权总分。更可靠的方法是先排除语义非法的 placement,再对剩余目标比较一个小的 Pareto set,例如 event crossing、state crossing、latency、throughput、host load 与 evidence completeness。

跨 target 状态没有共同的故障语义

BPF map 是熟悉的编程模型,但 kernel、GPU、NIC 与用户态里的“map”不会自动变成一份 coherent object。所有 update 都同步,可能把 offload 带来的性能收益抵消掉;所有 state 都 local,又可能让 split policy 直接出错。

这里真正需要的不是“大一统 distributed map”,而是每个 state object 自己说明:authoritative owner 是谁、哪些 replica 可以 stale、哪些 update 可交换、什么时候必须建立 generation fence、设备在迁移中消失时如何恢复。

这个问题也可能有一个刻意简单的答案。大量实际 policy 或许只需要 device-local counter 加一张很小的 host-authoritative config table。如果这已经覆盖大多数 workload,就不值得构建复杂的全局一致 map system。

现有 evaluation 很少直接回答“应该放在哪里”

accelerator paper 通常把自己选择的 target 与 host baseline 比较。这足以证明某种加速方式有没有用,却无法回答另一个 target 会不会更好,也无法解释什么 workload property 决定赢家。

placement mechanism 需要一类不同 benchmark:同一条 logical policy 与同一组 expected effect 必须至少能在两个位置执行。否则 planner 只能证明自己的 cost model 自洽,而不能证明它做出了更好的系统设计。

可观测性也是 placement contract 的一部分

policy offload 也会把一部分“解释权”下放到设备。device 负责 deny、schedule、memory placement 或 transformation 后,operator 至少需要知道当时是哪一版 policy、在哪个 target、对哪类 input、产生了什么 effect。

所有 event 都同步回 host 会破坏 fast path;所有证据只留在 device 又很难重建跨 target incident。因此 placement 不仅需要 execution budget,还需要 evidence budget。

值得继续实现和验证的方向

1. Target manifest 与 placement planner

缺口。 runtime 可以告诉用户“支持 BPF”,却把 event、helper、memory domain、authority 与 crossing cost 都留在文档里,让上层自己猜。

机制。 每个 target 提供机器可读 manifest,声明 instruction conformance group、可用 event/attach class、context version、helper/kfunc 类能力、map/state class、可读 memory domain、允许产生的 effect、authority constraint,以及测得的 event/state crossing cost。policy 另一侧声明需要的 event、state、effect 和 latency bound。planner 先做语义筛选,再从合法 placement 中按实际成本选择。

第一版只放置 whole program 或开发者显式声明的 component,不尝试自动切 arbitrary bytecode。

与现有工作的差异。 前一篇 userspace-runtime contract 判断一个 backend 是否满足语义;这里要在多个满足条件的 backend 之间做选择。相对 fabric_ext,这个 manifest 面向超出单一 fabric data-movement domain 的 event-driven policy。

产物。 一份 schema,Linux kernel BPF、bpftime 用户态、NIC/FPGA 与 GPU-side target 的 adapter,一个小型 constraint solver,以及能够解释“为什么这个位置不合法/为什么选这里”的命令行工具。

评测。 选择至少有两个合法 placement 的 workload,例如 packet filtering/accounting、用户态 hot function policy、GPU memory/scheduling control 和一条 cross-device policy。比较 expert manual placement、always-host、always-near-event 和 planner。测量非法 placement 拦截率、event/state crossing、p50/p99 decision latency、throughput、host CPU、device utilization 与 planner overhead;对适用的 data-movement workload,再和 fabric_ext 的 placement 做比较。

学术价值。 验证一份紧凑 target contract 是否真的能够预测跨 execution domain 的 Pareto frontier,而不是把某个 accelerator 的假设写死进去。

生产价值。 部署前可以得到“这条 policy 在这台机器上应该放哪里”的机器可检查答案,而且能够解释选择原因。

失败条件。 如果 manifest 最后只是互不相干的 backend feature catalog,或者人工静态 placement 在所有 workload 上都和 planner 一样好,这个通用 planner 就没有足够价值。

2. 以 generation 为边界的状态归属和迁移

缺口。 代码不变,只要 execution location 改变,map visibility 和 update ordering 就可能改变。

机制。 每个 state object 显式声明 owner,并限制在少量 consistency mode 中,例如 target-local、host-authoritative、device-authoritative、replicated read-mostly。迁移时创建新 policy generation:先验证新 target,只搬运该 mode 允许迁移的 state,建立 fence,然后激活新 generation。在 commit 前,旧 generation 与它的 authoritative state 保留,用于 rollback。

这个方案刻意不承诺“所有 map 全局 coherent”。无法安全迁移的 state 直接让某个 placement 变成 ineligible。

与现有工作的差异。 transactional eBPF upgrade 已经需要 program/link/map generation boundary;异构迁移在此基础上加入 memory-domain ownership 与 crossing semantics。

产物。 generation controller、state-description schema,以及少量 host/device map class 的 state bridge。

评测。 用 counter、sketch、quota table 和 read-mostly policy table 跑 network/GPU workload,并注入 device reset、controller crash、stale replica 与 transition 中断。测 lost/duplicated update、stale-read rate、迁移 pause、transfer bandwidth、rollback correctness,以及 local、host-authoritative、replicated 三种方式的性能差异。

学术价值。 找出哪些 state semantics 能跨 heterogeneous BPF runtime 实现,同时又不退化成一个通用 distributed shared-memory system。

生产价值。 只有 state contract 明确允许时 policy 才能移动,迁移失败从隐蔽 correctness risk 变成一个可以在 activation 前拒绝的操作。

失败条件。 如果大多数有用 BPF state 天然就是 target-local,或者 host-authoritative state 的成本已经很低,那么 generic cross-target migration 可能根本不值得做。planner 应该让 state 保持不动,只迁移 stateless component。

3. 带 ground truth 的 placement 与 provenance benchmark

缺口。 现有 evaluation 能证明某个 offload target 很有效,却很难回答“什么时候应该选它”。

机制。 benchmark 把 workload 写成 event graph、state object、expected externally visible effect 与 eligible target。每一次执行都记录 placement generation、boundary crossing、state movement,以及 planner 的选择原因。独立 ground-truth checker 根据最终 effect 判断正确性,而不是相信 runtime 自己的日志。

benchmark 同时包含 stable workload 与 phase-changing workload。后者尤其重要,因为它能直接回答动态 re-placement 有没有实现价值。

产物。 一套 harness,至少包含 packet processing、用户态函数控制、GPU resource policy 和一个 cross-fabric data-movement case,并复用 planner 所支持的 target adapter。

评测。 跨 placement 比较 throughput、p99 latency、host CPU、device utilization、transfer bytes、state staleness、semantic check failure 与 observability completeness,再检查 planner 是否能预测最佳合法位置,以及 phase change 带来的收益是否大于 generation switching cost。

学术价值。 把“靠近数据执行”从经验口号变成可证伪的系统假设,找到真正决定 placement 的 workload 特征。

生产价值。 在生产环境开启 offload 或 migration 之前,可以用同一套 suite 做 target selection regression test。

失败条件。 如果不同 target 上的 policy 语义差异太大,同一 logical effect 根本无法在两个 backend 表达,那么 generic placement benchmark 本身就是错误抽象,应该回到 target-specific evaluation。

我会先做哪个版本

我不会一开始就做一个在 CPU、NIC 与 GPU 之间不断搬程序的 distributed eBPF scheduler。那样一次性叠加了 runtime compatibility、state transfer、cost prediction、target failure 和 explainability,失败时很难知道问题出在哪一层。

更小的第一版只做三件事:

  1. 用机器可读形式描述 target capability 与 policy requirement;
  2. 排除非法位置,在 activation 前选择一个 placement;
  3. 记录 placement generation、state owner 与实际 crossing cost,让决策之后可以 replay。

然后把同一个 workload 放到几个合法位置实际跑。如果稳定 workload 的赢家基本不变,就保持 static placement;如果不同 phase 的最佳 target 经常变化,而且收益明显高于 transition cost,再为少数可以安全搬运的 state type 加 generation-scoped migration。

这样的顺序更容易验证。现有系统已经足以说明 specialization 与设备附近的 control point 有真实价值,但还没有足够证据证明透明动态迁移是大多数 workload 都需要的功能。

什么证据会改变这个结论?

有三类结果会削弱“需要通用 placement layer”的判断。

第一,如果绝大多数 policy 从一开始就只有一个语义合法 target,target-specific system 已经覆盖实际部署,那么 planner 没有真正的选择空间,做好 backend-specific tooling 就够了。

第二,如果 fabric_ext 的 semantic movement graph 或类似已有抽象可以自然表达 networking、userspace、GPU policy、device effect 与 state contract,就应该直接扩展那套 abstraction,而不是另外再做一个竞争的 placement model。

第三,如果真实 workload 中 cross-target state 和 evidence traffic 的成本始终高于 offload 节省的 event cost,那么“靠近事件”并没有想象中划算。更合适的架构会把 authority 与 state 留在 host,只把很窄的 stateless predicate 或 preprocessing 下沉到设备。

当前证据支持一个更克制的结论:eBPF 没有唯一最佳执行位置。真正缺少的是一套可以验证的 placement decision,用来处理 event、state、memory、authority 与 verifier semantics 都不同的多个 target。 先把静态 placement 做成可检查机制,再用 benchmark 决定是否值得获得动态迁移能力。