Rust in Everything
基于 tenferro-runtime 0.8.0

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;规格的顺序就是输入顺序,形状随之固定。
  • 符号叶子的程序形状可变,输入顺序却是图的遍历顺序,不是创建顺序;顺序传错、形状又一样时结果静默错。

💬 关联讨论

发起讨论
加载中...