30秒速览
- GPU按需实例跑LLM推理,每千token成本比OpenAI贵了7倍,必须上连续批处理和Spot混合才能打平
- MIG切分GPU听着美好,实际内存带宽争抢会让延迟飙到不可接受,用时间切片更稳
- 自建K8s集群省下的服务费根本不够填人力坑,EKS加Karpenter才是理性人的选择
- 存储和网络层面的“量化”——差分更新和gzip压缩——省的钱不比INT8少
- 别再用CPU利用率做AI服务的扩缩容标准了,KEDA加Prometheus才是正解
从“省着点花”到“精确到每token成本”——我在云账单里翻到的秘密
上个月我收到一张云账单,数字是上上个月的两倍多。财务总监直接把邮件转发给我,只写了三个字:“解释下。”那个项目是一个日活50万左右的视频内容推荐平台,我们给用户推送的每条视频摘要都要过一个7B的LLM做润色和高亮片段提取。推理服务跑在AWS的g5.2xlarge上(一块A10G),每天大概350万次请求。我天真地以为只要控制好实例数量、设好自动关机脚本就算成本优化到位了,结果现实狠狠地给我上了一课。
我翻了好几天Cost Explorer,发现罪魁祸首根本不是机器数量,而是我们根本没有理解“AI负载”和传统微服务负载的本质区别。传统的Web服务、API网关,CPU内存够用、请求延迟在几十毫秒之内就能搞定,按请求数估算容量也比较准。但GPU推理不一样:一次LLM生成可能耗时1到6秒不等,而且显存占用、批处理效率、KV cache复用率这些东西比CPU利用率更影响成本。更坑的是,AWS的GPU实例计费是按秒起步,哪怕你用了一秒钟,它也算一分钟。我当时跑的都是单请求同步处理,GPU的SM利用率经常在30%以下,大量时间在等下一个请求到来——妥妥地给云厂商送钱。
我花了两个通宵做了一个“每有效token成本”的分析模型。把我们每天的实际推理请求、生成的token数量、实例运行时长和费用拉出来,算出了一个惊人的数字:每生成1000个有效token(只统计最终返回给用户的,不含prompt)平均成本是$0.042。对比OpenAI的GPT-4o mini价格(当时每百万输出token $0.6),我们自建推理居然贵了快7倍!这还不算运维人力成本。那一刻我才明白,自建推理省钱就是个幻觉,除非你把底层基础设施的利用率拉到极致。
我开始把所有费用按维度拆解:
| 费用项 | 占比 | 优化空间 |
|---|---|---|
| 按需GPU实例 | 62% | 引入Spot + 预留实例 |
| 数据传输(跨AZ/出站) | 13% | 压缩、内网化 |
| EBS卷 & 快照 | 9% | 合并、删除孤立卷 |
| 负载均衡+网络 | 8% | 换轻量级代理 |
| 监控日志存储 | 5% | 采样、过期策略 |
| 其他(IP、支持费) | 3% | 几乎动不了 |
GPU实例的钱是大头,所以我先从这里动刀。但优化不能只用预留实例省钱那么简单,AI负载的弹性要求极高:晚高峰和凌晨的QPS能差8-10倍。预留实例买多了浪费,买少了扛不住。这时候必须把动态资源调度和请求排队做得足够精细。于是我做了几件事:
- 把推理服务从直连GPU的Flask进程改成基于Triton Inference Server的部署,利用它的dynamic batching和model concurrency。
- 引入请求优先级队列,非实时请求(如离线分析、A/B测试)可以用更低优先级的Spot实例处理。
- 自己写了一个成本跟踪器,每次推理结束后上报本次消耗的GPU计算时长,然后乘以我们实际的实例单价,算出单次成本。这样业务方可以拿着成本数据去判断某个功能是不是“配得上”这么贵的推理。
下面是我写的成本跟踪装饰器,集成在Triton的Python backend里:
import time
import threading
from dataclasses import dataclass
# 全局实例单价,从环境变量注入,精确到秒
GPU_INSTANCE_COST_PER_SEC = float(os.getenv("GPU_COST_PER_SEC", "0.00122")) # g5.2xlarge按需1.22美元/小时
@dataclass
class CostRecord:
request_id: str
gpu_time_ms: float
cost: float
class CostTracker:
def __init__(self):
self._lock = threading.Lock()
self.records = []
def add(self, record: CostRecord):
with self._lock:
self.records.append(record)
def flush_and_report(self):
# 每5秒批量发送到监控后端,减少网络开销
pass
tracker = CostTracker()
def track_gpu_cost(instance_cost_per_sec=GPU_INSTANCE_COST_PER_SEC):
"""
装饰器:计算GPU kernel执行时长,然后换算成美元。
注意:这里只统计CUDA kernel时间,不包含CPU预处理,
因为我们只关心GPU这台昂贵机器的真实工作时间。
"""
def decorator(func):
def wrapper(*args, **kwargs):
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
# 确保前面没有未完成的kernel干扰时间测量
torch.cuda.synchronize()
start_event.record()
result = func(*args, **kwargs)
end_event.record()
torch.cuda.synchronize() # 等待kernel执行完
elapsed_ms = start_event.elapsed_time(end_event) # 毫秒
cost = (elapsed_ms / 1000) * instance_cost_per_sec
# 记录但不阻塞
tracker.add(CostRecord(request_id=kwargs.get("request_id", ""),
gpu_time_ms=elapsed_ms, cost=cost))
return result
return wrapper
return decorator
# 在Triton Python model execute里使用
class TritonPythonModel:
@track_gpu_cost()
def _run_inference(self, input_tensor):
# 实际模型调用
return self.model.generate(input_tensor)
def execute(self, requests):
# ...准备输入
output = self._run_inference(input_tensor, request_id=requests[0].request_id())
# ...返回响应
有了这个数据,我就可以准确告诉产品经理:“你那个每次强制生成三句摘要的需求,平均一次消耗0.002秒GPU时间,折合0.0000024美元,但每天600万次,一天就是14.4美元,一个月就是432美元。”这种精确到几分钱的话术可比“我们的GPU推理太贵了”有力得多,财务和业务两边都买账。
这一步只解决了成本可视化,还不够。下一阶段我直接把GPU实例的计费模式玩出了花。
我把Spot实例当长期饭票用,差点被中断率教做人
Spot实例(AWS的叫法,GCP叫Preemptible VM,Azure叫Spot VMs)的折扣最高能到90%,对我们这种弹性AI推理服务简直是天然的诱惑。我们最初用按需实例跑一个A10G,时薪$1.22。如果用Spot,价格经常能到$0.35以下。一算账,如果能把80%的负载迁移到Spot上,年成本能省出一辆Model Y。
于是我兴冲冲地在Auto Scaling Group里加了Spot容量,把按需实例的比例压到20%。刚上线那两天一切正常,第三天中午我正在吃饭,告警突然炸了:推理延迟从P99 1.2s飙升到8s,错误率5%。登陆上去一看,10台Spot实例在2分钟内被回收了7台,新的Spot请求因为当时可用区容量不足一直pending。Triton的服务端队列瞬间堆积了几万个请求,连带着几个按需实例的显存被KV cache撑爆导致OOM重启。整个推荐流直接掉了18分钟,用户看到的都是“暂时无法生成摘要”的占位图。
那次事故给我上了深刻的一课:Spot中断不只是“可能会丢一台机器”,它可能引起雪崩。我花了两周时间重写了调度层,做了下面几项硬核改造:
- 基于中断预告的优雅退出。 AWS Spot实例在被回收前两分钟会通过metadata服务发出中断通知,我可以提前把模型从Triton的模型列表里unload、拒绝新请求、等待现有请求处理完再关机。但两分钟对LLM长生成可能不够,所以我把max_tokens限制在512以内,保证最坏情况下也能在30s内完成。
- 异构节点池+反亲和调度。 不用单一的Spot容量池,而是混合g5.2xlarge (A10G)、g5.4xlarge (单A10G但可用区不同)、甚至g4dn.xlarge (T4) 专门跑轻量级的摘要打分而非生成。Kubernetes里通过nodeSelector和topologySpreadConstraints让Pod分散在不同可用区,避免被一锅端。
- 请求缓冲与重试透明化。 在Triton前面加了一个Envoy代理,做了请求级重试和指数退避。当某个后端返回UNAVAILABLE时,Envoy自动换一个实例重试,应用层无感知。
下面是Kubernetes部署中Spot实例的Pod配置片段,注意我用的lifecycle preStop 和 terminationGracePeriodSeconds:
apiVersion: apps/v1
kind: Deployment
metadata:
name: triton-llm-spot
spec:
replicas: 4
selector:
matchLabels:
app: triton-llm
template:
metadata:
labels:
app: triton-llm
spot: "true"
spec:
# 只调度到带有spot标签的节点
nodeSelector:
lifecycle: spot
# 尽量分散在不同的AZ
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: triton-llm
terminationGracePeriodSeconds: 90 # 给两分钟通知留出充足时间
containers:
- name: triton
image: nvcr.io/nvidia/tritonserver:24.01-py3
args: ["tritonserver", "--model-repository=/models", "--model-control-mode=explicit"]
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
echo "Spot interruption, draining..."
# 从Triton里卸载模型,拒绝新请求
curl -X POST localhost:8000/v2/repository/models/llm_7b/unload
# 等待现有请求处理完毕,最多60秒
sleep 60
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
resources:
limits:
nvidia.com/gpu: 1
中断的处理逻辑还需要在应用层配合,我在Triton的Python backend里加了一个健康检查端点,当发现即将中断的信号时(从文件标志位读取),立即开始拒绝新推理请求并返回特定状态码,告诉Envoy“我不接客了,找别人去”。这个标志位由一个小型sidecar容器监控Spot中断通知后写入共享emptyDir。
经过这么一通折腾,我们的Spot可用率稳定在了99.7%。但这里有个成本上的“小九九”:由于需要预留缓冲,我们不能真把Spot比例拉到95%,必须留差不多35%的按需或预留实例兜底。我最终找到了一个黄金比例:65% Spot + 25% 预留实例(一年期,便宜40%)+ 10% 按需动态浮动。整体GPU成本降到了$0.52/小时,是原来纯按需的42%。
别被厂商宣传骗了,MIG分区的性能损耗能到15%
A100和H100上的MIG(Multi-Instance GPU)技术听起来很美好——把一张80GB的A100切成7个独立的GPU实例,每个10GB显存,跑多个小模型或者多租户推理。我当时想的是把一张A100切成四块5GB的分区,分别跑4个不同的模型(一个图像分类、一个文本向量化、两个LLM),这样一台机器当四台用,省到飞起。
现实是,切完分区之后,第一个跑benchmark的同事就来找我了:“图像分类的延迟从12ms涨到了18ms,而且抖动非常厉害。”我一开始以为是驱动bug,反复重装、调整MIG配置,问题依旧。后来用nvidia-smi和NVIDIA Nsight分析,发现MIG分区虽然显存和计算单元物理隔离,但L2缓存、内存带宽、DRAM控制器这些共享资源根本不可能完全隔离。当一个分区的模型在做大batch的KV cache存取时,另一个分区的计算核会明显受到带宽抢占的影响。
我测试了三种配置:
| 配置 | 模型A延迟 (P50) | 模型B延迟 | 总吞吐提升 |
|---|---|---|---|
| 单进程独占A100 | 12ms | – | baseline |
| MIG 2g.10gb 两个分区 | 13~14ms | 13ms | 1.85x |
| MIG 3g.20gb 两个分区 | 15ms | 15ms | 1.6x |
| MIG 1g.5gb 四个分区 | 18ms (抖动严重) | 20ms | 2.2x 但不稳定 |
性能损耗最小的是2g配置(大约8-10%),但显存只有10GB,装不下7B模型。3g.20gb勉强装下,但延迟已经开始漂移。4个1g分区完全没法跑LLM,只能跑轻量模型,而且干扰严重。最终我放弃了MIG方案,转向了GPU时间切片(Time-Slicing)配合CUDA MPS(Multi-Process Service),虽然隔离性差一些,但延迟更可控。
下面是我在Kubernetes里配置时间切片的device plugin,让多Pod共享GPU:
# 安装 nvidia-device-plugin 带 time-slicing 配置
# 在 daemonset args 里加入如下配置文件,mount进容器
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
data:
time-slicing-config.yaml: |
version: v1
sharing:
timeSlicing:
renameByDefault: false
resources:
- name: nvidia.com/gpu
replicas: 4 # 每个物理GPU被分成4个逻辑GPU
这样,一个Pod请求nvidia.com/gpu: 1时,实际上只占用了1/4的GPU时间片。但要注意,时间切片不隔离显存,如果两个Pod同时加载了7B模型,显存直接爆掉。所以我规定同节点的Pod必须加载同一个模型的不同副本,通过模型预热镜像把模型权重加载到共享内存,再由各进程mmap,避免重复占用显存。这又引出了另一个优化:模型存储和加载的IO。
时间切片方案最终让单张A100同时服务3个LLM推理进程(每个4GB显存占用,共享模型权重后总占用约18GB),整体吞吐量提升了2.6倍,P99延迟增加不到20%,比MIG实用得多。MIG只适合那种能严格划分内存、对延迟波动容忍度极高的批处理任务,不适合在线LLM服务。
Triton动态批处理:延迟和吞吐的刀尖上跳舞
动态批处理(dynamic batching)是推理服务优化的核心手段,但坑也多到可以写一本书。我们的LLM推理服务在单请求模式下,GPU的计算单元利用率(SM utilization)常年在30%徘徊,因为token生成是一步步出的,单次矩阵乘的维度很小,根本喂不饱GPU。只有把多个请求拼成batch,才能把计算密度提上来。
Triton提供了两种批处理方式:默认的dynamic batcher和inflight batching。一开始我用默认的,设了max_batch_size=8,delay=100微秒。结果发现小batch还行,batch一大,显存分配速度跟不上,反而增加延迟。后来我把整个架构改了,用continuous batching(也就是inflight batcher),让请求在迭代层面混合:任何时候有token生成结束的请求退出,新请求立刻补上。这样GPU的计算队列永远不空。
下面是Triton的模型配置文件,启用inflight batching:
# config.pbtxt
name: "llama_7b"
backend: "python"
max_batch_size: 16
parameters: [
{
key: "enable_inflight_batching",
value: { string_value: "true" }
}
]
instance_group: [
{
count: 1,
kind: KIND_GPU,
gpus: [ 0 ]
}
]
# 调度策略设为batch和stream结合
dynamic_batching {
max_queue_delay_microseconds: 50
}
但光配Triton还不够,Python backend里的代码必须支持从batch中提取单个请求的token完成状态,并适时插入新请求。我用的是vLLM的引擎做底层推理,它原生支持continuous batching。当我把这个配置上线后,单个A10G的吞吐从350 tokens/s飙到了1200 tokens/s,成本直接打三折。
代价是延迟的P99从1.2s变成了2.8s。因为continuous batching为了等更多请求拼batch,单个请求可能会被拖慢。但我们的业务对摘要生成的延迟要求是小于5秒,2.8秒完全可以接受。这就是trade-off——你愿意为了吞吐量牺牲一点延迟吗?我和业务方签了一个SLA:只要90%的请求在3秒内完成,成本减了多少我就给他们展示,最后他们欣然接受了。
这里有个经验教训:不要自己折腾batching逻辑,除非你是搞CUDA kernel开发的。直接用vLLM、TensorRT-LLM这种成熟的连续批处理引擎,让它们和Triton集成。我早期手写了一个Python的batching队列,靠asyncio和信号量,结果死锁、内存泄漏、请求饥饿轮着来,连续三周凌晨两点被叫起来。
自建Kubernetes集群的200天:一个运维黑洞的诞生
2024年初,我所在的团队曾经试图完全自建Kubernetes集群来跑AI推理——觉得托管服务(EKS, GKE)太贵,而且定制化受限。我们用terraform在三台裸金属服务器上搭了K8s,配上NVIDIA device plugin、Prometheus、Grafana、Istio。最开始感觉掌握了一切,但很快就被现实暴击。
首先是GPU驱动的安装问题。裸金属的NVIDIA驱动需要和内核版本匹配,一旦自动升级了内核,驱动就挂掉,然后所有的GPU Pod全部CrashLoopBackOff。我们甚至发生过内核恐慌直接导致整台机器重启的事故,因为某个CUDA版本和内核模块冲突。托管服务的节点组会自动处理这些,但自建就得自己写systemd service、hook住apt upgrade。维护成本直线上升。
其次,网络CNI插件Calico的BGP路由在大流量下出现了丢包,导致推理服务的gRPC流断连。我们不得不切到Cilium,又是一通折腾。存储方面,用Longhorn做分布式块存储跑模型仓库,结果IOPS不到100,加载一个13B模型要15分钟,严重影响Pod的启动时间。后来换成节点本地NVMe SSD + rsync同步模型文件,才解决问题。
最终压垮我们的是升级Kubernetes版本。从1.28升1.29,好几个API deprecation,一堆自定义controller不兼容,需要重写。团队里只有两个SRE能搞定,但他们还要管公司的其他基础设施。连续两个版本的升级都花了接近两周的深夜时间,最后我拍板:放弃自建,回滚到EKS,哪怕多付点服务费。
迁移到EKS后,我们计算了一下,虽然EKS集群管理费每月$73,加上节点组的运维少了很多人工投入。我们两个SRE之前每月至少投入80小时在维护K8s上,换算成人力成本远超EKS费用。自建集群的200天经历让我彻底明白:除非团队有专门的K8s平台组,而且业务量极大、极致压榨成本,否则托管服务就是最优解。
但托管不是“撒手不管”。我仍然在EKS上做了很多优化,比如用Karpenter代替Cluster Autoscaler,让节点弹性伸缩快了一倍;使用自定义的AMI预先缓存模型权重和Triton server镜像,缩短节点启动时间到2分钟以内。这些优化代码在下一节具体说。
量化不只有INT8,存储和网络也要“瘦身”
一说到AI负载的优化,所有人第一反应是模型量化:FP16到INT8,甚至INT4。但我在实践中发现,基础设施层面的“量化”——即减少数据传输和存储的体积——带来的成本节省不比模型量化小。
先看存储。我们的模型仓库里有十几个不同版本的7B模型、embedding模型、重排序模型,每个动辄十几GB。每次发版、回滚要在节点间拷贝,网络流量和EBS快照费用都很高。我引入了Delta update机制:不用全量传输模型文件,而是通过rsync或zstandard差分只传变更的权重分片。同时,在模型存储上使用了写时复制(copy-on-write)的文件系统,配合btrfs的reflink,让不同版本的模型共享相同的基础权重,仅保存差异部分。这让我们模型仓库的EBS使用量从420GB降到了110GB,快照成本降了70%。
网络层,LLM推理服务返回的JSON响应中经常有大段的文本内容,客户端再解析展示。我们原先没用压缩,因为认为CPU压缩会增加延迟。后来做了压测:在响应体大于2KB时启用gzip压缩,CPU多消耗0.3ms,但网络传输数据量减少60%以上(文本压缩比高)。这不仅能省出站流量费,还能降低客户端加载时间,特别是在移动网络下。我们直接在Envoy代理上开启了compress过滤器,对content-type为application/json的响应自动gzip,效果立竿见影。出站流量费用从每天$38降到了$14。
然后是模型推理的提示词(prompt)压缩。很多请求的prompt重复度极高,比如系统指令、上下文前缀。我们自己在Triton前端写了一个缓存层,根据prompt的哈希值缓存相应的KV cache(实际上是vLLM自动的prefix caching)。但即使这样,原始提示词的传输也消耗网络。我在客户端SDK里加入了prompt模板引用,只传变化的部分,服务端拼装完整prompt。比如一个请求只需要传{“template”: “summary_v2”, “params”: {“title”: “xxx”, “content”: “yyy”}},服务端内部把内容填入大段的system prompt模板。这又减少了约40%的请求体积,进一步降低出站和入站流量费。
下面是我们的Envoy配置,启用gzip和请求体大小限制:
# envoy.yaml 片段
http_filters:
- name: envoy.filters.http.compressor
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor
compressor_library:
name: text_optimized
typed_config:
"@type": type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip
memory_level: 5
compression_level: BEST_SPEED # 低CPU开销,高压缩率
compression_strategy: RLE
request_direction_config: {}
response_direction_config:
common_config:
min_content_length: 200 # 小于200字节不压缩
content_type:
- "application/json"
存储和网络的“瘦身”加起来,一个月省了约$1200,几乎相当于免费多跑了一个模型实例。这些优化不像模型量化那样性感,但都是实实在在地把成本数字往下砸。
当CPU使用率不再是扩缩容的唯一标准:AI负载下的HPA变形记
传统的Kubernetes水平自动伸缩(HPA)看CPU和内存利用率,这招对AI推理服务几乎完全失灵。GPU推理的CPU使用率不高(因为大部分活儿在GPU上),内存占用也基本恒定(模型加载后就稳定了)。真正能反映负载压力的是请求排队深度、GPU SM利用率和推理延迟。
一开始我们试图用HPA的external metrics,通过Prometheus查询请求队列长度来自动伸缩。配置如下:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: triton-llm-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: triton-llm
minReplicas: 2
maxReplicas: 10
metrics:
- type: External
external:
metric:
name: triton_pending_request_count
target:
type: AverageValue
averageValue: "5" # 平均每个Pod等待请求数超过5就扩容
但是队列长度是个滞后指标,扩容要等新Pod启动、模型加载完成,这个过程需要3-5分钟。流量激增的时候,往往来不及。我们改用了KEDA(Kubernetes Event-driven Autoscaling),它支持更灵活的触发器,并且可以把伸缩决策前置。
我用KEDA的Prometheus scaler监控Triton暴露的`nv_inference_queue_duration_us`指标,当排队时间中位数超过500ms就触发扩容。更重要的是,KEDA支持缩容到零,凌晨低峰期我们直接把推理服务缩到零,配合请求到来时通过冷启动触发器(比如HTTP请求到达时由网关唤醒),实现Serverless式的GPU推理。虽然冷启动需要约2分钟,但凌晨的请求极少,延迟多一点完全可以接受。
但唤醒冷启动对LLM是灾难,因为模型加载太慢。于是我们做了一个trick:不缩容到零Pod,而是保留一个最小实例(minReplicas=1),但这个实例在低负载时自动切换到一个更小更便宜的模型版本,比如从7B切换到1B,或者切换到CPU推理(用llama.cpp跑Q4量化的7B模型),虽然慢一点但凌晨请求量极少,能扛住。等流量上来,KEDA触发扩容,新Pod带7B模型上线,然后旧的迷你Pod被替换掉。这个切换通过修改Service的selector和一个蓝绿Deployment实现。
下面是我用KEDA定义的ScaledObject,配置冷却时间避免频繁伸缩抖动:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: triton-llm-scaler
spec:
scaleTargetRef:
name: triton-llm
minReplicaCount: 1 # 至少留一个迷你模型
maxReplicaCount: 8
cooldownPeriod: 300 # 缩容冷却5分钟
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server.monitoring.svc:9090
metricName: triton_queue_duration_p50
threshold: '500000' # 500ms,单位微秒
query: |
avg(rate(nv_inference_queue_duration_us{model="llama_7b", quantile="0.5"}[1m]))
为了支持凌晨的迷你模型切换,我还写了一个简单的operator,监听一个ConfigMap里的“mode”字段,动态调整Deployment的镜像和环境变量。这种做法虽然不优雅,但有效。省下了一台A10G实例凌晨7小时的花费,一个月又省了$250。
性能调优和成本控制在AI负载下是强耦合的,不能头痛医头。我从账单分析、Spot混合、动态批处理、MIG踩坑、存储网络量化、伸缩策略多个角度切入,最终把那个视频推荐平台的月GPU成本从$8,400压到了$3,100,P99延迟还略降了一点。最关键的是,现在我可以随时掏出单次推理的成本数据,告诉业务方他们花的每一分钱花在了哪里,这种透明让技术团队从“烧钱的黑洞”变成了成本优化的英雄。