从“有卡”到“跑顺”:上海开源大赛 DaoCloud 赛题背后的智算云工程题

2026-09-21 24 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

上海开源大赛公布了 DaoCloud 赛题,选做相关题目还可获得额外现金激励。比激励本身更值得关注的是题目指向的问题:当异构集群从千卡扩展到万卡,智算云的主要矛盾已经从“有没有算力”转向“算力能否稳定、高效地转化为训练与推理吞吐”。

GPU 到位并不等于任务跑得顺。训练作业可能长时间等待配额,推理服务会被 KV Cache 命中率和首 Token 时延拖住,几十乃至上百 GB 的模型权重还可能在训练节点、推理节点与终端之间反复搬运。参赛方案如果只展示一个能运行的 Demo,很难触及问题核心;更有价值的方向,是把调度、缓存、模型分发和可观测性连成一条可测量的工程链路。

瓶颈已经从设备数量转向数据与任务流动

智算集群中的“利用率低”通常不是单一组件造成的。GPU 空闲可能意味着没有任务,也可能意味着任务正在等待数据、权重、网络或其他 GPU。

可以把问题拆成四条路径:

路径 常见现象 建议观察的指标
训练调度 作业排队、资源碎片、部分卡空闲 排队时间 P95、Gang 调度成功率、GPU 利用率
在线推理 首 Token 慢、吞吐抖动 TTFT P50/P95、输出 Token/s、请求失败率
KV Cache 重复前缀仍被重新计算 Cache 命中率、复用 Token 数、缓存淘汰次数
模型分发 Pod 启动慢、网络反复传输权重 模型加载时间、跨节点流量、缓存命中后的启动时间

这里最容易踩的坑,是把 GPU 利用率当成唯一目标。利用率升高不一定代表业务体验改善:激进批处理可能提高吞吐,却让交互请求的首 Token 时延变差;缓存保留过多可以提高命中率,也可能挤占显存并触发 OOM。

因此,方案需要明确优化目标,例如:

在请求失败率不超过基线的前提下:
1. 将 TTFT P95 降低 20%;
2. 将重复前缀请求的计算量降低 30%;
3. 将模型副本启动时间降低 40%;
4. 不增加单节点峰值显存占用。

这些数字只是制定实验目标的示例,不代表赛事官方指标。实际提交应以赛题说明和评测规则为准。

先建立基线,再讨论缓存和调度优化

推理优化经常从“感觉更快了”开始,却因为缺少基线而无法复现。一个更稳妥的做法,是固定模型、输入、并发和硬件,分别测量首次请求与重复前缀请求的 TTFT。

下面的脚本只依赖 Python 标准库,可测试兼容 OpenAI Chat Completions 协议的流式推理服务。它会发送多轮具有相同长前缀的请求,并记录首个有效内容到达时间和总耗时。

--endpoint--model 和可选的 --token 改成自己的服务参数即可运行:

#!/usr/bin/env python3
import argparse
import json
import statistics
import time
import urllib.request


