Spot 资源实现深度解析:从虚拟设备到偷卡超卖的完整链路

1. 什么是 Spot 资源

Spot 资源(在 GangJob 中对应 spot-queue 队列)是一种服务质量不被保证的低优先级 GPU 资源

  • 随时可以被高优先级的 reserved 任务驱逐抢占
  • 计费便宜
  • 适用于短时调试任务、可被打断保留进度的任务

传统 spot 的调度范围是集群中所有空闲未分配的资源(未售出的 + 已售出但暂未分配的)。但本文要解析的是更深层的 spot 实现——偷卡超卖:即使资源已被 reserved 任务分配(如用户开发机占着 8 卡),只要用户"离开"(GPU 利用率为 0),就能把这块卡的算力偷给 spot 任务,用户"回归"时再驱逐 spot、还回资源。


2. 三层架构总览

Spot 资源的实现横跨三个组件,各层职责清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────┐
│ gang-scheduler │
│ (调度器 spot 插件) │
│ - spot-gpu 虚拟资源调度 │
│ - 优先级排序 (spothigh/spotmid/spotlow) │
│ - 抢占驱逐决策 │
└──────────────────────────┬──────────────────────────────┘
│ 调度 spot 任务到 platform/spot-gpu

┌─────────────────────────────────────────────────────────┐
│ spot-manager │
│ (节点设备管理组件) │
│ - spot-gpu 虚拟设备 Device Plugin │
│ - 低功耗检测 (lowpower detector) │
│ - 设备状态管理 (Healthy/Unhealthy) │
│ - 偷卡: 检测用户离开/回归 │
│ - 驱逐: Kill spot 容器进程 + 删 Pod │
│ - khook: 内核态 ioctl 劫持 │
└──────────────────────────┬──────────────────────────────┘
│ Annotation 指示设备同步

┌─────────────────────────────────────────────────────────┐
│ node-runtime │
│ (自定义 Container Runtime) │
│ - 劫持容器创建流程 (containerd spec 修改) │
│ - 将 ShadowPod/reserved Pod 的 GPU 设备同步给 Spot Pod │
│ - prestart hook: 容器启动前注入设备 │
└─────────────────────────────────────────────────────────┘

3. 底层基础:自定义 Container Runtime(node-runtime)

3.1 问题:K8s 不允许同一 GPU 分配给两个 Pod

K8s 以容器为单位调度资源,一个 GPU 设备要么分配给 Pod A,要么分配给 Pod B。但在容器层面,同一块 GPU 可以被多个容器挂载——这只是设备文件(/dev/nvidia0)的 bind mount。

3.2 解决:自定义 Runtime 拦截容器创建

K8s 容器创建流程:kubelet → containerd → spec 文件(OCI) → runtime → 容器

containerd 生成符合 OCI 标准的 spec 文件,runc 根据 spec 创建容器。spec 文件里定义了容器的设备挂载(devices)、环境变量(env)、挂载点(mounts)等。

关键发现:只要将容器 A 的 spec 中的设备挂载信息拷贝到容器 B 的 spec,就能让两个容器共享同一块 GPU。

node-runtime 仿照 nvidia-container-runtime 的"包一层"方式,在 containerd → runc 之间插入一层自定义 runtime。调用链变为:

1
containerd → node-runtime(拦截 spec)→ nvidia-container-runtime → runc → 容器

3.3 Spec 复制的具体流程

不是字节级拷贝来源容器的 spec——而是通过 annotation + kubelet + Device Plugin 的结构化流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
                    annotation 告诉 runtime"从哪同步"


┌─────────────────────────────────────────────────────────┐
│ ① 解析 annotation │
│ platform.k8s.io/container-devices-from.main = │
│ {"_/shadowPod-xxx/main": {"nvidia.com/gpu": ["GPU-..."]}}│
│ → 目标容器: main │
│ → 来源容器: shadowPod-xxx/main │
│ → 资源: nvidia.com/gpu │
└──────────────────────┬────────────────────────────────────┘
│ 拿到来源 Pod/容器名

