← 返回博客列表 Anthropic Fellow OA 复盘:GPU 调度 + ML Debug
Anthropic

Anthropic Fellow OA 复盘:GPU 调度 + ML Debug

2026-08-11

最近走了一遍 Anthropic Fellow 的在线测评,前后一共两个部分:OA1 是九十分钟的编程实现,分成五道递进的关卡;交卷后不到半小时,OA2 的 Debug 题就发到了邮箱。这篇复盘把两部分的形式、真实节奏和核心考点都记下来,如果你也在准备 Anthropic 这类偏系统与 ML 工程的 OA,希望这份笔记能帮你少走弯路。需要 OA辅助 或 OA代面 的同学,也可以直接照着后面的思路对齐节奏。

OA 整体流程概览

阶段 时长 形式 考点
OA1 90 分钟 五道独立关卡,每关一个 .py 文件,调用给定系统代码(系统代码不可修改) 系统实现、状态机建模、逐关加难
OA2 交卷后半小时内发放 Debug 题:修复给定代码里的 bug(ML 方向) 代码阅读、定位缺陷、保持向量化

两部分的味道完全不同:OA1 更像系统实现,你要在别人给好的接口上把调度逻辑搭起来;OA2 更像线上救火,考的是读代码和调试。下面分别拆开讲。


OA1:LLM 推理请求调度器(五关递进)

题目背景

OA1 的五道关卡围绕同一个主题:实现一个 LLM 推理请求调度器,风格接近 vLLM / SGLang。每一关是一个独立的 .py 文件,都要调用题目提供的「系统代码」里的函数,而系统代码本身不能改动。五关实现的东西各不相同、难度逐级递增,这里以最能体现调度核心的一关为例。

一个请求的生命周期是这样的:

  1. 请求先进入 waiting(等待)队列。
  2. 经过一次 Prefill 后变为 admitted(已接纳)。
  3. 之后逐 token Decode
  4. 生成 token 数达到 max_tokensfinished(完成)。

每个时间步 GPU 的算力有上限 max_work,一步内消耗不能超过它。调度顺序是硬性规定的:

思路

维护三组状态 waiting / admitted / finished。每一步从 remaining = max_work 开始:

  1. Decode 阶段:按到达顺序遍历 admitted,每个消耗 1 单元;generated 达到 max_tokens 的移入 finished,否则留在 admitted
  2. Prefill 阶段:从 waiting 队首取请求,只有 prompt_len <= remaining 才 Prefill 并移入 admitted;一旦队首装不下就 break
  3. 每一步返回本步调度的任务,并更新状态与已生成 token 数。

注意一个细节:本步刚被 Prefill 的请求,这一步不会 Decode——因为 Prefill 排在 Decode 之后,它要等到下一步才开始出 token。

Python 实现

先给出题目提供的「系统代码」示意接口(只读,不可修改):

from collections import deque
from typing import Dict, List


# ==== 系统代码(不可修改,仅示意接口) ====
class Request:
    def __init__(self, req_id: int, prompt_len: int, max_tokens: int):
        self.req_id = req_id
        self.prompt_len = prompt_len   # Prefill 需要的 GPU 单元数
        self.max_tokens = max_tokens   # 需要 Decode 出的 token 总数
        self.generated = 0             # 已生成的 token 数


def prefill(req: Request) -> int:
    """系统代码:完成 Prefill,返回消耗的 GPU 单元数。"""
    return req.prompt_len


def decode_one(req: Request) -> int:
    """系统代码:Decode 一个 token,返回消耗的 GPU 单元数(恒为 1)。"""
    req.generated += 1
    return 1

复杂度prefilldecode_one 均为 O(1)。

再是我们要实现的调度器:

class Scheduler:
    """LLM 推理调度器:先 Decode 全部 admitted,再用剩余算力 Prefill waiting。"""

    def __init__(self, max_work: int):
        self.max_work = max_work
        self.waiting: deque[Request] = deque()   # 尚未 Prefill
        self.admitted: deque[Request] = deque()  # 已 Prefill,正在 Decode
        self.finished: List[Request] = []

    def add(self, req: Request) -> None:
        self.waiting.append(req)

    def step(self) -> Dict[str, List[int]]:
        remaining = self.max_work
        decoded: List[int] = []
        prefilled: List[int] = []

        # 阶段一:Decode —— 按到达顺序服务所有 admitted,每个消耗 1 单元
        carry: deque[Request] = deque()
        while self.admitted:
            req = self.admitted.popleft()
            if remaining < 1:
                carry.append(req)              # 算力耗尽,下一步再 Decode
                continue
            remaining -= decode_one(req)       # 消耗 1 单元,generated += 1
            decoded.append(req.req_id)
            if req.generated >= req.max_tokens:
                self.finished.append(req)      # 达到上限,完成
            else:
                carry.append(req)
        self.admitted = carry

        # 阶段二:Prefill —— 从 waiting 队首开始,队首装不下就立刻停
        while self.waiting:
            head = self.waiting[0]
            if head.prompt_len > remaining:
                break                          # 队首放不下:不能跳过它去调度后面更小的请求
            self.waiting.popleft()
            remaining -= prefill(head)
            self.admitted.append(head)
            prefilled.append(head.req_id)

        return {"decode": decoded, "prefill": prefilled}

复杂度:单步 O(A + P),A 为当前 admitted 数量、P 为本步新 Prefill 的数量;空间 O(N),N 为在途请求总数。

Dry run 演示

max_work = 4,一开始加入三个请求(到达顺序 R0、R1、R2):