def run_once(endpoint, model, token, prompt):
    body = json.dumps({
        'model': model,
        'stream': True,
        'temperature': 0,
        'messages': [
            {'role': 'user', 'content': prompt}
        ]
    }).encode('utf-8')

    headers = {'Content-Type': 'application/json'}
    if token:
        headers['Authorization'] = f'Bearer {token}'

    request = urllib.request.Request(
        endpoint.rstrip('/') + '/v1/chat/completions',
        data=body,
        headers=headers,
        method='POST'
    )

    started = time.perf_counter()
    first_content_at = None
    output = []

    with urllib.request.urlopen(request, timeout=300) as response:
        for raw_line in response:
            line = raw_line.decode('utf-8').strip()
            if not line.startswith('data:'):
                continue

            data = line[5:].strip()
            if data == '[DONE]':
                break

            event = json.loads(data)
            choices = event.get('choices', [])
            if not choices:
                continue

            content = choices[0].get('delta', {}).get('content')
            if content:
                if first_content_at is None:
                    first_content_at = time.perf_counter()
                output.append(content)

    finished = time.perf_counter()
    return {
        'ttft_ms': round((first_content_at - started) * 1000, 2)
        if first_content_at else None,
        'total_ms': round((finished - started) * 1000, 2),
        'output_chars': len(''.join(output))
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--endpoint', required=True)
    parser.add_argument('--model', required=True)
    parser.add_argument('--token', default='')
    parser.add_argument('--rounds', type=int, default=5)
    args = parser.parse_args()

    fixed_prefix = (
        '你是集群故障分析助手。以下规则在每次请求中保持不变:'
        '优先检查资源、调度、网络、存储和缓存,并给出验证命令。'
    ) * 200
    prompt = fixed_prefix + '\n问题:为什么推理服务的首 Token 时延突然升高?'

    results = []
    for index in range(args.rounds):
        result = run_once(
            args.endpoint, args.model, args.token, prompt
        )
        result['round'] = index + 1
        results.append(result)
        print(json.dumps(result, ensure_ascii=False))

    valid_ttft = [r['ttft_ms'] for r in results if r['ttft_ms'] is not None]
    if valid_ttft:
        print(json.dumps({
            'ttft_median_ms': round(statistics.median(valid_ttft), 2),
            'first_round_ms': valid_ttft[0],
            'repeated_round_median_ms': round(
                statistics.median(valid_ttft[1:] or valid_ttft), 2
            )
        }, ensure_ascii=False))


if __name__ == '__main__':
    main()

例如:

python3 benchmark_ttft.py \
  --endpoint http://127.0.0.1:8000 \
  --model your-model-name \
  --rounds 8

如果服务需要鉴权,再添加:

--token "$INFERENCE_API_TOKEN"

首轮与后续请求的差异可以作为缓存效果的线索,但不能直接等同于 KV Cache 命中率。连接复用、模型预热、动态批处理和并发噪声都会影响结果。正式实验还应增加对照组:保持输入长度一致但改变前缀,并在相同并发下交替发送两组请求。

一个有说服力的方案应该闭合四个环节

围绕智算云问题,可以这样组织一个可验证的原型,而不是堆叠互不关联的功能。

1. 调度层:知道任务为什么等

调度器不仅要报告“资源不足”,还应区分 GPU 型号不匹配、拓扑约束、配额不足、资源碎片和 Gang 成员未就绪。参赛原型至少要让用户看到排队原因与持续时间。

2. 缓存层:知道哪些计算可以复用

KV Cache 优化需要定义缓存键、隔离边界与淘汰策略。多租户环境中不能为了命中率忽略数据隔离;包含敏感上下文的缓存也不能被其他租户复用。

可以重点比较:

  • 相同系统提示词、不同用户问题;
  • 完全相同的多轮上下文;
  • 不同租户下内容相同的请求;
  • 显存压力下缓存淘汰前后的延迟变化。

3. 分发层:避免每次启动都搬完整权重

模型权重分发可以采用共享存储、节点本地缓存、分层缓存或预热 DaemonSet。选择方案时不能只看下载速度,还要检查版本一致性、校验失败、磁盘回收以及滚动升级期间的双版本占用。

模型文件至少应带有不可变版本和摘要,例如:

model:
  repository: object-storage/models/example-llm
  revision: sha256-7b8d3f2
  expectedDigest: sha256:REPLACE_WITH_REAL_DIGEST
cache:
  nodeLocalPath: /var/lib/model-cache
  maxSizeGiB: 500
  evictionPolicy: lru

这是一份可改造的配置示例,并非赛事指定格式。真正下载模型时,应在加载前校验摘要,避免同名权重被覆盖后继续使用旧缓存。

4. 观测层:证明收益来自哪里

仪表盘不应只放一条 GPU 利用率曲线。建议同时展示:

  • 队列长度与排队时长;
  • TTFT、端到端延迟和 Token 吞吐;
  • GPU 利用率、显存占用与 OOM 次数;
  • KV Cache 命中、淘汰与占用;
  • 模型下载流量、加载耗时与节点缓存命中率。

把这些指标放在同一时间轴上,才能回答“延迟下降究竟来自缓存、预热、批处理还是负载下降”。

参赛与落地前的检查清单

DaoCloud 赛题提供了一个把真实集群难题做成开源原型的入口,额外现金激励则降低了选做成本。不过,具体奖金、提交方式、时间节点和评测口径仍应以大赛正式规则为准。

准备方案时,可以逐项检查:

  • [ ] 是否给出了可复现的硬件、模型、并发与输入配置;
  • [ ] 是否同时记录优化前后的基线;
  • [ ] 是否报告 P50、P95,而不只展示平均值;
  • [ ] 是否说明缓存隔离、数据安全和故障回退策略;
  • [ ] 是否覆盖模型升级、节点重启与缓存失效;
  • [ ] 是否能解释吞吐、延迟、显存和成本之间的取舍;
  • [ ] 是否提供一条命令即可运行的最小验证路径。

真正有价值的智算云方案,不是让某一次演示跑出漂亮数字,而是让任务在资源竞争、节点故障和模型升级时仍然跑得稳,并且能够用数据解释为什么更快。


相关推荐