最近走了一遍 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 文件,都要调用题目提供的「系统代码」里的函数,而系统代码本身不能改动。五关实现的东西各不相同、难度逐级递增,这里以最能体现调度核心的一关为例。
一个请求的生命周期是这样的:
- 请求先进入
waiting(等待)队列。 - 经过一次 Prefill 后变为
admitted(已接纳)。 - 之后逐 token Decode。
- 生成 token 数达到
max_tokens时finished(完成)。
每个时间步 GPU 的算力有上限 max_work,一步内消耗不能超过它。调度顺序是硬性规定的:
- 先 按到达顺序为所有
admitted请求做 Decode,每个消耗 1 单元。 - 再 用剩余算力,按到达顺序为
waiting请求做 Prefill,每个消耗prompt_len。 - 关键约束:如果
waiting队首的请求装不下,就立刻停止这一轮的 Prefill——不能跳过它去调度后面更小的请求。
思路
维护三组状态 waiting / admitted / finished。每一步从 remaining = max_work 开始:
- Decode 阶段:按到达顺序遍历
admitted,每个消耗 1 单元;generated达到max_tokens的移入finished,否则留在admitted。 - Prefill 阶段:从
waiting队首取请求,只有prompt_len <= remaining才 Prefill 并移入admitted;一旦队首装不下就break。 - 每一步返回本步调度的任务,并更新状态与已生成 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
复杂度:prefill 与 decode_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.py 和 tests/test_trees.py,从失败的测试出发定位并修复问题。测试和文档不能改;不要求做速度优化,但不能把 NumPy 向量化操作换成明显更慢的多层循环。
思路
不要一上来通读全部代码,而是从报错和失败测试倒推:
- 先跑完整测试套件,看哪些用例挂了、报什么错。
- 针对每个失败用例逐个收敛,定位到具体函数。
- 重点排查这几类地方:
- 树的停止条件(深度 / 样本数 / 纯度)是否正确。
- 随机特征与随机阈值的选取(Extra-Trees 的核心是阈值在特征取值范围内均匀随机)。
- 左右划分的掩码边界(
<与<=、以及左右是否互补完备)。 - 空节点 / 叶子的处理。
- 预测聚合(分类多数投票、回归取均值)。
- 测试期望的随机种子约定。
- 保持原有接口和 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-one:np.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),切忌为了「看起来对」而退化成逐样本的多层循环。
备考建议
- OA1 先锁基础分:把请求状态机和「Decode 优先、Prefill 队首不可跳过」这条主干打牢,靠前的关卡稳拿,再向难关叠加约束。
- OA2 从报错出发:先跑全套测试,用失败用例定位,别通读全代码;改一处、跑一次,逐步收敛。
- 守住既有约束:系统代码不可改、测试不可改、向量化不可退化——这些是隐性评分点,踩线会直接掉分。
- 两部分节奏衔接:OA1 交卷后半小时内 OA2 就会来,中途别彻底放松,留好精力切换到「读代码 + 调试」的状态。
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,获取一对一定制备考方案。
联系方式
- 微信:Coding0201
- Email:[email protected]
- Telegram:@OAVOProxy