请求 prompt_len max_tokens
R0 2 2
R1 3 1
R2 1 1

逐步走一遍 step()

step 起始 remaining Decode Prefill 结束状态(waiting / admitted / finished)
1 4 R0(-2);R1 需 3 > 2,停 [R1,R2] / [R0] / []
2 4 R0(-1,generated=1) R1(-3);R2 需 1 > 0,停 [R2] / [R0,R1] / []
3 4 R0(generated=2,完成)、R1(generated=1,完成) R2(-1) [] / [R2] / [R0,R1]
4 4 R2(generated=1,完成) waiting 空 [] / [] / [R0,R1,R2]

重点看第 1 步:remaining = 2 时队首 R1 需要 3 装不下,即便后面的 R2 只需 1 也不能跳过 R1 去 Prefill R2——这正是题目最容易踩的坑。

拿分策略

五关逐级加难,稳妥的打法是先把靠前的基础关吃干净:状态机(waiting → admitted → finished)和「先 Decode 后 Prefill、队首装不下即停」这条主干一旦搭对,后面几关多半是在它上面叠约束(比如抢占、优先级、批大小限制)。别一上来死磕最后一关,把基础分锁死再往上冲。


OA2:Debug Extremely Randomized Trees

题目背景

交完 OA1 不到半小时,邮箱就收到了 OA2——一道 Debug 题,ML 方向。题目给了一份基本能跑但有 bug 的 Extremely Randomized Trees(Extra-Trees)实现,缺陷会导致崩溃或准确率异常。你需要阅读 trees.pytests/test_trees.py,从失败的测试出发定位并修复问题。测试和文档不能改;不要求做速度优化,但不能把 NumPy 向量化操作换成明显更慢的多层循环

思路

不要一上来通读全部代码,而是从报错和失败测试倒推

  1. 先跑完整测试套件,看哪些用例挂了、报什么错。
  2. 针对每个失败用例逐个收敛,定位到具体函数。
  3. 重点排查这几类地方:
    • 树的停止条件(深度 / 样本数 / 纯度)是否正确。
    • 随机特征与随机阈值的选取(Extra-Trees 的核心是阈值在特征取值范围内均匀随机)。
    • 左右划分的掩码边界(<<=、以及左右是否互补完备)。
    • 空节点 / 叶子的处理。
    • 预测聚合(分类多数投票、回归取均值)。
    • 测试期望的随机种子约定
  4. 保持原有接口和 NumPy 向量化;每修一处就重跑相关测试,最后再跑全套。

一个典型 bug 与修复

最常见的一类缺陷出在左右划分:右子集本应是左子集的补集,却被复制粘贴写成了同一个方向,导致右子树永远为空——要么递归停不下来,要么准确率崩掉。

# Before(bug):左右用了同一个比较方向,右子集恒为空
left_mask = X[:, feat] <= threshold
right_mask = X[:, feat] <= threshold      # 复制粘贴错误,应是补集
# After(fix):右子集取补集,保证划分互斥且完备,且保留向量化
left_mask = X[:, feat] <= threshold
right_mask = ~left_mask

另一类高频坑是随机特征采样的 off-by-onenp.random.randint(0, n_features - 1) 永远取不到最后一个特征,应写成 np.random.randint(0, n_features)(上界开区间)。

# Before(bug):最后一个特征永远不会被选中
feat = np.random.randint(0, n_features - 1)
# After(fix):randint 上界是开区间,用 n_features 才能覆盖全部特征
feat = np.random.randint(0, n_features)

说明:这类修复都是常数级改动,不改变算法整体复杂度——建树仍是 O(n · d · depth) 量级,向量化划分保持 O(n),切忌为了「看起来对」而退化成逐样本的多层循环。


备考建议


FAQ

Q1:OA1 的五关一定要全做完吗?

不一定。五关递进加难,评分是累积的。稳妥策略是先把靠前的基础关做干净、拿满分,再往难关冲;把主干(状态机 + 调度顺序)搭对,后面几关往往是在它上面加约束。

Q2:调度里为什么队首装不下就要停,而不是跳过去调度更小的请求?

这是题目明确规定的调度语义:Prefill 严格按到达顺序,waiting 队首放不下就立刻停止本轮,保证先到先服务、避免大请求被无限延后。跳过队首去 Prefill 后面更小的请求会违反题意,直接判错。

Q3:本步刚 Prefill 的请求,这一步会 Decode 吗?

不会。每步是「先 Decode 全部 admitted,再 Prefill waiting」。请求是在 Decode 阶段之后才被 Prefill 并加入 admitted 的,所以它要等到下一步才开始逐 token Decode。

Q4:OA2 的 Debug 题,应该先通读全部代码吗?

不建议。更高效的做法是先跑完整测试套件,从失败用例和报错信息倒推,锁定具体函数再看局部代码。重点查停止条件、随机特征 / 阈值选取、左右划分掩码、空节点处理和预测聚合。

Q5:OA2 说不能退化向量化,具体指什么?

题目允许你不追求极致性能,但禁止把 NumPy 的向量化操作(如布尔掩码划分、批量比较)改写成明显更慢的逐样本多层循环。修复 bug 时保留原有的向量化写法,改动控制在常数级即可。


正在准备 Anthropic Fellow 这类偏系统实现 + ML 工程的 OA? 我们熟悉 vLLM / SGLang 风格的推理调度题和 Extra-Trees 这类 Debug 题的考法,能陪你把状态机建模、调度语义和调试节奏一次打磨到位,提供全程 OA辅助 与 OA代面 支持。

立即添加微信 Coding0201获取一对一定制备考方案

联系方式