┌─────────────────────────────────────────────────────────┐
│ ② 向 kubelet 查来源容器的已分配设备 │
│ kubeletClient.RequestContainer(ns, podName, container) │
│ → kubelet 返回来源容器分配了哪些设备 ID │
│ → GetDeviceMap() 提取: {"nvidia.com/gpu": ["GPU-111"]} │
└──────────────────────┬────────────────────────────────────┘
│ 拿到设备 ID

┌─────────────────────────────────────────────────────────┐
│ ③ 调 Device Plugin Allocate(gRPC) │
│ deviceInterpreter.Request(deviceIDs) │
│ → Device Plugin 返回 ContainerAllocateResponse: │
│ - Envs: NVIDIA_VISIBLE_DEVICES=GPU-xxx │
│ - Mounts: /dev/nvidia0, driver libs... │
│ - Devices: /dev/nvidia0 (c 195:0) │
│ - Annotations: container-devices-from... │
└──────────────────────┬────────────────────────────────────┘
│ Allocate 响应

┌─────────────────────────────────────────────────────────┐
│ ④ 合并到目标容器的 spec │
│ - mergeEnv(): 把 env 追加到 spec.Process.Env │
│ - spec.Linux.Devices += 响应里的设备 │
│ - spec.Mounts += 响应里的挂载(驱动库等) │
│ - MergeDevices(): 设备 ID 列表写入 annotation │
└──────────────────────┬────────────────────────────────────┘
│ spec 修改完成

nvidia-container-runtime → runc → 容器创建
(容器里有了来源容器的 GPU 设备)

关键代码路径

1
2
3
4
5
6
7
8
9
10
11
12
13
// node-runtime/pkg/runtime/runtime_factory.go
func newContainerRuntime(logger, cfg, argv, options) (Runtime, error) {
// 1. 加载 OCI spec
ociSpec, err := oci.NewSpec(logger, argv)
specs, err := ociSpec.Load()

// 2. 构建 spec modifier(多个 modifier 链式调用)
specModifier, err := options.NewSpecModifier(logger, cfg)
// modifier 链: DevicesModifier → Repeat → ReplaceMounts → ContainerService → Generic → CPUSet

// 3. 返回包装了 modifier 的 runtime
return NewModifyingRuntimeWrapper(lowLevelRuntime, ociSpec, specModifier)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// node-runtime/pkg/runtime/modifier/devices.go
func (m *DevicesModifier) Modify(spec *specs.Spec) error {
// ① provider(spec) → 解析 annotation → 查 kubelet → 得到设备 ID map
dm, err := m.provider(spec)

// ② MergeDevices(spec, dm) → 设备 ID 写入 spec annotation
MergeDevices(spec, dm)

// ③ 遍历每个资源的设备 ID → 调 Device Plugin Allocate
for resName, deviceids := range dm {
containerInfo, err := m.InterpretDevice(resName, deviceids)
// containerInfo = Device Plugin Allocate 响应
// 包含 Envs, Mounts, Devices, Annotations

// ④ 合并到 spec
mergeEnv(containerInfo.Envs, &spec.Process.Env)
// spec.Linux.Devices += containerInfo.Devices
// spec.Mounts += containerInfo.Mounts
}
}

provider 有三种来源providers/merge.go Merge 合并):

Provider 来源 适用场景
ContainerSync 从 annotation 解析来源容器 → 查 kubelet 获取该容器分配的设备 容器间共享(Dind 场景、spot 偷卡)
PodSync 从 annotation 解析来源 Pod → 查 kubelet 获取该 Pod 所有容器分配的设备 Pod 间共享(推理服务复用)
Direct 直接从 annotation 中的设备 ID 分配 直接指定设备

ContainerSync 解析 annotation 的格式

