1. 先把话说清楚:SOQ 到底在解决什么问题
“量子计算服务化”这个词这两年被提得越来越多,但真到落地的时候,很多人的第一反应还是“不就是把一段电路提交到云上跑一下吗”。我一开始也这么以为,直到把一个内部用的小工具从“脚本直连硬件”改成一套带调度、编译、校准感知和误差治理的服务,才发现中间隔着的东西比想象中多得多。
SOQ,Service-Oriented Quantum,直译是“面向服务的量子计算”,本质是把量子处理器这种稀缺、昂贵、脾气还不稳定的算力,包装成一组能被普通业务系统调用的能力接口。调用方不需要知道今天哪台机器校准得好、不需要自己算物理比特映射、不需要为一次失败的作业重写整份实验脚本,他只需要说清楚三件事:我要解什么问题、我能接受多长的等待、我能容忍多大的误差。
这件事的价值在于角色分离。算法研究员关心线路深度和拟设结构,业务开发关心“调用一次多少钱、多久出结果”,平台运维关心“机器有没有被跑满、队列有没有堆积”。这三类人如果共用一套脚本,最后一定是互相拖累——研究员改一行参数把线上任务打挂,运维为了稳定把所有实验参数锁死。SOQ 做的事情,就是把这三类诉求切成三层,各自演进。
适合读这篇内容的人大概是这么几类:手上有量子硬件或云上量子后端访问权限、想把它接进自己业务系统的开发者;正在做混合量子-经典算法、被工程问题折磨的研究人员;以及需要在团队里搭一套“量子算力中台”的架构同学。我会把架构分层、关键组件、一套能跑起来的最小实现、以及踩过的坑都摊开讲。
2. 架构怎么切:SOQ 的分层与形态选型
2.1 从“提交电路”到“调用能力”,抽象层级变了什么
最原始的用法是:本地写一段线路,序列化成中间表示,提交给后端,轮询状态,拿回测量计数,自己解析。这条路径短、直接、调试方便,但它把所有工程责任都推给了调用方。
服务化之后,抽象往上抬了一层。调用方描述的是“我要在一个 12 节点的图上找一个接近最优的划分”,服务端负责把它翻译成量子线路或量子-经典混合流程、选一台当前状态合适的机器、做线路映射与优化、执行、做读出校正、把结果整理成业务能直接消费的结构。这个转换的核心难点在于:量子硬件的物理约束是动态变化的,同一段线路在不同时间、不同机器上跑,结果质量可能差出一个数量级。把这个不确定性封装掉,才是服务化的真正工作量所在。
我个人的判断标准是:如果你只是偶尔跑个 demo,直连完全够用;一旦出现“同一个任务要重复跑几十次取统计”“多个团队抢同一批机器”“结果需要给出可信度评估”这三类需求中的任意一个,就该考虑上服务层了。
2.2 一份可落地的六层参考架构
我把实际项目中反复出现的模块整理成六层,横向再挂两个支撑面。这套切法不是教科书标准,是从“出问题的时候能快速定位到哪一层”这个角度反推出来的。
| 层级 | 主要职责 | 常见实现方式 | 出问题时的典型症状 |
|---|---|---|---|
| 接入层 | 鉴权、配额、限流、请求校验 | 网关 + 令牌桶 | 429 频发、单租户打满全局队列 |
| 能力层 | 把业务语义翻译成量子任务 | 任务模板 + 参数校验 | 参数越界、模板与硬件能力不匹配 |
| 编排层 | 混合流程控制、多轮迭代、超时回收 | 有状态工作流引擎 | 迭代不收敛、任务悬挂 |
| 编译层 | 逻辑到物理映射、门分解、线路优化 | 转译器 + 路由算法 | 深度爆炸、SWAP 数量异常 |
| 执行层 | 作业排队、批处理、结果聚合 | 队列 + 执行器池 | 排队时间抖动、结果错配 |
| 硬件层 | QPU 及其控制系统 | 厂商后端 | 校准漂移、连通性变更 |
两个横向支撑面分别是校准与元数据面(谁在什么时候、处于什么状态)和可观测面(指标、日志、追踪)。很多团队一开始不建这两个面,等到线上结果开始“莫名其妙变差”时才发现无从下手——因为没有任何历史校准数据可以对照。
注意:校准元数据一定要带时间戳存下来,而且要存原始值而不是只存“好/坏”的标签。事后复盘时,往往就是靠某一天的读出错误率突然跳变,才定位到问题。
2.3 三种服务形态,别选错
同样是“服务化”,形态差别很大。我按响应模式和适用场景分成三类:
| 形态 | 交互模式 | 典型延迟 | 适合场景 | 主要代价 |
|---|---|---|---|---|
| 直通型 | 提交即返回作业 ID,异步轮询 | 秒级到小时级 | 单次实验、批量采样 | 调用方要自己处理状态机 |
| 会话型 | 建立会话,多次提交共享硬件上下文 | 会话内毫秒到秒级 | 变分算法迭代、需要保持校准一致 | 会话独占资源,成本高 |
| 能力型 | 同步调用,返回业务结果 | 取决于内部分解 | 业务系统嵌入、优化求解 | 封装成本高,灵活性下降 |
选型的核心是迭代次数。如果你的算法需要几百上千次“跑一次、改参数、再跑一次”的循环,直通型的排队开销会吃掉绝大部分时间,这时候会话型的价值就体现出来了——它让连续多次提交落在同一台机器、同一段校准窗口内,避免每轮都重新适应硬件状态。反过来,如果只是每天跑一次报表类的采样任务,能力型接口最省心。
我见过一个反例:某团队把变分算法硬塞进直通型接口,每轮迭代排队平均 40 秒,200 轮就是两个多小时,其中真正算的时间不到 3 分钟。换成会话型之后整体压到十分钟以内。这个差距不是算法优化能补回来的。
3. 核心组件拆解:调度、编译、校准、误差治理
3.1 作业调度器:队列设计比算法更重要
调度器要同时满足三类诉求:公平(多租户不被互相饿死)、效率(机器不空转)、可预期(用户知道大概等多久)。这三者天然冲突,实际工程里通常用“分级队列 + 预留窗口”的组合。
分级队列的做法是给每个租户分配权重,用加权公平队列做排队,同时给短作业(预估执行时间低于阈值)单独开一条快车道。预估执行时间这个字段非常关键,它决定了作业能否进快车道。我的经验是按线路深度和 shots 数量做线性估算,再乘一个安全系数:
预估时长 ≈ (深度 × 单层执行开销 + shots × 单次读出开销) × 安全系数安全系数取 1.5 到 2.0 比较稳,因为深线路在真实硬件上的执行时间往往不是线性的——中途的动态电路、中测量、重置操作都会拉长实际耗时。宁可估高一点,让快车道判定保守,也不要估低了导致快车道被长作业堵死。
预留窗口是给“必须在某个时间点出结果”的用户准备的,比如某次对外演示、某个跨团队联合实验。实现上就是在队列里插入一个带时间范围的保留槽,在这段时间内该资源对其他租户不可见。这里有个坑:预留窗口一旦建立,就一定要有超时释放机制,否则运维半夜被叫起来手动清理死锁的槽位是常态。
3.2 编译中间层:SWAP 数量是隐形的成本开关
逻辑线路到物理线路的转换,核心工作是把双比特门作用在硬件不直接相连的比特对上。做法是插入 SWAP 把量子态搬过去,代价是每个 SWAP 由三个 CNOT 组成,而且会额外增加深度。
举个具体的例子。假设一个任务有 30 个双比特门,硬件连通度中等,初始映射选得不好时,路由阶段可能插入 20 个以上 SWAP——也就是额外 60 多个 CNOT。如果双比特门的平均错误率是 5e-3,累计成功率大致按 (1-ε)^n 估:
0.995^60 ≈ 0.74看起来还能接受?但别忘了原本那 30 个门也有成本,加起来的门总数从 30 涨到 90 以上,成功率掉到 0.64 左右。而一个更好的初始映射可能只需要 8 个 SWAP,总门数控制在 60 以内,成功率回到 0.74 附近。同一段逻辑线路,映射策略不同,结果质量能差十几个百分点,这就是为什么编译层必须做成服务的一部分而不是让调用方自己调。
编译层还有几个容易忽略的点。门分解的目标门集必须跟随后端能力动态更新,有些后端支持直接的原生双比特门,有些需要分解成更基础的序列,分解方式不同,深度差得很多。线路优化里的门消去、门融合要谨慎用,因为某些优化在理想情况下等价,在有噪声的硬件上未必等价——特别是涉及测量和重置的线路,优化器如果把测量顺序改了,结果可能完全变样。
注意:转译优化等级不要无脑开最高。高等级优化会显著拉长编译时间,而收益在深线路上不一定明显。我的做法是把优化等级做成任务参数,默认中等,允许调用方按需提高。
3.3 校准数据服务:让“选哪台机器”变成一次查询
多后端环境下,选机器是每天都要面对的问题。手工挑选不可持续,必须有一个校准数据服务,定期采集每个后端的指标,给调度器提供打分依据。
采集的指标至少包括:单比特门错误率、双比特门错误率、读出错误率、T1/T2 相干时间、连通图、队列深度。采集频率上,错误率类指标每天至少一次,队列深度这类动态指标要实时或准实时。存储上按时间序列存,方便后续做趋势分析和“某天异常”的归因。
有了数据,选机就可以抽象成一个打分函数:
score = w1 × (1 − 读出错误率) + w2 × (1 − 双比特门错误率) + w3 × 连通度匹配度 − w4 × 归一化排队时长 − w5 × 近期任务失败率权重怎么定?取决于任务类型。浅层、门数多的任务对双比特门错误率更敏感,w2 该调大;需要大量采样的任务对读出错误率敏感,w1 权重上去。连通度匹配度可以粗略定义为“线路中需要跨比特交互的门,有多大比例在硬件上是直接相邻的”,这个值可以在编译前用逻辑线路的交互图跟硬件的耦合图做一次匹配估算,成本很低。
这套打分不能只算一次就完事。我的做法是在任务提交时算一次做初选,在真正执行前再算一次做复核——因为排队可能等很久,等到执行时硬件状态已经变了。如果复核发现当前机器状态明显劣化,允许一次“改派”,但只允许一次,避免反复改派导致任务永远排不上。
3.4 误差缓解与结果可信度:不给结论打分的服务是不合格的
现阶段的硬件还处在“有噪声、需要缓解”的阶段,服务层如果只返回原始计数,等于把最难的判断推给了调用方。真正有用的输出应该包含三部分:结果、误差估计、以及这个结果能不能信的判断。
误差缓解常用的几类手段,我按工程成本排个序。读出误差校正性价比最高,只需要一个校准矩阵,在结果后处理阶段做一次矩阵求逆或贝叶斯展开,成本几乎为零,效果在读出错误率较高的机器上非常明显。零噪声外推成本中等,需要把同一线路在不同噪声强度下跑多次再外推,适合期望值估计类任务,但对采样类任务不适用。概率错误消除理论最干净,但需要把每个门替换成门的随机采样组合,采样开销随门数指数增长,目前基本只在小规模验证里用。
可信度评估这块,我总结了三条实用的判断线。第一,跟经典可验证的小实例对照:如果你的问题在 8 到 12 个比特的规模下经典方法还能算出精确解,那就必须做这个对照,结果偏差超出预期就说明服务链路有问题。第二,多次运行的一致性:同一配置跑 5 次,结果分布的波动应该与理论统计误差同量级,明显超出就说明硬件状态不稳或有系统性偏差。第三,理论值边界检查:很多问题有已知的上下界(比如组合优化问题的最优值下界),如果服务返回的结果突破了这些界,直接判定无效。
关于统计误差的量级,这里给一个能直接用的估算。要分辨两个概率值之间 Δp 的差异,采样次数大致需要满足:
N ≳ (z × 0.5 / Δp)²其中 z 是置信倍数,取 3 对应约 99.7% 的置信水平。代入 Δp = 0.02,得到 N ≈ (3×0.5/0.02)² = 5625,再考虑实际线路有噪声、分布不是理想均匀,通常要上浮三到四倍,也就是两万次 shots 起步。很多“结果看起来有优势”的结论,最后被证明只是 shots 不够导致的统计涨落——这类问题我在评审里见过不止一次。
4. 实操:搭一套最小可用的 SOQ 服务
4.1 环境与依赖准备
最小实现不需要多复杂的栈。我的建议是:一个轻量 Web 服务做接入和作业管理,一个消息队列或后台任务池做异步执行,一个时间序列存储放校准数据,外加一个对象存储放原始结果。语言选团队最熟的那个,别为了“量子”两个字去换栈。
目录结构大致这样:
soq/ ├── api/ # 接入层:路由、鉴权、参数校验 ├── capability/ # 能力层:任务模板、语义翻译 ├── compiler/ # 编译层:映射、路由、优化 ├── executor/ # 执行层:作业状态机、后端适配 ├── calibration/ # 校准面:采集、打分、选机 └── observability/ # 指标、日志、追踪后端适配器建议统一抽象成一个接口,把不同厂商的 SDK 差异全部关在这一层里。接口方法不用多,五个就够:能力查询、提交、状态查询、结果获取、取消。这样上层代码跟具体硬件完全解耦,换机器只改适配器。
4.2 服务端:作业状态机是稳定性的地基
作业状态机要设计得足够“死板”,状态迁移路径固定,非法迁移直接拒绝。我用的是这六个状态:
| 状态 | 含义 | 允许迁往 |
|---|---|---|
| SUBMITTED | 已接收,待校验 | VALIDATED / REJECTED |
| VALIDATED | 校验通过,待编译 | COMPILED / FAILED |
| COMPILED | 已编译,待执行 | RUNNING / FAILED |
| RUNNING | 执行中 | DONE / FAILED / CANCELLED |
| DONE | 完成 | 终态 |
| FAILED / REJECTED / CANCELLED | 异常或取消 | 终态 |
伪代码层面长这样,路由注册方式按你手头的框架替换:
# 提交作业:先落库再返回,保证幂等 @app.post("/v1/jobs") def submit_job(req): token = req.headers.get("Idempotency-Key") if token and store.exists(token): return store.get(token) # 重复提交直接返回原作业 job = { "id": gen_id(), "state": "SUBMITTED", "payload": req.json, "created_at": now(), } store.save(job, idem_key=token) queue.push(job.id) return {"job_id": job.id, "state": job.state}状态迁移集中在一个函数里做,别散落在各处:
ALLOWED = { "SUBMITTED": {"VALIDATED", "REJECTED"}, "VALIDATED": {"COMPILED", "FAILED"}, "COMPILED": {"RUNNING", "FAILED"}, "RUNNING": {"DONE", "FAILED", "CANCELLED"}, } def transition(job_id, to_state, reason=None): job = store.get(job_id) if to_state not in ALLOWED.get(job["state"], set()): raise IllegalTransition(job["state"], to_state) job["state"] = to_state job["reason"] = reason job["updated_at"] = now() store.save(job) metrics.inc(f"job.state.{to_state}")这套看起来啰嗦,但线上出问题时它能救命。我遇到过执行器重启导致作业状态卡在 RUNNING 的情况,因为状态迁移有校验、有超时扫描,清理任务能把超时的 RUNNING 标记为 FAILED 并释放资源,不至于整条队列堵死。
超时扫描这个机制一定要有,而且要分阶段设置不同超时:编译阶段超时短一些(比如 5 分钟),执行阶段按预估时长乘 3 倍设置。超过就判失败并回收。没有超时的队列系统,早晚会因为一个畸形任务全线停摆。
4.3 编译与后端选择:把打分和映射串成一条链
完整链路是这样:解析任务 → 生成逻辑线路 → 查校准数据给后端打分 → 初选后端 → 做映射与路由 → 估算代价 → 如果代价超阈值且存在备选后端,换一个重试 → 定稿编译结果 → 入执行队列。
代价估算可以用一个粗略的复合指标:
cost = 深度 × 0.4 + 双比特门数 × 0.6 + shots × 1e-4系数是经验值,0.6 给双比特门是因为它的错误率通常比单比特门高一个量级,权重该更高。这个 cost 不追求精确,追求的是“在几个候选后端之间排出正确顺序”。
映射阶段值得多花点心思。如果硬件耦合图的直径不大,可以先做一次图同构近似匹配,把逻辑线路中交互最频繁的比特对优先映射到物理上相邻的位置。朴素的贪心实现大概几十行代码,效果比默认顺序映射好很多。我给一个简化思路:
def greedy_initial_layout(logical_edges, coupling_graph): degree = Counter() for a, b in logical_edges: degree[a] += 1 degree[b] += 1 # 逻辑上最"热"的比特,优先放到物理度数最高的节点 logical_order = [q for q, _ in degree.most_common()] physical_order = sorted(coupling_graph.nodes, key=lambda n: -coupling_graph.degree(n)) return dict(zip(logical_order, physical_order))这个启发式的道理很直白:交互最多的逻辑比特放在连长最多的物理比特上,后续路由要搬家的次数自然就少。实测在中等规模线路上能减少两三成的 SWAP 数量,成本几乎为零,属于典型的“改了不亏”。
4.4 客户端:幂等、重试、退避,一个都不能少
客户端这边最容易出问题的不是调用本身,而是重试。网络抖动导致响应丢失,客户端一重试,服务端收到两次提交,跑了两遍,计费两遍,结果还对不上——这种事故我见过不止一次。
解决办法是幂等键。每次逻辑提交生成一个唯一键,重试时带同一个键,服务端据此去重。键的生成要跟业务语义绑定,比如“任务模板 ID + 参数哈希 + 批次号”,这样即使客户端进程重启,重新构造出来的键也能对上。
def submit_with_retry(client, payload, idem_key, max_retry=5): backoff = 1.0 for attempt in range(max_retry): try: resp = client.post("/v1/jobs", json=payload, headers={"Idempotency-Key": idem_key}, timeout=(5, 30)) if resp.status_code in (200, 201, 202): return resp.json() if resp.status_code == 429: # 限流,退避后重试 raise RateLimited() if 400 <= resp.status_code < 500: # 客户端错误,重试无意义 raise ClientError(resp.text) except (Timeout, ConnectionError, RateLimited): if attempt == max_retry - 1: raise sleep(backoff + random.uniform(0, 0.5)) # 加抖动,避免重试风暴 backoff = min(backoff * 2, 30)两个细节值得强调。一是退避要加随机抖动,否则一批客户端在同一时刻失败、同一时刻重试,会把刚恢复的服务再打挂一次。二是4xx 不要重试,参数错误重试一万次还是错,只会浪费配额。
轮询状态时也一样,别用固定间隔死循环。用带抖动的指数退避,起始间隔 1 秒,上限 30 秒,再配合服务端返回的建议轮询间隔字段,整体负载能降一个量级。
4.5 混合流水线:把经典优化器包在量子求解器外面
大多数实际任务不是“跑一次量子线路”,而是“经典优化器反复调用量子求解器”。服务层要把这个循环封装好,对外暴露成一个任务。
以变分类算法为例,外层是经典优化器,内层是量子线路执行。关键工程点有三个:参数初始化、收敛判据、以及最重要的迭代预算控制。
def run_variational(task, backend, budget): params = init_params(task.n_params) # 小角度随机初始化,别全零 best = {"energy": float("inf"), "params": None} used = 0 while used < budget.max_iters: energy = backend.execute(task.ansatz, params, shots=budget.shots) used += 1 if energy < best["energy"]: best = {"energy": energy, "params": params} grad = finite_difference_grad(task, params, backend, budget.shots) params = params - budget.lr * grad if norm(grad) < budget.tol: break if used % 10 == 0 and energy_history_stagnant(used, window=10): params = perturb(params, scale=0.1) # 跳出平台期 return best参数初始化别全零,全零会让某些拟设的梯度直接消失,优化器原地不动。收敛判据不能只看梯度范数,噪声会让梯度一直有残值,得配合“连续若干轮无改善”一起判断。迭代预算要卡死,因为在有噪声的硬件上,迭代次数多了之后优化器基本是在拟合噪声,越跑越差是常态。
注意:有限差分求梯度需要的线路执行次数是参数个数的两倍,参数一多,预算消耗非常快。如果硬件支持参数化线路的批量执行(同一结构、不同参数一次提交),优先用它,能省掉大量往返开销。
5. 行业场景:哪些问题值得用量子服务
5.1 组合优化:价值在“可行解质量”,不在“最优解”
组合优化是当前落地讨论最多的方向,因为它的问题建模直观、跟硬件能表达的伊辛模型形式契合度高。典型场景包括配送路径规划、生产排产、资源分配、投资组合的离散化选择。
但要泼一盆冷水:当前阶段,这些问题上的量子求解器在绝大多数公开基准上还打不过成熟的经典启发式。所以服务化设计的重点不该是“我要跑赢经典”,而是“我要在特定约束结构下提供有价值的可行解”。有些问题带大量硬约束(比如时间窗、容量限制),经典求解器处理起来要么慢要么需要大量松弛,而量子退火或变分方法在编码约束上比较自然——这是可以切入的角度。
工程上的做法通常是双轨:经典求解器给一个基准解,量子服务在它附近做局部搜索或多样性补充,最后取优。这样即使量子部分没赢,整体也不会输,服务的价值体现在“多了一个候选来源”。
5.2 量子化学与材料模拟:最贴近原生优势的方向
分子电子结构计算是我认为最贴合量子计算原生表达能力的场景。分子哈密顿量天然可以用费米子算符表示,映射到量子比特后有成熟的构造方法,变分求解基态能量的流程也相对标准化。
现实约束在哪?比特数和线路深度。一个中等大小的分子做完整基态计算,需要的比特数和线路深度都远超当前硬件能力,必须做活性空间截断、基组简化、对称性利用等一系列约化。这些约化手段正是服务层该封装的东西——让化学背景的使用者只需要提供分子结构和精度要求,服务层负责选基组、做约化、构造拟设、选机器、跑迭代、给误差棒。
这个场景对结果可信度的要求最高。化学计算的结论会直接影响后续的实验设计,误差估计不充分等于误导。我的做法是强制要求输出三样东西:能量值及其统计误差、与经典近似方法(如密度泛函类方法)的对照值、以及“当前硬件条件下这个精度是否可靠”的明确标注。
5.3 采样与蒙特卡洛:短期最现实的落脚点
相比优化和模拟,采样类任务对“超越经典”的要求没那么苛刻,因为它要的是分布质量而不是精确最优值。金融风险场景里的路径采样、物理过程模拟里的构型采样、概率图模型里的边缘分布估计,都属于这一类。
这类任务的服务化设计有个鲜明特点:输出是分布,不是单点。所以接口设计上不能只返回一个数值,要返回分布摘要、矩统计、以及关键的收敛诊断信息。调用方拿到分布后自己做下游决策,服务层不越界。
采样任务的成本模型也跟别的不一样。它主要花在 shots 上,而 shots 跟统计精度是平方反比关系,所以要给调用方一个明确的“精度-成本”换算表,让他自己权衡。比如“要 1% 精度约需 N 次采样,成本 X;要 0.5% 精度约需 4N 次采样,成本 4X”,这种透明化的定价方式比打包一个固定规格要合理得多。
5.4 什么时候不该用量子服务
这部分我特别想说,因为见过太多硬上的项目。
规模已经能被经典方法轻松拿下的问题,别上。一个 20 个节点以内的路径规划,经典求解器秒出精确解,用量子服务纯属给自己加复杂度。
对延迟极度敏感、要求毫秒级响应的在线链路,别上。当前的排队和执行延迟决定了它只能做异步任务,塞进实时链路只会变成系统里最脆弱的一环。
结果无法验证、又要求高可靠性的场景,别上。如果一个问题既没有经典方法可以做交叉验证,也没有理论边界可以卡,那量子服务返回什么都无法判断对错,这种不确定性不该被引入生产决策。
精度要求超出当前硬件能力两三个数量级以上的,别上。老老实实等硬件进步,或者先用经典方法顶着,把资源投在别处。
6. 常见问题与排查实录
6.1 问题速查表
| 现象 | 常见原因 | 排查手段 | 处理方式 |
|---|---|---|---|
| 结果分布明显偏向某一个比特串 | 读出偏置、线路映射错误 | 跑一个全零输入的基准线路 | 启用读出校正,复核映射表 |
| 同一配置多次运行差异极大 | 硬件状态漂移、shots 不足 | 对比前后两次的校准数据 | 加 shots,缩短短路重跑 |
| 编译后深度暴涨 | 初始映射差、SWAP 过多 | 打印映射前后的门数和深度 | 换映射策略,或拆分线路 |
| 任务长期卡在 RUNNING | 执行器崩溃、超时扫描缺失 | 查执行器日志与心跳 | 补超时回收,加心跳上报 |
| 幂等失效导致重复执行 | 幂等键含时间戳或随机数 | 检查键的构造逻辑 | 键改为业务语义哈希 |
| 排队时间波动剧烈 | 快车道判定失准 | 对比预估时长与实际时长 | 调安全系数,重设阈值 |
| 期望值估计偏差随规模增大 | 噪声主导,未做缓解 | 对比不同 shots 下的估计值 | 上零噪声外推,降线路深度 |
6.2 几个不太容易想到的坑
坑一:shots 不是越多越好。直觉上多采样总没错,但在有噪声的硬件上,长时间连续采样会跨越硬件状态变化的窗口,前面的样本和后面的样本其实来自不同的噪声条件,混在一起反而引入系统性偏差。我的经验是单次作业的 shots 有个上限,超过就拆成多个作业分批次跑,每批次之间插入短暂间隔,最后再做统计合并。合并时要注意记录每个批次的时间戳,方便事后归因。
坑二:校准数据的新鲜度比精度更重要。一开始我执着于把校准指标测得特别准,花了大量时间做重复测量取均值。后来发现,硬件状态可能几个小时就变了,一份“很准但很旧”的校准数据,比一份“粗糙但很新”的数据更误导人。现在我的策略是高频粗测、低频精测,选机打分用高频那份,容量规划用低频那份。
坑三:线路优化可能悄悄改变语义。有一次某个任务的返回结果跟预期完全对不上,查了两天,最后发现是优化器把一个带条件操作的线路做了等价变换,在无噪声模型下等价,在真实硬件上因为测量时机变化导致后续操作作用在了错误的态上。教训是:涉及测量、重置、条件分支的动态线路,关掉激进优化,宁可在深度上吃点亏。
坑四:多后端的结果不能直接混用。同一个任务在不同后端上跑出来的结果,因为噪声特性不同,统计分布会有系统性差异。如果要做多后端交叉验证,必须把每个后端的结果单独统计、单独给误差棒,然后做一致性检验,而不是把计数直接加在一起。混在一起算出来的“平均值”没有物理意义。
坑五:定价模型要跟成本结构对齐。早期我们按“任务数”计费,结果用户都提交超小任务刷次数,队列被打满。改成按“shots 数 + 编译复杂度”计费之后,行为立刻正常了。这个坑的本质是:计费维度决定了用户行为,而用户行为决定了系统负载,设计计费模型其实是在设计流量模型。
最后分享一个我在实际搭这套东西时的体会:服务化最难的部分从来不是量子那部分,而是工程那部分。量子线路的构造、转译、执行,都有成熟的库和文档可以参考;真正消耗时间的是状态机设计、幂等处理、超时回收、校准数据的存储与查询、以及把“我不知道这台机器今天状态如何”这种不确定性,变成“系统知道,调用方不用知道”的确定性。这套工程骨架搭好之后,换硬件、换算法、换场景,上层几乎不用大改——这也是我觉得值得在架构上多花功夫的原因。