binding shape mismatch for concrete-shape placeholder
主线位置:变换主线的「编译、准备 → 执行」:同一个函数换一个输入长度再调用。
版本:JAX 0.12.0.1(CPU,x64)、tenferro-rs 0.8.0(CPU,native)
flowchart TB c["n 换成 2000"] --> jj["JAX jit"] jj --> rt["自动重新追踪"] c --> tp["tenferro Prepared"] tp --> err["形状不符报错"] c --> cache["按 n 查缓存"] cache --> miss["没有就建程序"] miss --> run["run"] cache --> run
实验代码:JAX 脚本一个,tenferro 程序 p09_shape_recompile(CpuEngine::serial(),release)。函数 f(x) = Σ sin(x)·x²,x_i = 2i/(n−1)。计时是单次调用的墙钟时间,只用来区分「建了新程序」和「命中缓存」,不当基准。平台 M2 Max、macOS 27、rustc 1.99.0。
现象
JAX 里 jit 一次,之后什么长度都能传,形状变了就重新追踪、编译,结果按形状缓存:
jf = jax.jit(f)
for n in (1000, 2000, 1000):
xn = jax.block_until_ready(xs(n))
y = jax.block_until_ready(jf(xn))
n=1000: 1235.325872458312688 19.26 ms cache size 1
n=2000: 2470.067398026252249 21.30 ms cache size 2
n=1000: 1235.325872458312688 0.05 ms cache size 2
tenferro 里对应 jit 参数的是占位符。照「建一次、到处用」的习惯写:
let x = TracedTensor::input_concrete_shape(DType::F64, &[1000])?;
let p = GraphCompiler::new().compile_with_input_specs(&f(&x)?, &[(&x, DType::F64, &[1000][..])])?;
let pr = prepare(&rt, &eng, &p, &[&xs(1000)?])?;
run1(&rt, &mut eng, &pr, &[&xs(2000)?])
n=1000: 1235.325872458312915
n=2000: Err: binding shape mismatch for concrete-shape placeholder: expected [1000], got [2000]
prepare(n=2000): Err: binding shape mismatch for concrete-shape placeholder: expected [1000], got [2000]
n = 1000 时和 JAX 差 2.3e-13(量级 1.2e3,求和顺序不同)。换成 2000,run 报错,拿同一个程序重新 prepare 也报错。
出处
input_concrete_shape 和 compile_with_input_specs 里写的 [1000] 是程序的一部分,不是提示。执行前核对输入的元数据,形状不同就返回 Error::PlaceholderShapeMismatch,这就是标题里那条消息。JAX 的 jit 是一个以形状和 dtype 为键的缓存,键不命中就重新追踪;tenferro 把「追踪、编译、准备」交给调用方,没有这层缓存。占位符、Prepared 和 JAX 缓存的对照见记图与 jit 的「换一个形状」一节。
还有一条相近的错误:用符号形状的叶子编译、准备一次之后换形状 run,矩阵乘这类按具体维度特化的运算报 prepared specialization projection mismatch,那是 tenferro 卷的常见坑prepared specialization projection mismatch。两条的共同点:一个 Prepared 只服务一种形状。
修法
自己写 jit 那层缓存:按形状建程序,准备一次,存起来。
struct ShapeCache {
progs: HashMap<usize, Prepared>,
}
impl ShapeCache {
fn call(&mut self, rt: &Runtime, eng: &mut CpuEngine, x: &Tensor) -> R<f64> {
let n = x.shape()[0];
if !self.progs.contains_key(&n) {
let ph = TracedTensor::input_concrete_shape(DType::F64, &[n])?;
let p = GraphCompiler::new().compile_with_input_specs(&f(&ph)?, &[(&ph, DType::F64, &[n][..])])?;
self.progs.insert(n, prepare(rt, eng, &p, &[x])?);
}
let pr = self.progs.get(&n).ok_or("cache")?;
Ok(run1(rt, eng, pr, &[x])?[0])
}
}
n=1000: 1235.325872458312915 377.4 us cached shapes 1
n=2000: 2470.067398026252704 274.8 us cached shapes 2
n=1000: 1235.325872458312915 9.7 us cached shapes 2
和 JAX 的三次调用一一对应:两次新形状各建一个程序(tenferro 0.27–0.38 ms,JAX 19–21 ms),第三次命中缓存。缓存的键要包含所有会变的东西:多个输入时是每个输入的形状和 dtype。
形状只在少数几维上变、又不想为每种形状重建图时,可以用符号形状的数据叶子编译一次,换形状时只重新 prepare,见记图与 jit。
怎么确认没中招
- 程序要处理的输入形状不止一种时,测试里至少用两种形状各调一次。
- 看到
binding shape mismatch for concrete-shape placeholder,先查是不是把为别的形状准备的Prepared拿来复用了。 - 缓存会一直增长:形状种类很多时(例如每步截断数都不同),给它设上限或按需清理。
小结
- JAX 的
jit是按形状缓存的,换长度自动重新编译;tenferro 的Prepared绑定一种具体形状,换长度报binding shape mismatch for concrete-shape placeholder。 prepare同一个程序也不行:形状写在程序里。- 修法是自己按形状缓存程序;符号形状的叶子可以省掉重建图,但仍要按形状重新
prepare。