想象你是一家大型餐厅的主厨,厨房里有好几个工作台:一个万能操作台(CPU)、几个火力强劲的炒锅工位(GPU)、还有专门为批量制作招牌菜设计的流水线工作站(TPU)。每道菜的食材放在哪个台面上,直接决定了出餐速度。如果你把所有食材都堆在操作台上,炒锅工位就闲着;如果频繁把食材在台面间端来端去,时间全浪费在路上了。JAX的设备放置(Device Placement),就是给深度学习这间“大厨房”设计的一套食材调度智慧——它决定每个数据(张量)放在哪个设备上计算,以及如何让数据在设备之间高效流动。这套能力,让开发者能像掌控厨房节奏的大厨一样,精准调度每一份算力。
设备放置是JAX性能优化的基石。掌握它,你就能在多卡训练、大模型推理等场景中合理分配计算资源,让每一块GPU和TPU都物尽其用。
可视化演示
动态演示JAX设备放置的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
JAX设备放置:设备放置 设备放置概念与默认策略 device_put显式设备放 核心机制可视化 JAX设备放置整体架构与关键组件
设备类型概览
走进这间深度学习“大厨房”,你首先得认识每个工作台的脾气。CPU就像那位什么菜都会做的万能老师傅,单道菜可能不快,但什么活都能接,从切葱剥蒜到算账记账都行。GPU则是一排火力十足的炒锅工位,一次能同时翻炒上千份食材——矩阵乘法、卷积这类“大锅菜”是它的拿手好戏,单个食材处理不算快,但胜在并行批量。TPU则是专门为深度学习定制的流水线工作站,针对大规模矩阵运算做了极致优化,就好比一条只为招牌菜设计的自动化产线,别的活儿它不干,但干起矩阵乘法来效率惊人。
理解设备类型时,还要看清它们的“厨房布局”差异。CPU拥有较大的缓存和主存,就像操作台旁边的大货架,什么食材都能堆;GPU则有高带宽的显存,类似灶台旁的快速取用架,存取速度极快但容量有限;TPU的高带宽内存(HBM)则像流水线旁的专用料仓,只为连续投喂矩阵运算而设计。在JAX中,一切计算都组织成一个计算图,而设备放置决定了这个图上的每个节点(运算)由哪位“厨师”来执行。
认清每个工作台的特长之后,你的调度策略就有了基础。正如大厨不会用流水线工作站去切一根葱,你也应该把大规模矩阵运算交给GPU或TPU,把数据预处理这类杂活留给CPU。设备类型决定了“天花板”,而接下来要学的放置规则,则决定你能否触达这个天花板。
- 设备集合含义JAX中可见的设备是一个有序集合,CPU作为后备选项始终存在。
- 性能倾向GPU和TPU的并行能力远超CPU,但单任务的灵活性较低。
- 内存访问差异各设备拥有独立内存空间,跨设备访问必须经过拷贝。
默认放置规则
当你第一次站在厨房里还没发号施令时,食材默认会放在哪儿?JAX的答案很简单:全部放在CPU这个“万能操作台”上。这就好比新买的食材默认堆在中央台面,因为你还没决定哪道菜用哪个工位。在JAX中,你用jnp.array()创建的数组,默认位于CPU上,jit编译的函数也算在CPU的“辖区”内。
但这里有个非常优雅的设计——计算跟随数据。就像大厨看到食材在哪个台面,就会直接在那个台面开火操作一样,JAX的运算会自动在数据所在的设备上执行。如果你把数组x放在了GPU上,那么x + 1这个计算也会在GPU上完成,无需你额外指定。这种设计让代码的设备感知变得隐式而安全,不会因为跨设备访问而引发错误。
默认规则给入门者提供了“安全模式”:你什么都不用管,代码也能正确运行。但这也意味着你可能把GPU晾在一边——就像大厨只顾着用操作台,炒锅工位全都空着。当你开始追求性能时,就需要使用device_put这样的“主动调度指令”来打破默认规则了。
- 默认放置逻辑JAX数组默认创建在CPU上,所有
jnp.操作都会跟随输入数据的设备位置。 - 数据跟随计算若输入张量在GPU,则运算也在GPU上执行,不需要手动标注设备。
- XLA编译的全局状态JAX底层由XLA编译器管理设备分配,它维护一个全局的“设备分配表”。
device_put机制
现在你决定把一些食材从操作台搬到炒锅工位上去。这道工序在JAX里就叫做设备指定(device placement),最直接的命令是jax.device_put。它就像大厨把一筐食材端起来,明确地说:“这筐去三号炒锅!”——把一个数组显式地放到指定的设备上。
device_put是异步的。这意味着大厨只需要把食材“放上传输带”就可以继续忙别的,不用站在传输带旁边等它抵达。在代码层面,jax.device_put(x, device)会立即返回,数据在后台搬运。但当你需要确保数据真的到位了才能进行下一步时,就需要调用.block_until_ready()——这相当于大厨盯着传输带,直到确认食材真正到达了指定台面才继续。
device_put 给了你精细控制数据位置的能力,但切忌滥用。每做一次跨设备搬运,就像端着一锅热汤从厨房一头跑到另一头——费时费力还容易洒。好的做法是:把数据一次性搬到目标设备,然后在这个设备上完成一系列计算,最后再搬走结果。频繁地在设备之间来回搬运数据,往往是性能不佳的元凶。
device_put 就像把食材放上传输带送往指定工位,异步意味着你不用站在原地等,但 block_until_ready() 会让你等到它确实到达。- 设备目标显式化
device_put将张量移动到指定设备,返回新数组。 - 异步传输行为函数返回时,数据可能尚在传输中,不会阻塞主线程。
- 强制同步等待
block_until_ready()挂起调用者直到操作完成。
jit设备绑定
有了device_put搬运食材,接下来要让整个做菜流程固定在某个工位上进行。jit编译就是给一段计算流程“锁定厨房工位”的机制。当你用@jax.jit装饰一个函数时,JAX会把这个函数的整个计算图交给XLA编译器,编译成针对特定设备的机器码——就像大厨把一道菜的完整工序写下来,然后指派给指定灶台一次性执行。
jit有一个device参数,可以明确指定编译后的代码跑在哪个设备上:
如果不指定,jit会沿用“计算跟随数据”的默认规则,自动选择一个设备。值得注意的是,jit的静态输入(如整型形状参数)会被“烘焙”进编译图,就像做菜时把盛菜用的盘子尺寸也提前定好——盘子变了,就得重新编译一次。这种提前锁定能让编译器做更激进的优化,包括内存复用、算子融合等。
jit 和设备放置形成了完美搭档:device_put 负责“把食材放到正确的位置”,jit 则负责“让加工程序在那个位置高效运转”。就像大厨把切好的食材放到指定灶台旁,然后用一套预先排练好的流程,在最短时间内完成出菜。这套组合拳,是你用好JAX性能的关键。
jit 就像是把菜谱和灶台绑定在一起,每次按照锁定好的计划执行,省去了每次思考“去哪做、怎么做”的开销。- 编译器目标设备
jit的device参数指定编译产物运行的设备,可以是设备索引或jax.Device。 - 静态输入烘焙只有静态输入参与编译图构建,非静态输入以参数形式传入。
- 图优化增益XLA对固定的图做算子融合和内存规划,实现接近手写内核的性能。
设备作用域
控制设备放置还有一种更“批量化”的手段——设备作用域(Device Scope)。想象一下,大厨说:“今天午餐时段,所有凉菜都在一号台准备。”这就是一个“作用域”,在某个上下文范围内,所有新建的食材都默认放到指定台面。在JAX中,jax.device_scope() 提供了这种能力:
作用域可以嵌套。内层作用域会覆盖外层作用域,就像大厨先说了“凉菜去一号台”,然后又补充“但是拍黄瓜去二号台切”。这种嵌套覆盖规则让你可以在一个大的设备策略下,为个别张量设置例外。JAX 在 0.4.26 版本后引入了 jax.default_device 等新API来管理更细粒度的放置,但 device_scope 作为上下文管理器依然是理解设备放置的最佳入口。
设备作用域特别适合“批量搬运”场景。比如你有一段数据预处理代码,里面创建了大量中间张量,你希望它们全部落在GPU上——这时你不需要逐个调用device_put,只需用一个device_scope包裹整段代码即可。这极大简化了代码,也降低了忘记手动搬运的风险,就像大厨一条指令就让整个团队把工作台切换到正确的工位上。
- 上下文管理器
device_scope以with语句使用,作用域内所有新分配的张量都放在目标设备。 - 嵌套优先级内层作用域的放置目标覆盖外层作用域。
- 作用域退出恢复退出
with块后,放置规则恢复为之前的状态。
多设备协调
当你拥有多个GPU或TPU时,厨房就变成了多工位协作模式。大厨面临的新问题是:如何让几个灶台同时高效运转?最简单且最常用的策略是数据并行分片——就像大厨把10锅菜分给3个灶台同时炒,每个灶台炒一部分,最后再汇总。在JAX中,你可以用jax.device_put配合jax.pmap或jax.sharding将数据分片到多个设备。
跨设备通信是协调的关键。想象三个灶台之间传菜需要经过一条走廊——这就是设备间的通信总线(如NVIDIA的NVLink、TPU的ICI互联)。当每个灶台需要其他灶台的中间结果时,就产生了通信开销。JAX的jax.lax.all_gather、jax.lax.psum等操作符在计算图中显式表达这些通信需求。
多设备协调是一门权衡的艺术。分片越多,单个设备的工作量越少,但通信开销越大。就像灶台越多,每个灶台炒得越少,但传菜走廊变得越来越拥堵。当通信时间超过省下的计算时间时,多设备反而变慢。理解这个权衡,就是在理解深度学习框架的扩展性本质。
- 分片放置将数据的第一个维度切分到n个设备上。
- 梯度汇总通信每个设备计算本地梯度后,需要全局求和。
- 负载均衡目标理想情况下各设备计算时间相等。
数据传输优化
厨房效率的最大杀手是什么?不是炒菜慢,而是食材在台面之间来回搬。在JAX中,最影响性能的往往不是计算本身,而是数据在CPU、GPU、TPU之间的传输。JAX社区用一个“不变量”来指导优化:减少设备间数据移动,尤其要减少与CPU(Host)之间的传输。CPU内存和GPU显存之间的PCIe传输,远慢于设备本身的计算速度。
优化的核心策略是通信计算重叠。就像大厨在等汤炖的时候,不会干站着——他会利用这段时间切下一道菜的食材。JAX的jit编译器会自动进行传输与计算的调度重叠,让数据搬运和使用并行进行。此外,jax.block_until_ready()的使用时机也很关键:不要在每次设备间传输后都强制同步等待,而是让计算流水线自然地流起来。
数据传输优化本质上是在“搬得少”和“算得快”之间找平衡。大厨的智慧在于:提前规划好每一份食材在哪里被使用,让它们在第一次需要之前就位,然后用完就不再搬运。JAX渐进式放置(progressive placement)机制会自动将常驻数据留在设备上,你在编码时也要培养这种“数据本地性”思维,让每次device_put都物有所值。
- 传输时间模型传输耗时约等于数据量除以带宽。
- 重叠方式在一批数据传输时发起下一批计算。
- 分片策略影响更大的分片减少通信量,但增加每设备的计算负担。
调试与监控
即使是大厨,也有看不到厨房全局的时候。设备放置的调试,核心是回答三个问题:数据在哪个设备上?设备是否被充分利用?传输是否成为瓶颈?JAX提供的基础API是jax.devices()——它列出当前所有可见设备,就像厨房门口的工位图,标明每个灶台的位置和状态。
更详细的诊断工具包括:x.train_pos(显存使用统计)、jax.profiler(性能剖析)、以及XLA的HLO可视化。它们组合起来,能帮助你把目光聚焦到具体瓶颈上。在JAX中,每个数组都有一个.device()方法,可以直接查询它当前所在的设备——相当于在每份食材上贴了一张“当前位置”的标签。
调试设备放置就像审视厨房动线:你会发现某道菜总是慢,不是炒得慢,而是食材从仓库到灶台的路太长。通过jax.profiler和放置查询,你能准确找到“食材在路上”的时间,然后优化放置策略。这种调试能力,是JAX工程师从“能用”走向“精通”的分水岭。
- 设备可见性查询
jax.devices()返回所有可用设备列表。 - 张量位置查询每个
jax.Array对象可通过.device属性查看所在设备。 - 剖析工具定位profiler 记录每个运算的设备耗时与传输时间。
AI 应用场景
大模型分布式训练中的设备调度
阶段:模型训练与性能优化
解决的问题:训练GPT等大模型时,单张GPU显存不足以容纳模型参数与梯度。传统做法是盲目添加设备,却因数据传输频繁和负载不均导致GPU利用率极低。AI通过device_put将模型分片放置到多张GPU,再用jit(device=...)锁定每片计算的执行设备,实现高效的数据并行。
具体案例:一家AI公司训练百亿参数模型。工程师先通过jax.devices()确认8张A100的可见性,用jax.sharding将模型参数分片到8张GPU,然后用jit编译训练步骤并通过device_scope管理混合精度状态。最终GPU利用率从32%提升到87%,训练速度提升2.8倍。
实时推理服务的延迟优化
阶段:模型部署与推理
解决的问题:在线推荐系统要求毫秒级响应,而模型推理若频繁在CPU和GPU间搬运数据,会产生数百毫秒的PCIe传输延迟。AI利用设备放置策略,让预处理留在CPU、推理常驻GPU,减少不必要的数据复制,并用block_until_ready精确控制同步时机。
具体案例:某推荐系统服务,原实现中每个请求都会产生2-3次CPU↔GPU的传输,P99延迟达180ms。工程师使用device_put将embedding表常驻GPU,将批处理推理函数用jit(device=GPU)锁定,P99延迟降至45ms,吞吐提升4倍。
多租户GPU集群的算力编排
阶段:资源管理与调度
解决的问题:云平台上多个用户共享GPU池,如何动态放置任务以最大化利用率?设备放置的知识在此转变为“算力编排”能力——根据每个任务的GPU需求、数据位置和优先级,决定在哪个物理设备上启动计算。
具体案例:某云平台的AI服务团队使用JAX的device_put和device_scope,为每个新任务动态选择负载最低的GPU,将任务数据通过异步传输预置到该设备,并把核心计算用jit绑定到 해당设备。平台整体GPU利用率从45%提升到76%,任务排队时间减半。
Python 演示
import jaximport jax.numpy as jnpimport numpy as np def inspect_device(name, arr): print(f"{name}: shape={arr.shape}, device={arr.device()}") # 1. 查看当前所有可见设备print("可见设备:", jax.devices()) # 2. 默认放置规则:数组创建在CPU上cpu_arr = jnp.ones((4, 4))inspect_device("CPU默认数组", cpu_arr) # 3. 使用 device_put 显式放置到第一个GPUif jax.devices(backend="gpu"): gpu = jax.devices(backend="gpu")[0] gpu_arr = jax.device_put(cpu_arr, gpu) inspect_device("device_put到GPU", gpu_arr) # 计算跟随数据:加法在GPU上执行 result = gpu_arr + 1 inspect_device("GPU上的加法结果", result) # block_until_ready 强制同步等待 result.block_until_ready() # 4. 使用 jit 并指定设备@jax.jit(device=("gpu" if jax.devices(backend="gpu") else "cpu"))def compute_square(x): return x ** 2 x = jnp.arange(16).reshape(4, 4)y = compute_square(x)inspect_device("jit计算的结果", y) # 5. 使用 device_scope 批量放置with jax.device_scope("gpu"): a = jnp.ones((3, 3)) b = jnp.full((3, 3), 2.0) inspect_device("作用域内的a", a) inspect_device("作用域内的b", b) c = a @ b inspect_device("作用域内的矩阵乘法", c) # 6. 传输计时对比def timed_transfer(): data = np.random.rand(1000, 1000).astype(np.float32) t0 = jax.device_put(data, jax.devices(backend="gpu")[0]) t0.block_until_ready() return t0 if jax.devices(backend="gpu"): gpu_data = timed_transfer() print("数据传输完成,GPU数据:", gpu_data.device())
device_put显式搬运、jit设备绑定和device_scope批量放置。jax.devices()列出所有可见设备,相当于查看厨房里有哪些可用灶台。jax.device_put(cpu_arr, gpu)将数组从CPU显式搬运到GPU,类似把食材端到指定灶台。result.block_until_ready()强制等待异步传输完成,像大厨盯着食材确认到达。@jax.jit(device="gpu")将函数编译并绑定到GPU,类似把固定菜谱锁定到指定灶台。jax.device_scope("gpu")上下文管理器,作用域内创建的数组自动放到GPU。arr.device()查询数组当前在哪个设备上,类似于查看食材的当前位置标签。
常见误区
误区我以为每次调用 `device_put` 都会把数据完整复制一份,所以尽可能少用,导致所有数据都堆在CPU上。
正解device_put 确实是移动或拷贝数据,但JAX的不可变数组语义下,device_put 在需要时会共享底层缓冲(buffer aliasing),而不是盲目复制。更重要的是,该搬运时搬运才是正确的:把数据留在CPU上导致GPU空转,才是更大的浪费。就像厨师把食材放上传输带虽然要花时间,但如果为了省这一步而让整个炒锅工位等菜,那才是真正的低效。
为何常见:初学者往往听说“数据传输很贵”就因噎废食,忽略了设备利用率才是更大头的性能指标。
误区我觉得 `jit(device=...)` 就是把数据放到那个设备上,和 `device_put` 差不多,用一个就行。
正解两者作用层面完全不同。device_put 是数据层面的操作——把张量从A设备搬到B设备;jit 的 device 是计算图编译层面的操作——决定编译产物在哪个设备上执行。打个比方,device_put 是“把食材端到三号灶台”,jit(device=...) 是“把三号灶台的做菜流程固化下来”。没有食材,流程无从执行;没有流程,食材不会自动变成菜。两者配合才有意义。
为何常见:二者都涉及“设备”两字,初学者容易混淆数据移动和计算编译两个不同的抽象层级。
误区我想当然地以为把模型从1张GPU扩展到4张GPU,训练速度就能提升4倍。
正解多设备带来的是通信开销。每个训练步骤中,各设备需要同步梯度(AllReduce),这个过程随着设备数增加而指数级增长通信量。当设备间通信时间超过计算节省的时间时,扩展收益会骤降甚至变负。就像大厨把一锅菜分给10个灶台,但传菜时间和协调成本比炒菜时间还长。正确做法是评估通信计算比,选择合适的并行策略(数据并行、模型并行或混合并行)。
为何常见:直观直觉认为“人多力量大”,忽略了分布式计算中通信开销这一核心瓶颈。
误区我以为只要在 `device_scope` 里创建的数组就一定会被放到指定设备上,无论它之前是否已经有了设备绑定。
正解device_scope 的影响范围限于新建数组。如果你在作用域内对外部传入的、已有设备绑定的张量做运算,结果数组遵循“计算跟随数据”规则——新旧设备不一致时,JAX会按最优方式处理(通常是把结果放在输入所在的设备上)。作用域只决定“没有明确指定设备的新分配”落在哪里。就像大厨说“所有凉菜去一号台准备”,但已经在上菜的菜不会因为你一句话就跑回厨房重做。
为何常见:作用域这个词让人误以为它是绝对的设备覆盖规则,而忽略了它只是“默认值”机制。
误区我担心异步传输不安全,所以在每次设备操作后面都加上 `.block_until_ready()` 确保数据到位。
正解过度使用 block_until_ready 会破坏计算流水线的重叠能力。JAX的异步调度允许传输和计算并行进行——你等一次同步,就等于让后面的所有流水线全部暂停对齐。就像大厨每走两步就停下来同步一下团队进度,整个厨房的节奏反而被打乱了。正确的做法是:只在真正需要同步的边界(比如计时、调试、跨框架交互)使用,中间的计算尽量让它们自然地“流水线化”。
为何常见:初学者对异步机制不放心,试图用同步等待换取安全感,却没意识到这会牺牲最大的性能优势。
面试题
Q1. 在JAX中,使用jnp.array()创建的数组默认放置在哪类设备上?
- A.第一个GPU
- B.CPU← 正确答案
- C.TPU
- D.随机指定
答案:B。JAX的默认规则是所有新创建的数组放在CPU上,就像新买的食材默认堆在中央操作台。只有当你显式调用device_put或使用device_scope时,才会被放到GPU或TPU上。
Q2. 关于JAX的“计算跟随数据”规则,下列说法正确的是?
- A.计算操作发生在CPU上,数据在GPU上也没关系
- B.计算操作自动在输入数据所在的设备上执行← 正确答案
- C.计算操作总在TPU上执行
- D.计算操作需要手动指定设备才能执行
答案:B。JAX的核心设计是计算自动跟随输入数据所在的设备执行。如果数组x在GPU上,x + 1就在GPU上计算。这就像食材在哪个台面,就直接在哪个台面下锅,无需额外指定。
Q3. jax.device_put(x, device)函数的主要作用是什么?
- A.将函数编译到指定设备
- B.将数组x显式移动到指定设备← 正确答案
- C.统计数据在设备上的时长
- D.创建新的多维度数组
答案:B。device_put是数据层面的搬运操作,将张量从当前设备移动到目标设备。它可以异步进行,使用block_until_ready()等待完成。它用于数据放置,不涉及编译。
Q4. 在JAX中,关于jax.jit(device=...)参数的描述,哪些是正确的?
- A.它指定编译后的计算在哪个设备上执行← 正确答案
- B.
device_put可以被它替代 - C.它与
device_put作用层面相同 - D.它决定了计算图的编译目标设备← 正确答案
答案:AD。jit(device=...)指定编译后代码的执行设备和编译目标,这是计算图层面的设置(选项A和D正确)。它与device_put不同——后者是数据层面的移动操作,二者互补但不可互相替代(B、C错误)。
Q5. 在device_scope嵌套使用中,哪个作用域的放置规则优先级最高?
- A.最外层作用域
- B.最内层作用域← 正确答案
- C.所有作用域同等优先级
- D.全局默认值
答案:B。device_scope的嵌套规则是内层覆盖外层。就像大厨先说了“凉菜去一号台”,然后又补充“但拍黄瓜去二号台”,更具体的指令胜出。内层作用域优先级最高,退出后恢复外层或全局默认规则。
Q6. 下列哪项是JAX多设备数据处理的主要挑战?
- A.无法在多个设备上运行计算
- B.设备间通信开销可能超过计算收益← 正确答案
- C.GPU无法进行张量运算
- D.内存容量无限大
答案:B。多设备计算的核心挑战是通信开销。每个训练步骤需要跨设备同步梯度,设备越多通信越密集。当通信时间超过省下计算时间时,扩展收益变负。就像灶台太多时传菜走廊变成了瓶颈。
Q7. 以下哪些方式可以帮助减少JAX中设备间数据传输的性能损耗?
- A.使用
block_until_ready()在每个传输后立即同步 - B.让传输与计算重叠,利用异步调度← 正确答案
- C.减少Host(CPU)与设备之间的往返传输← 正确答案
- D.在每次操作后都调用
device_put将数据搬回CPU
答案:BC。减少传输损耗的核心策略是减少传输次数和让传输与计算重叠(B、C正确)。在每个传输都同步等待(A)和频繁搬运回CPU(D)反而会放大性能问题,破坏流水线。
Q8. 如何查看一个JAX数组当前位于哪个设备上?
- A.
arr.shape - B.
arr.device()← 正确答案 - C.
arr.dtype - D.
jax.devices()
答案:B。arr.device() 返回该数组所在的设备信息。jax.devices() 是查看所有可用设备,arr.shape和arr.dtype则分别查看形状和数据类型,均与位置无关。
Q9. 关于数据传输时延模型,以下哪些说法正确?
- A.传输时间 = 数据量 / 带宽← 正确答案
- B.减少传输次数比减少单次数据量更有效← 正确答案
- C.CPU与GPU之间的传输比GPU显存内访问更快
- D.通信计算重叠可以让总时间接近max(计算,传输)← 正确答案
答案:ABD。传输时间由数据量和带宽决定(A正确),每次传输还有固定开销,所以减少次数收益更明显(B正确)。CPU-GPU之间的PCIe传输远慢于显存访问(C错误),通信计算重叠让总时间近似max而非叠加(D正确)。
Q10. 在JAX中,关于XLA编译器与设备放置的关系,以下哪项描述最准确?
- A.XLA不关心设备,只优化计算图
- B.XLA根据数据放置将计算图分配到对应设备并做优化← 正确答案
- C.device_put后需要手动调用XLA编译器
- D.XLA仅适用于CPU设备
答案:B。XLA编译器集成了设备分配逻辑,它会根据输入的设备位置决定每个算子映射到哪个设备硬件。jit固定计算图后,XLA负责在目标设备上生成优化代码。device_put与XLA是两个层次的概念,C错误。XLA支持CPU、GPU和TPU,D错误。