参数组 1/8 · 调度
代码基线 a71b766ce(2026-09-23)
讲解范围:vllm_ascend/ascend_config.py 中 scheduler_config 子配置
(enable_balance_scheduling / recompute_scheduler_enable / short_request_first_config / profiling_chunk_config /
batch_job_sched_config / dyntra_lb_config)+ 顶层 enable_reduce_sample、mega_moe_max_tokens,
以及它们的 8 个实现文件(BalanceScheduler、ShortRequestFirstRequestQueue、ProfilingChunkScheduler、
RecomputeScheduler、BatchJobAwareScheduler、DyntraLBDPEngineCoreProc、sampler 分支、mega_moe 算子加载)。
一句话:这批参数控制"推理服务每一小步,算哪些请求的哪些 token"。 大模型生成一个回答不是一次算完的,而是一个字一个字往外蹦(后面第③节会从常识爬到这)。 每"蹦"一步,调度器(scheduler,一个专门决定下一步算什么的程序)就要重新组织一批活儿。 vLLM 上游有一套通用调度器,但昇腾集群上有几个它没管好的问题——多个实例忙闲不均、 长短请求互相堵、预填充块大小靠拍脑袋、MoE 通信算子有容量上限——vllm-ascend 就用这 8 个参数 把自家的调度增强"插"进去。每个参数对应一个具体问题,这是本文唯一需要先记住的事:
忙闲不均 → enable_balance_scheduling 卡间不均 → dyntra_lb_config 长堵短 → short_request_first_config chunk 拍脑袋 → profiling_chunk_config 抢占代价高 → recompute_scheduler_enable 在线离线混跑 → batch_job_sched_config MoE 融合算子容量 → mega_moe_max_tokens 采样重复计算 → enable_reduce_sample
先明确这段代码"住"在哪。vLLM 是推理服务框架的上游(负责通用逻辑),vllm-ascend 是它
在昇腾 NPU 硬件上的插件(负责让同样的逻辑在华为芯片上跑得快)。
调度器本体在上游 vllm.v1.core.sched.scheduler.Scheduler;
vllm-ascend 不改上游代码,而是在启动时按参数把调度器"换"成自己的子类
(这个"偷梁换柱"发生在 vllm_ascend 的 patch 机制里,第④节看完整链路)。
级 1 · LLM 的回答是逐字蹦出来的。常见的普通 Web 服务(比如查订单),一次请求 就是一次数据库查询、一次返回,服务器"处理完"这个请求就结束了。但大模型不同:它回答一段话, 是把这段话一个 token 一个 token 接连生成的(token 可粗略理解为"词元",模型的最小文字单位)。 生成 500 字的回答,模型的前向计算(forward,喂入内容算出下一个词)要跑几十上百次。 所以同一个请求,会在服务器里活很久、反复被计算。
级 2 · 所以"调度"是每一步都在发生的动作。既然所有请求都是半成品、都要反复计算, 服务器每个时刻只能算一批(芯片算力有限)。于是需要一个角色,每算完一步就重新决定: 下一步把哪些请求、各自算到哪、一起凑成一个批次(batch,一起送进芯片的一组任务)。 这个角色就是调度器(scheduler)。它每步的输出叫 SchedulerOutput——一张"这一步都算什么"的清单。 调度器不是启动时跑一次就完事,而是和计算交替进行的循环。
级 3 · 组 batch 的本质是"花两本预算"。调度器凭什么决定算谁、不算谁?凭两本预算: 一本是 KV cache(键值缓存)的显存块——每个请求算过的历史都要缓存下来供下一步复用, 缓存按"块"(block,固定大小的显存单元)计费,显存就那么多; 另一本是每步 token 总量上限——一步最多送多少个 token 进芯片。调度器的所有策略, 拆开看都是"这两本预算怎么分配":先给谁、给多少、什么时候抢回来(抢占,preempt:预算不够时 把某些请求的缓存扔掉、退回重算)。
级 4 · 昇腾集群上,通用调度器少了四样东西。上游 vLLM 的调度器是为 NVIDIA 生态写的, 拿到昇腾多卡集群上,出现了四类它没覆盖的问题:① 多实例忙闲不均——为了容量和隔离, 服务常开多份数据并行实例(DP,data parallel:几个实例各自持有一份模型、各自接请求), 上游调度器各管各的,有的实例排长队、有的在发呆;② 长请求堵短请求——一个超长输入 (比如整本书)的预填充会占住预算很久,排它后面的短问题("今天天气?")明明一秒能答完却干等; ③ 预填充块大小没依据——长输入会切块(chunk)分步算,块切多大影响时延和吞吐, 上游是固定值;④ 昇腾特有的算子与并行形态——MoE(专家混合,一种把大模型拆成多个 "专家"子网络按需调用的结构)的通信计算融合算子有 token 容量上限,跨 DP 的细粒度并行需要 "重算代替干等",采样阶段有可以省掉的重复计算。8 个参数正是按这四类问题对号入座的。
术语表(回头查阅用,不是首次引入):token=词元,模型最小文字单位;forward=前向计算; scheduler=调度器;batch=批次;KV cache=键值缓存(请求的"记忆",按块计费);block=显存块; preempt=抢占(收回预算,缓存丢弃或卸载);DP=data parallel 数据并行(多实例各持模型各接请求); chunk=把长输入切块分步算;MoE=专家混合结构;HCCL=华为集合通信库(多卡间通信的底层); symmetric buffer=对称缓冲(多卡可直接互相读写的共享显存区)。
这 8 个参数的生效路径完全一致,学会一次就认识全家。启动命令里写下
--additional-config '{"scheduler_config": {"enable_balance_scheduling": true}}',
这串 JSON 被解析进 AscendConfig(vllm_ascend 自己的配置容器,ascend_config.py),
先做类型与互斥校验(比如 profiling_chunk 与 balance 不能同时开,第⑦节细讲),
然后平台的 patch 机制按参数值把上游调度器替换成对应子类或注入队列。
也就是说:每个参数背后都站着"一个真实存在的类/分支",参数只是扳道岔。
下面这段是参数的"户口本":全部 6 个子配置都在 scheduler_config 名下
(行号 391-423),另有 enable_reduce_sample(L464)与
mega_moe_max_tokens(L483)在顶层。每行默认值就是"不开"——
vllm-ascend 的原则是增强特性全部 opt-in(需显式打开)。
vllm_ascend/ascend_config.py · AscendConfig 默认值示例 · L391-423 "scheduler_config": { # ← 6 个调度子配置都住这里
"enable_balance_scheduling": false, # ⚡多实例均衡调度(→5.1)
"recompute_scheduler_enable": false, # ⚡重计算调度(→5.5)
"short_request_first_config": {
"enabled": false,
"threshold": 256, # ↳ prompt ≤256 token 算"短请求"
"long_max_wait_ms": 0.0 # ↳ 长请求最多被晾这么久(防饿死)
},
"profiling_chunk_config": {
"enabled": false,
"smooth_factor": 1.0,
"min_chunk": 4096,
"need_timing": null,
"max_fit_chunk": 30
},
"batch_job_sched_config": {
"enabled": false,
"max_jobs": 20,
"reserve_margin_blocks": 2,
"reserve_max_blocks": 8,
"low_available_tokens_threshold": 4096,
"short_decode_token_threshold": 32
},
"dyntra_lb_config": {
"enabled": false,
...
"bubble_threshold": 5.0, # ↳ 气泡超过均值 5 倍才触发迁移
"long_req_block_threshold": 700,
},
},
下面 8 节结构完全相同:先说病,再看治法(图),最后对代码。每段代码都来自 a71b766ce 真实源码,行号可直接跳转核对。
病:服务开了多份 DP 实例(每个实例一份模型、各自接请求),上游调度器各管各的。 请求分配稍有不均,就会出现 rank0 排着 12 个请求忙到冒烟、rank1 空转发呆——整体吞吐被最闲的那张卡拉低。
治法:开了这个开关后,调度器被换成 BalanceScheduler。它在每一轮循环里用
all_gather(集合通信:所有人上交自己的数字,同时拿到所有人的数字)汇总各实例正在处理的请求数,
把全局视图注入调度决策。代码里有一句非常值得学的注释:all_gather 是集体操作,任何 rank 都不能缺席——
哪怕某个实例这轮没活儿(空转轮),也必须参与 gather,否则忙的实例会永远等它,全组死锁。
vllm_ascend/patch/platform/patch_balance_schedule.py · BalanceScheduler.balance_gather · L146-162 def balance_gather(self):
"""All-gather per-rank running counts into ``self.balance_queue``.
... This MUST be invoked on every rank every non-idle iteration, including
dummy-batch iterations where a drained rank never enters ``schedule()`` --
``all_gather`` is a collective, so any rank skipping it deadlocks
the busy ranks. ← ⚡集体操作纪律:缺席即死锁
"""
if not self._balance_enabled or self.dp_group is None:
return
running_tensor = torch.tensor([len(self.running)], dtype=torch.int, device="cpu")
dist.all_gather(self.balance_queue, running_tensor, group=self.dp_group) # →NPU/HCCL 集合通信
病:5.1 只看"请求数",但请求的胖瘦差别巨大(KV cache 占的块数不同)。 数量均衡了,显存占用照样可能一头撑死一头空转——空转比例就是气泡(bubble)。
治法:DyntraLBDPEngineCoreProc._balance_load(DyntraLB = 动态负载均衡)改按
KV 块数衡量负载:算出"最忙卡的块数相对平均值的超出倍数",超过
bubble_threshold(默认 5.0)才动手;动手的方式是产出 _Modification
(每张卡的迁出块 / 迁入块 / 是否冻结)清单,把请求从忙卡搬去闲卡。注意
start_step=250:服务刚起的前 250 步请求分布本来就不稳,先不搬,避免抖动。
vllm_ascend/patch/platform/patch_dyntra_lb_core.py · DyntraLBDPEngineCoreProc._balance_load · L121-136class DyntraLBDPEngineCoreProc(DPEngineCoreProc):
@staticmethod
def _balance_load(
requests_by_rank: Sequence[tuple[Sequence[int], int]], # ↳ 各卡(请求块列表, 总块数)
dev_num: int,
max_num_seqs: int | None = None,
max_iters: int = 1000,
threshold: float = 5, # ↳ 即 bubble_threshold,气泡倍数阈值
) -> list[_Modification]:
modifications: list[_Modification] = [{"out_blk": [], "in_blk": [], "freeze": False} for _ in range(dev_num)]
def calc_bubble(cards_info: Sequence[_CardData]) -> float: # ⚡气泡 = 块数不均的程度
tot_blks = [c["tot_blk"] for c in cards_info]
max_blk = max(tot_blks)
avg_blk = sum(tot_blks) / dev_num
病:一个 2 万 token 的长文档和一句"今天天气?"同时到达。预填充(prefill,处理输入) 按 chunk 切着算,长请求反复占预算,短请求排在后面——明明一秒能答完,TTFT(首字延迟)却被拖到几十秒。
治法:把等待队列拆成三份:immediate(系统插单)、short
(prompt ≤ threshold=256 token)、long。取请求时严格按优先级:
立即队列 → 但要防饿死:长队列队头等太久(超过 long_max_wait_ms)且短队列非空时,
强制"晋升"长请求先走(注释里的 aged long promotions;连续晋升还会触发告警——那是阈值设歪了的信号)。
于是默认顺序是短先行、长请求有保底等待时间。
vllm_ascend/core/short_request_first_scheduler.py · ShortRequestFirstRequestQueue · L148-183(节选) def _classify_queue(self, request: Request) -> RequestQueue:
if self.immediate_predicate is not None and self.immediate_predicate(request):
return self._immediate_queue
if request.num_prompt_tokens <= self.threshold: # ⚡threshold=256:短请求判据
return self._short_queue
return self._long_queue
def _long_head_wait_ms(self) -> float | None:
if self.long_max_wait_ms <= 0 or not self._long_queue:
return None
head = self._long_queue.peek_request()
enqueued_at = self._long_enqueue_at.get(head.request_id)
if enqueued_at is None:
return None
return (time.monotonic() - enqueued_at) * 1000.0 # ↳ 长请求队头已等多久
def _select_schedulable_queue(self) -> RequestQueue | None:
if self._immediate_queue:
return self._immediate_queue # ⚡1. 插单最优先
if self._long_queue:
wait_ms = self._long_head_wait_ms()
if wait_ms is not None and wait_ms >= self.long_max_wait_ms and self._short_queue:
return self._long_queue # ⚡2. 防饿死:长请求等满时限,插队
if self._short_queue:
return self._short_queue # ⚡3. 默认短先行
if self._long_queue:
return self._long_queue
return None
病:长输入要切块(chunk)分步预填充。块太小→步数多、每步固定开销白掏; 块太大→单步时延尖刺、在线请求被卡。切多大最好取决于具体模型和硬件——拍脑袋定一个数, 换模型就失准。
治法:ProfilingChunkScheduler 在模型加载完成后,用真实的前向计算
跑一遍不同 chunk 大小、采集时延样本、拟合出"chunk 大小 → 时延"的二次曲线;此后每次调度按曲线
和 smooth_factor(平滑系数,防抖动)选当前最优 chunk,限制在
min_chunk(4096)与 max_fit_chunk(30 块)之间。
注意 它与 enable_balance_scheduling 互斥(两者都重写调度主流程,ascend_config.py:596 校验),二选一。
vllm_ascend/core/scheduler_profiling_chunk.py · ProfilingChunkScheduler.run_profiling_chunk_init · L127-135 def run_profiling_chunk_init(self, model_executor) -> None:
"""Profile prefill latency using real model forward passes.
Called by EngineCore after model_executor is ready. Collects latency
samples at different chunk sizes and fits the quadratic model.
""" # ⚡不是估算,是真跑模型计时
if self._profiling_initialized:
return
self._profiling_initialized = True
if model_executor is None:
logger.warning("[ProfilingChunk] No model_executor provided, skipping profiling")
return
病:显存预算不够时,调度器只能抢占(preempt)——把某些请求的 KV cache 扔掉、把它们退回等待队列。上游的做法是之后重算这些 KV:长输入重算一次预填充非常贵, 在 PD 分离(prefill/decode 分开部署)形态下更麻烦。
治法:RecomputeScheduler 改成"尽力而为":被抢占请求的 KV
能卸载就卸载(offload 到主机侧),实在卸不动才送回 P 节点重做 prefill(这就是名字里的 recompute);
顺带为投机解码固定图形状(给首个 decode 请求补位)。它还是 DyntraLB 的基座:
留了两个钩子(_apply_load_balance_modifications 应用跨卡迁移、
_can_admit_waiting_request 过滤准入),5.2 的迁移请求靠它安全落地。
另一个身份是前置条件:细粒度 TP(finegrained_tp_config)的跨 DP HCCL 通信要求它开启
(ascend_config.py:690 校验,不开直接报错)。
vllm_ascend/core/recompute_scheduler.py · RecomputeScheduler · L85-103class RecomputeScheduler(Scheduler):
"""Use vLLM scheduling with best-effort decode-side preemption offload.
This keeps a local copy of vLLM's schedule() only to pad the first decode
request for stable Ascend speculative-decode graph shapes. Preempted KV is
offloaded when possible; otherwise the request is sent back to P to redo
prefill. ← ⚡卸载优先,重算兜底(名字的由来)
"""
prefill_capacity_bound: bool
def _apply_load_balance_modifications(self) -> None:
"""Hook for DyntraLB to apply cross-rank scheduling decisions.""" # ⚡5.2 的迁移在此落地
def _can_admit_waiting_request(self, request: Request) -> bool:
"""Hook for DyntraLB to filter waiting-request admission."""
return True
病:推理服务常要混跑两类流量:在线用户请求(要低延迟)和批量作业 (batch job,比如强化学习训练的推理回滚、离线评测——要大吞吐,一个作业可能生成几万个 token, 而且没人知道它会生成多长)。离线作业混在普通队列里,会悄悄吃光 KV 块预算,把在线请求饿死。
治法:BatchJobAwareScheduler 给离线作业建了一本"学习账":
从请求 ID 里解析出作业名(JobNameParser)→ JobDecodeEstimator 用
EWMA(指数加权平均:最近的历史权重更大)在线学习"这个作业平均生成多长"
(没见过的作业用冷启动默认值)→ RunningBlockReserver 按预测值给在跑作业
预留 KV 块(预留上限 reserve_max_blocks=8、余量 reserve_margin_blocks=2),
可用块跌破 low_available_tokens_threshold(4096)时限制新作业进入。
decode 很短(≤short_decode_token_threshold=32)的作业不占预留。
vllm_ascend/core/batch_job_aware_scheduler.py · JobDecodeEstimator · L136-148 def predict(self, job_name: str) -> int:
"""Return the predicted decode length for *job*."""
job_stat = self._job_stats.get(job_name)
if job_stat is None:
return self._COLD_START_DEFAULT_DECODE # ⚡没见过的新作业:冷启动默认值
return int(job_stat.ewma_average) # ↳ 历史 EWMA 均值
def observe(self, job_name: str, decode_len: int) -> None:
"""Record one decode‑length observation for *job*.
This method updates the job statistics using the EWMA (Exponentially
Weighted Moving Average) formula for online learning.
""" # ⚡每结束一个请求就回填,越学越准
病:MoE 层每个 token 要发给若干专家、算完再收回来(dispatch/combine 两次全卡通信)。 昇腾把"通信 + 大矩阵乘"融成了 mega MoE 算子:多卡直接读写一块 对称缓冲(symmetric buffer:多卡都能寻址的共享显存)来交换 token——但缓冲区在分配时 就要申报容量:最多接多少 token。
治法:mega_moe_max_tokens(默认 65536)就是 P 节点 / PD 混合节点上
这块缓冲的容量申报值。代码里有个关键保护:申报值超过算子的绝对安全上限
(absolute_safe_max_recv_token_num)会被夹住;反过来如果实际 dispatch 后
单卡收到的 token 数超过申报值,官方注释原话是会发生精度劣化(precision degradation)——
所以这不仅是性能参数,还是精度风险点: 专家数多 / batch 打得猛时要把申报值调大(在显存允许范围内)。
vllm_ascend/ops/fused_moe/moe_comm_method.py · MoE 初始化与缓冲分配 · L270-275、L334-343 self.enable_fused_mc2 = get_ascend_config().enable_fused_mc2
if self.enable_fused_mc2 == 1 and is_mega_moe_supported(): # ⚡前置:融合 MC2 + 硬件支持
self.mega_moe_symm_buffer = None
self.get_symm_buffer_for_mega_moe, self.mega_moe = moe_utils.load_cann_mega_moe_ops() # →NPU
# P nodes and PD-mixed nodes use the configured value. ...
max_recv_token_num = get_ascend_config().mega_moe_max_tokens # ⚡申报值
# absolute_safe_max_recv_token_num is the max value required by mega moe api
if max_recv_token_num > absolute_safe_max_recv_token_num:
max_recv_token_num = absolute_safe_max_recv_token_num # ↳ 超上限被夹住
logger.warning_once(
"MegaMoe symm buffer: max_recv_token_num is set from "
"mega_moe_max_tokens=%d ... If the actual per-rank received token count after "
"dispatch exceeds this value, precision degradation will ...") # ⚡超发=精度风险
病:模型算完注意力后,最后要把隐藏态投到词表上取下一个 token(采样,sampling)。 TP(张量并行:一层矩阵切开多卡算)下每张卡只持有词表的一片(V_local),但上游采样流程里 存在所有卡都要做的重复处理;词表十几万列时,这块开销不小。
治法:enable_reduce_sample 让每张卡只在自己的分片上做 top-k
(k 取 min(top_k, V_local)),去掉重复计算。注意三点(都在校验代码里):
它是实验特性(启动会打 warning);与细粒度 TP 互斥;
PD 分离场景不支持。
vllm_ascend/sample/sampler.py · 采样分支 · L169-177 if get_ascend_config().enable_reduce_sample: # ⚡实验特性开关
tp_group = get_tp_group()
B, V_local = logits.shape # ↳ 本卡只持有词表分片 V_local
rank = tp_group.rank_in_group
if top_k is None or (p is None and k is None):
k_for_topk = V_local # ↳ 不限 k 时,分片全量即是上限
else:
k_for_topk = min(top_k, V_local) # ⚡去重:k 不会超过本分片宽度
读完 8 个参数再看全局,会发现一个漂亮的设计:vllm-ascend 没有写一个"万能调度器",而是把每种增强写成
可叠加的类,启动时按打开的参数组装出最终的调度器。
基座是 RecomputeScheduler(它留了给 DyntraLB 用的钩子),
DyntraLB 以 Mixin(混入类:把一组能力"掺"进别的类)方式叠加,还有异步版本组合;
balance 是独立的一支。理解这棵树,就能看懂日志里"我的请求到底被哪个调度器处理了"。
顺带认识四个核心数据对象(都已在代码里见过):
SchedulerOutput=每步"算什么"的清单(5.5 有其子类 RecomputeSchedulerOutput,
多带一个 RecomputeReqInfo 标记重算请求);
_Modification=一张卡的迁移清单(out_blk 迁出块 / in_blk 迁入块 /
freeze 冻结标志,DyntraLB 的产出物);
_JobStat=批量作业的 EWMA 记账本;三队列(immediate/short/long)=短请求优先的等待区。
参数互斥与前置校验都集中在 AscendConfig.derive_and_validate() 里,启动即报错(不会带病运行)。
必记四条:① profiling_chunk × balance 互斥(都重写调度主流程);
② 细粒度 TP → 必须开 recompute_scheduler(跨 DP HCCL 通信依赖重算调度,否则直接报错);
③ enable_reduce_sample 是实验特性:与细粒度 TP 互斥、PD 分离不支持;
④ mega_moe:前置 enable_fused_mc2=1 且硬件支持,且申报容量 < 实际单卡收到的 token 会精度劣化。
另外两个运行期纪律:balance 的 all_gather 任何 rank 不得缺席(缺席=死锁);dyntra 前 250 步(start_step)不搬移。
vllm_ascend/ascend_config.py · derive_and_validate · L807-820 if self.enable_reduce_sample:
logger.warning_once(
"enable_reduce_sample is an experimental feature. Use with caution.") # ⚡实验特性
if (...细粒度 TP 相关字段...):
raise ValueError(
"enable_reduce_sample is incompatible with "
... # ⚡红线一:与细粒度 TP 互斥
if (...PD 分离场景...):
...raise ValueError(
"enable_reduce_sample is not supported on PD-disaggregated "
"scenarios. Please disable enable_reduce_sample.") # ⚡红线二:PD 不支持
复述自检:合上页面,试着回答四个问题——①调度器每步产出的"清单"叫什么、受哪两本预算约束? ②balance 和 dyntra 的差别(数量 vs 块数)?③短请求优先怎么防长请求饿死? ④细粒度 TP 为什么必须开 recompute_scheduler?四个都答得上来,这一篇就学会了。
| 章节 | 层次 | 小节标题 | 图数量 | 图编号+视角 | 单图最大节点数 | 代码块数 | 是否达标 |
|---|---|---|---|---|---|---|---|
| ①干嘛的 | 概览 | 这段代码是干嘛的 | 1 | 图1-1 结构 | 4 | 0 | ✓ |
| ②位置 | 概览 | 它在系统中的位置 | 1 | 图2-1 结构 | 4 | 0 | ✓ |
| ③爬坡 | 概览 | 概念爬坡(4 级)+术语表 | 4 | 图3-1~3-4 问题/执行/数据视角(各单视角) | 5 | 1(B1) | ✓ |
| ④主干 | 概览 | 参数生效链 | 1 | 图4-1 执行 | 5 | 1(B1 计入③) | ✓ |
| ⑤展开 | 细节 | 8 个参数小节 | 8 | 图5-1~5-8 执行/结构/数据(各单视角) | 5 | 9(B2-B9,B10 在⑦) | ✓ |
| ⑥数据 | 细节 | 继承树与数据对象 | 1 | 图6-1 结构 | 6 | 0 | ✓ |
| ⑦边界 | 细节 | 互斥与前置 | 1 | 图7-1 结构 | 4 | 1(B10) | ✓ |
| ⑧小结 | 概览 | 一张图带走 | 1 | 图8-1 结构 | 4 | 0 | ✓ |
| 章节 | h3 小节标题 | 是否配图 | 图编号 | 备注 |
|---|---|---|---|---|
| ③ | 级1/级2/级3/级4(4 个小节) | ✓ | 3-1/3-2/3-3/3-4 | 术语表在爬坡后,标"回头查阅用" |
| ⑤ | 5.1~5.8(8 个小节) | ✓ | 5-1~5-8 | 每节一图一代码,无纯文字小节 |
| ⑥ | 继承树与数据对象 | ✓ | 6-1 | 结构视角 |
| ⑦ | 互斥与前置 | ✓ | 7-1 | 附 B10 校验代码 |
| ⑧ | 小结 + 复述自检 | ✓ | 8-1 | 附 4 道复述题 |
| 级别 | 引入的概念 | 上一级是什么 | 怎么从上一级推出 | 图编号 | 是否空降 |
|---|---|---|---|---|---|
| L0 | 通用编程/系统常识 | — | — | — | — |
| L1 | 逐 token 生成、前向计算 | 普通 Web 请求一次完成 | 回答是连续生成的,同一请求反复被计算 | 3-1 | 否 |
| L2 | 调度器/调度循环/SchedulerOutput | 请求都是"半成品" | 因为反复计算,所以每步都要重新决定算谁 | 3-2 | 否 |
| L3 | KV cache 块预算、token 上限、抢占 | 每步重新组 batch | 组 batch 需要依据,依据就是两本预算 | 3-3 | 否 |
| L4 | DP 忙闲不均/长堵短/chunk/MoE 容量等昇腾特有问题 | 通用调度器+两本预算 | 上游没管集群与昇腾特有约束,参数按问题对号入座 | 3-4 | 否 |