Rust in Everything
基于 tenferro-runtime 0.8.0

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。

💬 关联讨论

发起讨论
加载中...