1
2
3
4
5
6
7
8
9
10
// annotation key: platform.k8s.io/container-devices-from.main
// annotation value:
{
"_/shadowPod-xxx/main": {
"nvidia.com/gpu": ["GPU-111", "GPU-222"]
}
}
// _/shadowPod-xxx/main → 同 namespace 下的 shadowPod-xxx Pod 的 main 容器
// nvidia.com/gpu → 资源类型
// ["GPU-111", "GPU-222"] → 要同步的设备 ID

为什么不是直接拷贝来源容器的 spec

因为设备挂载到容器需要的不只是 /dev/nvidia0 这一个文件,还有:

  • 驱动库(/usr/lib/x86_64-linux-gnu/libnvidia-xxx.so
  • 环境变量(NVIDIA_VISIBLE_DEVICES
  • 可能的 RDMA 设备(/dev/infiniband/uverbs0

这些信息由 Device Plugin 的 Allocate 响应提供——不同设备类型(NVIDIA/Metax/AMD/RDMA)的 Device Plugin 返回不同的挂载信息。通过 Device Plugin 接口,runtime 实现了设备类型无关的设备同步。

4. 中间层:节点设备管理(spot-manager)

spot-manager 是运行在每个节点上的守护进程,负责 spot-gpu 虚拟设备的管理、偷卡检测、驱逐。

4.1 spot-gpu 虚拟设备

为 spot 任务引入虚拟资源类型 platform/spot-gpu

1
2
真实 GPU: nvidia.com/gpu     ← reserved 任务申请的资源
虚拟 GPU: platform/spot-gpu ← spot 任务申请的资源

一一对应关系:节点上每块真实 GPU 对应一个 platform/spot-gpu 虚拟设备。

状态流转

状态 含义 spot 能否调度
Healthy 真实 GPU 空闲 / reserved 用户已离开 ✅ 可以调度 spot
Unhealthy 真实 GPU 被 reserved 任务分配且用户在使用 ❌ 不可调度,已有 spot 被驱逐
1
2
3
4
5
6
7
// spot-manager/pkg/solvers/spotgpu/spotgpu.go
// 初始化时,所有 spot-gpu 设备放入 IdleSpace
idleDevices := []string{}
for _, d := range dps[0].GetDevice() {
idleDevices = append(idleDevices, d.ID)
}
cache.Create(consts.IdleSpace, idleDevices)

4.2 低功耗检测(Lowpower Detector)

spot-manager 的 lowpowerDetector 负责检测 reserved 用户是否"离开":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// spot-manager/detectors/lowpower/utilization.go
func (d *lowpowerDetector) detect(ctx context.Context) (*detectors.Event, error) {
utilActive, err := d.isUtilActive() // GPU 利用率 > 0?
processActive, err := d.isProcessActive(ctx) // 有 GPU 进程在跑?

if utilActive || processActive {
// 用户在使用 → 更新 lastActiveTime
d.idle = false
d.lastActiveTime = time.Now()
return nil, nil
}

// 超过 maxInactiveTime 没活动 → 判定用户"离开"
if time.Since(d.lastActiveTime) > d.maxInactiveTime && !d.idle {
d.idle = true
// 产生 LowPower 事件 → 激活 spot-gpu 为 Healthy
return &detectors.Event{
Type: consts.EventTypeLowPower,
Target: d.target,
Devices: []detectors.DeviceStatus{{Using: false}},
}, nil
}
return nil, nil
}

两个检测维度

  1. 利用率检测isUtilActive):从 DCGM(NVIDIA GPU 监控)或各厂商 exporter 拉 GPU 利用率,> 0 则 active。

  2. 进程检测isProcessActive):用 NVML 检查 GPU 上是否有 compute 进程在跑:

1
2
3
4
5
6
7
8
9
10
11
12
// spot-manager/detectors/lowpower/process.go
func (d *lowpowerDetector) isProcessActive(ctx context.Context) (bool, error) {
// 获取目标容器的进程列表
processes, err := d.getContainerProcesses(ctx)
// 遍历 GPU,检查每个 GPU 上是否有目标容器的进程
for i := 0; i < gpuNum; i++ {
device, _ := nvml.DeviceGetHandleByIndex(i)
dProcessList, _ := device.GetComputeRunningProcesses()
// 如果 GPU 上有属于目标容器的进程 → active
}
return false, nil // 没找到 → inactive
}

4.3 偷卡机制(Steal GPU)

当 lowpower detector 判定用户"离开"时:

1
2
3
4
5
6
// spot-manager/pkg/solvers/spotgpu/lowpower.go
func (s *spotGPUSolver) solveIdle(e detectors.Event) error {
// 用户离开了 → 激活 spot-gpu 设备为 Healthy
// → 节点 annotation 更新为可调度 spot
// → 调度器看到 Healthy 的 platform/spot-gpu → 可以调度 spot 任务
}

当 lowpower detector 检测到用户"回归"(GPU 利用率从 0 变 > 0,或有新进程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// spot-manager/pkg/solvers/spotgpu/lowpower.go
func (s *spotGPUSolver) solveBusy(e detectors.Event) error {
// 用户回归了 → 立刻驱逐正在偷用 GPU 的 spot 任务

// 1. 更新驱逐 metrics
evictedUsers := s.cache.GetOwnerUsers(e.Target)
s.updateEvictCounter(evictedUsers, consts.EvictReasonOwnerBack)

// 2. Kill 容器进程(通过 containerd task client)
s.evictProcessFor(e)

// 3. 删除 Pod
s.evictFor(e.Target)

// 4. 更新缓存
s.cache.Delete(e.Target)

// 5. spot-gpu 设备变回 Unhealthy
}

驱逐流程evict.go):

1
2
3
4
5
6
7
8
9
10
11
12
13
func (s *spotGPUSolver) evictProcessFor(e detectors.Event) error {
users := s.cache.GetOwnerUsers(e.Target) // 获取所有偷用该 GPU 的 spot Pod
for _, user := range users {
pod, _ := s.kubeClient.CoreV1().Pods(ns).Get(...)
for _, container := range pod.Status.ContainerStatuses {
id := strings.TrimPrefix(container.ContainerID, "containerd://")
// 先 Kill 容器进程(不等 Pod 删除,直接杀进程更快)
s.taskClient.Kill(context.Background(), id)
}
}
// 再删 Pod
s.evictFor(e.Target)
}

关键:先 Kill 进程再删 Pod——Pod 删除需要走 K8s graceful deletion(默认 30s grace period),太慢;直接 Kill containerd 进程能立即释放 GPU。

4.4 khook:内核态 ioctl 劫持

eBPF 无法阻塞系统调用,因此用内核模块实现:

  • 劫持对 GPU 设备的 ioctl 系统调用
  • 解析参数中的设备参数判断是否是 GPU 设备
  • 通过 pid 和 pidnsid 判断容器身份
  • 用户"回归"时:阻塞其 ioctl 系统调用,直到 spot 任务被驱逐、GPU 资源彻底释放
1
2
3
// spot-manager/pkg/kspot/
// khookUser.Undetect(pidNsIds) → 通知内核模块停止监控
// khookUser.Detect(pidNsIds) → 重新开始监控

这是偷卡的核心安全网:用户回归后,内核模块阻塞其对 GPU 的访问,等 spot 驱逐完成后才放行——用户感觉不到中间过程。


4.5 检测机制:离开用轮询,回归用事件触发

device-manager 的检测是非对称设计——"离开"轮询、"回归"事件触发,原因如下:

方向 机制 为什么
离开 轮询(detectInterval 周期查利用率 + 进程) 用户不操作 GPU 就没有事件可触发,只能主动查;晚几分钟发现无所谓
回归 事件触发(内核模块 khook 劫持 ioctl) 必须秒级响应——轮询周期内用户会看到别人的进程/显存被占;khook 检测到 ioctl 后立即阻塞,驱逐完成后才放行

离开检测流程(轮询)

1
2
3
4
5
6
7
每 detectInterval 周期:
① isUtilActive() → 拉 DCGM/exporter 指标查 GPU 利用率
② isProcessActive() → NVML 查 GPU 上有没有 compute 进程
③ 如果 active → 刷新 lastActiveTime,继续轮询
④ 如果 inactive 且超过 maxInactiveTime → 判定"离开"
→ 发 Event{Using: false} → solveIdle → 激活 spot-gpu
→ 通知 khook: Detect(pidNsIds) → 开始监控回归

回归检测流程(事件触发)

1
2
3
4
5
6
7
8
用户操作 GPU → ioctl 系统调用
→ khook 内核模块立即捕获(不等轮询周期)
→ 阻塞该 ioctl(用户感知不到延迟)
→ device-manager 收到"回归"信号
→ solveBusy → 驱逐 spot(Kill 进程 + 删 Pod)
→ spot-gpu 变回 Unhealthy
→ khook: Undetect → 放行用户 ioctl
→ 用户无感

关键:离开后才会启动 khook 监控(Detect),回归驱逐完后停止监控(Undetect)。khook 不是常驻监控,而是按需启停——只在 spot 偷卡期间监控用户回归。


5. 调度层:gang-scheduler 的 spot 插件

5.1 spot-gpu 虚拟资源调度

调度器的 spot 插件负责 spot 任务的调度和抢占决策:

1
2
3
4
5
6
7
8
9
10
11
12
// gang-scheduler/pkg/scheduler/plugins/spot/api.go
const SpotGPUResourceName = "platform/spot-gpu"

const (
ExclusiveSpot SpotGPUType = 1 // 独占 spot(偷卡场景)
SharedSpot SpotGPUType = 2 // 共享 spot
)

const (
SpotGPUStrategyExclusiveFirst SpotGPUStrategy = "idle" // 优先空闲独占
SpotGPUStrategSharedFirst SpotGPUStrategy = "lowpower" // 优先偷卡共享
)

5.2 优先级体系

spot 任务有三个优先级级别,对应不同权重:

1
2
3
4
// gang-scheduler/pkg/scheduler/plugins/spot/plugin.go
SpotHighPriorityWeight = 3000 // spotheigh
SpotMidPriorityWeight = 2000 // spotmid
SpotLowPriorityWeight = 1000 // spotlow

驱逐决策时按权重排序,低优先级先被驱逐:

1
2
3
4
5
6
7
type pluginParams struct {
PriorityClassPolicyWeight int // 100
TaskNumberPolicyWeight int // 10
TaskResourcePolicyWeight int // 10
RunningDurationPolicyWeight int // 10
RunningDurationEvictionPolicy string // "shortest" (默认驱逐运行时间最短的)
}

5.3 节点状态与调度策略

节点 annotation 控制 spot 调度策略:

1
2
3
4
// gang-scheduler/pkg/scheduler/plugins/spot/api.go
NodeAnnoShareGPU = spotpis.AnnoNodeLowPowerSpotKey // 节点是否支持偷卡
NodeAnnoGPUStrategy = spotpis.AnnoSpotPreferKey // spot 策略: idle/lowpower
PodAnnoSpotGPUOwner = spotpis.AnnoDeviceOwnerKey // spot Pod 使用的来源设备

5.4 驱逐决策(podgroups.go)

当 reserved 任务需要资源时,spot 插件计算驱逐哪些 spot 任务:

1
2
3
4
5
6
7
8
9
// gang-scheduler/pkg/scheduler/plugins/spot/podgroups.go
func newEvictPodGroupInfos(evictSpotTasks []*spotTaskInfo) evictPodGroupInfos {
// 按优先级权重排序 spot 任务
// 计算驱逐多少 spot 任务能满足 reserved 需求
// 生成驱逐 PodGroup 信息
for _, t := range evictSpotTasks {
evictPgs[t.PodGroup.Name].evictResource += t.RequestGPUMilli[ExclusiveSpot]
}
}

6. 完整工作流:从用户离开到偷卡到回归驱逐

6.1 偷卡启动(用户离开)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用户开发机(reserved, 8卡)→ 白天不用了 → GPU 利用率 = 0 持续 > maxInactiveTime

spot-manager lowpower detector:
① isUtilActive() → false(DCGM 显示 GPU 利用率 0)
② isProcessActive() → false(NVML 无 compute 进程)
③ 判定 idle → 发 Event{Type: LowPower, Using: false}

spotGPUSolver.solveIdle():
④ 激活 spot-gpu 虚拟设备 → Healthy
⑤ 更新节点 annotation: 可调度 spot
⑥ khookUser.Detect(pidNsIds) → 内核模块开始监控用户回归

gang-scheduler:
⑦ 看到 Healthy 的 platform/spot-gpu → 调度 spot GangJob 到该节点
⑧ spot Pod 申请 platform/spot-gpu → Device Plugin Allocate

node-runtime:
⑨ runtime 检查 annotation → 将 reserved Pod 的 GPU 设备同步到 spot Pod
⑩ spot Pod 容器启动 → 用户开发机的 GPU 被偷用

6.2 偷卡驱逐(用户回归)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
用户回到开发机 → 执行 nvidia-smi / torch 脚本 → 对 GPU 的 ioctl 系统调用

内核模块 (khook):
① 检测到目标 pid namespace 对 GPU 设备的 ioctl → 用户"回归"
② 阻塞该 ioctl 系统调用 → 用户感知不到延迟

spot-manager lowpower detector:
③ isUtilActive() → true 或 isProcessActive() → true
④ 判定 busy → 发 Event{Type: LowPower, Using: true}

spotGPUSolver.solveBusy():
⑤ 更新驱逐 metrics (evictReason: OwnerBack)
⑥ evictProcessFor():
→ containerd taskClient.Kill(spot 容器 ID) → 立即杀进程
⑦ evictFor():
→ kubeClient.CoreV1().Pods().Delete(spot Pod) → 删 Pod
⑧ cache.Delete(target) → 清除设备映射
⑨ spot-gpu 设备变回 Unhealthy
⑩ khookUser.Undetect(pidNsIds) → 内核模块放行用户 ioctl

用户:
⑪ ioctl 系统调用返回 → GPU 上没有其他人的进程 → 感觉什么都没发生

7. 数据结构

7.1 MappingCache(设备映射缓存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// spot-manager/pkg/apis/mapping_cache.go
type MappingCache struct {
cache map[string]*PodStatus // owner pod key => PodStatus
sync.Mutex
}

type PodStatus map[string]*DeviceStatus // device id => DeviceStatus

type DeviceStatus struct {
ID string // 设备 ID
SharingPod string // 当前使用该设备的 spot Pod
}

// 核心方法
func (mc *MappingCache) Create(owner string, devices []string) // 创建 owner 与设备的映射
func (mc *MappingCache) Use(owner, user, deviceID string) // 标记设备被 spot Pod 使用
func (mc *MappingCache) UnUse(owner, user, deviceID string) error // 解除设备使用关系
func (mc *MappingCache) GetOwnerUsers(owner string) []string // 获取 owner 的所有 spot 使用者
func (mc *MappingCache) GetDeviceOwner(deviceID string) string // 获取设备的 owner

7.2 设备 ID 转换

真实 GPU ID 和 spot-gpu 虚拟设备 ID 的转换:

1
2
3
4
5
// 真实 GPU: "GPU-xxx"
// spot-gpu: "spot-GPU-xxx"
func OriginalID_To_SpotID(id string) string {
return "spot-" + id
}

7.3 Pod 类型

1
2
3
4
5
6
// spot-manager/pkg/apis/pod_device_type.go
const (
PodDeviceShadow PodDeviceType = "shadow" // ShadowPod(占位 Pod,持有真实 GPU)
PodDeviceShare PodDeviceType = "share" // SharePod(推理服务共享)
// spot Pod 通过 platform/spot-gpu 调度,不是 shadow/share
)

8. 与 GangJob spot-queue 队列的关系

在 alcor-chaos 的 GangJob case 中,我们使用 --queue spot-queue 创建 GangJob:

1
chaos inject workload gangjob --name dev-23 ... --queue spot-queue

spot-queue 是一个 QueueType=spot 的队列:

1
2
3
// gang-scheduler/pkg/scheduler/cache/cache_base.go
case string(v1alpha1.QueueTypeSpot):
taskType = api.SpotTask

这意味着:

  1. GangJob 提交到 spot-queue 队列 → 调度器识别为 SpotTask
  2. 调度器按 spot 优先级和 platform/spot-gpu 可用性调度。
  3. GangJob 的 Pod 申请 platform/spot-gpu(而非 nvidia.com/gpu)。
  4. node-runtime runtime 将来源容器(ShadowPod 或 reserved 开发机)的 GPU 同步给 spot Pod。
  5. spot GangJob 可以被 reserved 任务驱逐(资源不够时)。

这就是为什么我们造的 GangJob case 用 spot-queue 队列:它绕开了 tenant 100000 的 quota 队列(po-dcsldksmdrryl5wv),走 spot 通道调度到 a9 的 GPU 上,即使 a9 的 GPU 已被分配。


9. 多 GPU 厂商适配

spot-manager 的 GPU Collector 适配器接口是设备无关的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// spot-manager/pkg/metrics/collector/gpu.go
type GpuAdapter interface {
ConvertLabels(*model.Metric, *GpuMetric) bool
Convert(context.Context, PlainMetricModel) (ConvertedGpuMetricModel, error)
}

var adapters = map[string]GpuAdapter{
ADAPTER_NVIDIA: &NvidiaAdapter{},
ADAPTER_METAX: &MetaxAdapter{},
ADAPTER_HUAWEI: &HuaweiAdapter{},
ADAPTER_ENFLAME: &EnflameAdapter{},
ADAPTER_KUNLUNXIN: &KunlunxinAdapter{},
ADAPTER_HYGON: &HygonAdapter{},
// ... 11 种芯片
}

底层的 runtime 设备同步机制不依赖任何厂商的特定 API——只是把 spec 里的设备文件挂载信息拷贝过去,任何 /dev/xxx 类型的设备都能同步。


10. 总结

组件 职责 核心机制
node-runtime 自定义 Container Runtime 劫持 containerd spec 文件,通过 annotation 同步 GPU 设备到多个容器
spot-manager 节点设备管理 spot-gpu 虚拟设备 Device Plugin + lowpower 检测 + 偷卡/驱逐 + khook 内核模块
gang-scheduler 调度器 spot 插件 spot-gpu 调度 + 优先级排序 + 抢占驱逐决策

核心技术链路

1
2
3
4
5
6
7
8
9
10
11
12
13
reserved 用户离开
→ spot-manager lowpower detector(GPU 利用率 0 + 无进程)
→ spot-gpu 设备 Healthy
→ gang-scheduler 调度 spot 任务
→ node-runtime runtime 同步 GPU 设备到 spot Pod
→ spot 任务偷用 GPU

reserved 用户回归
→ khook 内核模块阻塞 ioctl
→ spot-manager 驱逐 spot(Kill 进程 + 删 Pod)
→ spot-gpu 设备 Unhealthy
→ 内核模块放行用户
→ 用户无感

三个关键设计

  1. 虚拟设备platform/spot-gpu):与真实 GPU 一一对应,状态 Healthy/Unhealthy 控制可调度性,不侵入 K8s 原生 GPU 分配逻辑。
  2. 内核态劫持(khook):通过内核模块劫持 GPU 的 ioctl 系统调用,精确感知用户"回归"并阻塞直到驱逐完成——这是偷卡安全性的关键。
  3. 自定义 Runtime(node-runtime):在容器创建层面同步 GPU 设备,设备类型无关,适配所有 GPU 厂商。