prepared specialization projection mismatch
主线位置:traced 主线的执行一步,「
prepare一次、run多次」。准备缓存的键、特化要求和run的五步见 执行与缓存;符号形状的叶子见 TracedTensor。
flowchart TB
prog["符号形状 matmul"] --> prep["prepare 2x3 3x4"]
prep --> pr["Prepared"]
pr --> same["run 同样维度"]
same --> ok["结果"]
pr --> other["run 5x3 3x4"]
other --> proj{"投影等于准备时"}
proj -->|否| err["projection mismatch"]
other --> again["重新 prepare"]
again --> ok
实验 crate exp-tenferro8/pit2-exp,bin p22_prepared,CPU(CpuEngine::serial()),debug,M2 Max、macOS 27、rustc 1.99.0。
现象
两个符号形状的叶子,编译一次 a.matmul(&b),用 2×3 和 3×4 的输入准备一次,然后拿同一个 Prepared 跑别的维度:
let ma = TracedTensor::from_tensor_symbolic_shape(ones(2, 3)?)?;
let mb = TracedTensor::from_tensor_symbolic_shape(ones(3, 4)?)?;
let program = GraphCompiler::new().compile(&ma.matmul(&mb)?)?;
let prepared = rt.prepare(&program, &reads_2x3_3x4, &[engine.target()])?;
for (m, k, n) in [(5, 3, 4), (2, 7, 4), (2, 3, 9)] {
rt.run(&mut [&mut engine], &prepared, &reads_mkn) // 打印结果或错误
}
prepared and run on 2x3 * 3x4: shape Output::Owned { dtype: F64, shape: [2, 4] }
same Prepared, run 5x3 * 3x4: Err: Runtime::run (Execution): runtime state failure: provider contract violation: prepared specialization projection mismatch
RuntimeStateSource { op: "Runtime::run", phase: Execution, source: SharedPrepareError(ProviderContract { source: ProjectionMismatch { expected: SpecializationProjection { ... concrete_dimensions: [(0, 2), (1, 3)], ... }, actual: SpecializationProjection { ... concrete_dimensions: [(0, 5), (1, 3)], ... } } }) }
same Prepared, run 2x7 * 7x4: Err: Runtime::run (Execution): runtime state failure: provider contract violation: prepared specialization projection mismatch
same Prepared, run 2x3 * 3x9: Err: Runtime::run (Execution): runtime state failure: provider contract violation: prepared specialization projection mismatch
(Debug 输出每次约 2000 个字符,只留了不同的那一处。)
- 程序是符号形状的,编译时接受任何维度;可
Prepared换任何一维都被拒绝。 - 「provider contract violation」读起来像是某个提供者违反了约定、库有 bug。实际原因只在 Debug 输出深处:第 0 个输入的第 0 维准备时是 2,现在是 5。
对比:同一个 Runtime 上,逐元素加规约的符号程序 sum(x * x) 为 3 个元素准备,拿去跑 1000 个元素没问题:
sum(x*x) prepared for n=3, run n=1000: [1000.0]
出处
run 在执行前调 validate_execution_signature,把这次的输入签名按准备时记下的特化要求再投影一次,和准备时的投影比:
let actual = prepared.specialization.requirements().project(signature)?;
if actual != prepared.specialization {
return Err(Arc::new(PrepareError::ProviderContract {
source: ProviderContractError::ProjectionMismatch {
expected: Box::new(prepared.specialization.clone()),
actual: Box::new(actual),
},
}));
}
要求从哪来:CPU 提供者的 minimum_specialization_requirements 对矩阵乘(DotGeneral)要所有轴的具体维度加布局类别,对索引类(Gather、Slice、Pad、Concatenate、DynamicTruncate 等)要所有轴的具体维度,对逐元素、规约和布局类只要 dtype 和秩。所以 sum(x * x) 的 Prepared 不关心长度,矩阵乘的关心每一维。
报错的措辞来自错误类型的复用。ProjectionMismatch 本来描述的是「A prepared operation returned a different specialization projection」,即提供者返回的特化和运行时要求的不一致,那才是提供者的错;validate_execution_signature 把「调用方换了输入维度」也报成同一个变体,外面再包一层 ProviderContract。这是 0.8.0 的上游措辞问题:错误本身是对的,名字误导人。
修法
换了维度就用新输入重新 prepare。准备缓存按结构、纪元、引擎和特化投影做键,同一组维度第二次 prepare 命中缓存:
let p2 = rt.prepare(&program, &reads_5x7_7x4, &[engine.target()])?;
let out = rt.run(&mut [&mut engine], &p2, &reads_5x7_7x4)?;
prepare again for 5x7 * 7x4: shape [5, 4], first 7.0
prepared_plans: entries=5 hits=1 misses=5
after preparing 5x7x4 again: entries=5 hits=3 misses=5
- 每个
Prepared只对准备时那组维度有效;只有不含矩阵乘、索引类运算的程序能跨长度复用。 - 维度在一个小集合里变化时(几种 batch 大小),每种维度各留一个
Prepared,以维度为键自己存一张表,run前按输入形状取;或者每次都调prepare,命中缓存的代价见 执行与缓存 的「时间花在哪里」。 - 想把第一次执行的开销挪到启动时,对每组维度各
prepare一次再warm_up(它就是一次丢掉输出的run)。
怎么确认没中招
- 搜「
prepare一次、循环里run」的写法,看循环里输入的形状会不会变;会变,且程序里有matmul、dot_general、gather、slice这类运算,就要按形状重新prepare。 - 看到
prepared specialization projection mismatch,先打印 Debug 里的concrete_dimensions,比较expected和actual。 - 测试里用两组不同的维度连着跑同一个
Prepared。
小结
- CPU 的矩阵乘和索引类运算按具体维度特化;符号形状的程序编译一次没问题,但
Prepared只适用于准备时那组维度。 - 换维度
run报provider contract violation: prepared specialization projection mismatch;这是调用方的问题,不是提供者的 bug,错误变体被复用了。 - 修法是用新输入重新
prepare(同一组维度命中缓存),或每组维度各留一个Prepared。