has no runtime input binding
主线位置:traced 主线的编译和执行,
GraphCompiler::compile→Runtime::prepare/run的inputs。四种叶子和形状在什么时候确定见 TracedTensor,prepare和run的输入规则见 执行与缓存 的「输入:prepare和run都要给」。
flowchart TB
p["无数据占位符"] --> c1["compile"]
c1 --> err["未绑定报错"]
p --> c2["带 specs 编译"]
c2 --> extra{"规格多于用到的"}
extra -->|是| err2["多余规格报错"]
extra -->|否| ord1["顺序 = specs 顺序"]
s["带数据符号叶子"] --> c3["compile"]
c3 --> ord2["顺序 = 图的遍历顺序"]
实验 crate exp-tenferro8/pit2-exp,bin p20_placeholder,CPU(CpuEngine::serial()),debug,M2 Max、macOS 27、rustc 1.99.0。
现象
想写一个「编译一次、喂不同数据」的程序 b - a,用不带数据的占位符:
let a = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
let bb = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
let diff = (&bb - &a)?; // b - a
match c.compile(&diff) { /* 打印 Display 和 Debug */ }
compile: Err: placeholder User { id: 1 } has no runtime input binding
UnboundPlaceholder { input_key: "User { id: 1 }" }
- 还没到运行时,
compile就失败了,消息里却说「runtime input binding」。 - 报的是
User { id: 1 },也就是第二个创建的b:它在b - a里先被遍历到。
改用 compile_with_input_specs,顺手把一个后面才用得到的占位符也写进规格:
let unused = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
c.compile_with_input_specs(&diff, &[(&a, DType::F64, &[2]), (&bb, DType::F64, &[2]), (&unused, DType::F64, &[2])])
specs [a, b, unused]: Err: GraphCompiler::compile_with_input_specs (Compile): invalid argument bindings: binding 2 declares a placeholder the output does not depend on; remove it from the bindings
换成带数据的符号叶子,编译通过,输入顺序却和创建顺序相反:
let sa = TracedTensor::from_tensor_symbolic_shape(v(0.0)?)?; // 先建 a
let sb = TracedTensor::from_tensor_symbolic_shape(v(0.0)?)?; // 再建 b
let p = c.compile(&(&sb - &sa)?)?;
run(&p, &[&v(10.0)?, &v(1.0)?]) // 以为是 a=10, b=1
run(10, 1): b - a = [9.0, 9.0]
input_count = 2, index of a = Some(1), index of b = Some(0)
same program, length 5: [2.0, 2.0, 2.0, 2.0, 2.0]
按创建顺序传 a = 10, b = 1,期望 b - a = -9,得到的是 9:程序的第 0 个输入是 b。两个输入形状相同,不会有任何报错。
出处
compile 给每个输入找一个描述,descriptor_for_input 依次查默认张量、调用方给的规格,都没有就报错:
if let Some(spec) = binding_specs.get(key) {
return Ok(spec.clone());
}
if allow_unbound_placeholders {
return descriptor_for_unbound_input(key);
}
Err(Error::UnboundPlaceholder {
input_key: format!("{:?}", key),
})
input_symbolic_shape 建的占位符没有默认张量,普通的 compile 又不给规格,于是失败。Error::UnboundPlaceholder 的文字「has no runtime input binding」是为运行时缺输入写的,编译期复用了同一个变体。
compile_with_input_specs 按 bindings 列表的顺序收集 input_order,编译完再逐个检查每个规格在程序里有没有位置,没有就是上面那条「binding 2 declares a placeholder the output does not depend on」。文档的理由是:输出用不到的占位符不会成为程序输入,运行时传给它的张量对不上任何位置。这条检查是提交 f3a8c1a2(首次进入 v0.8.0)加的,提交说明写着之前多余的规格被静默丢掉,「a tensor passed for it at run time was rejected or, when counts matched, bound to a retained constant」:个数凑巧对上时,传进来的张量会顶替一个常数。现在编译期就拒绝。
没有规格时,输入顺序是 graph.inputs 的顺序,来自 computegraph 物化图时从输出往回的遍历,b - a 先遇到 b。CompiledGraph 上能查顺序的 input_keys / input_key_index 都标了 #[doc(hidden)](program.rs),文档里看不到。
修法
占位符用 compile_with_input_specs,只列输出真正用到的,bindings 的顺序就是输入顺序:
let p = c.compile_with_input_specs(&diff, &[(&a, DType::F64, &[2]), (&bb, DType::F64, &[2])])?;
run(&p, &[&v(10.0)?, &v(1.0)?]) // a=10, b=1
specs [a, b], run(10, 1): b - a = [-9.0, -9.0]
run with a=[3]: Err: binding shape mismatch for concrete-shape placeholder: expected [2], got [3]
run with 1 input: Err: Runtime::run (Execution): invalid argument inputs: expected 2 inputs, or one explicit input for each unbound placeholder
代价是规格里的形状被当作具体形状,之后换长度就被拒绝(第二行)。要真正的可变长度,用带数据的符号叶子 from_tensor_symbolic_shape,同一个程序可以喂别的长度(上面的 length 5);这时顺序由图决定,要自己查:
let ia = sa.input_key().and_then(|k| p.input_key_index(&k)); // Some(1)
let ib = sb.input_key().and_then(|k| p.input_key_index(&k)); // Some(0)
input_key_index 是隐藏的 API,0.8.0 能用,不保证以后还在。只有一个可变长度输入时不存在顺序问题;有多个时,在同一个模块里固定一种写法,并用一组形状相同、数值不对称的输入(例如 10 和 1)测一次顺序。
怎么确认没中招
- 程序有两个以上输入时,写一个单测:每个输入填不同的常数,检查输出是不是按你以为的顺序算出来的。
- 用
program.input_count()核对输入个数:from_vec_col_major建的具体叶子也是输入(带默认值),漏传时报expected 2 inputs, or one explicit input for each unbound placeholder。 - 改了表达式的写法(
b - a改成-(a - b))、或删掉了一个占位符的用处之后,重新跑一遍顺序测试和编译。
小结
- 不带数据的占位符直接
compile报placeholder ... has no runtime input binding,是编译期错误;用compile_with_input_specs给 dtype 和形状。 - 规格只能列输出用到的占位符,多一个报
binding N declares a placeholder the output does not depend on;规格的顺序就是输入顺序,形状随之固定。 - 符号叶子的程序形状可变,输入顺序却是图的遍历顺序,不是创建顺序;顺序传错、形状又一样时结果静默错。