矩阵乘结果不对,也没有报错
主线位置:直接计算主线的第 1 步。
TypedTensor::from_vec_col_major建张量,这一步决定后面每个运算看到的是哪个矩阵。为什么拥有型张量只有列主序、视图怎么表示步长,见列主序与视图。
flowchart TB
rows["按行写的 Vec"] --> ctor["from_vec_col_major"]
ctor --> chk{"元素个数对吗"}
chk -->|对| t["按列读进张量"]
t --> wrong["矩阵错位"]
wrong --> mm["matmul 照常算"]
mm --> out["结果错,无报错"]
rows --> fix["from_vec_row_major"]
fix --> ok["重排成列主序"]
实验 crate exp-tenferro8/pit1-pit(tenferro 0.8.0,默认 native provider),bin p01_row_major,release;M2 Max、macOS 27、rustc 1.99.0。
现象
想算 A = [[1,2,3],[4,5,6]] 乘 B = [[1,0],[0,1],[1,1]],答案是 [[4,5],[10,11]]。照 C、NumPy、Rust 嵌套 Vec 的习惯把两个矩阵按行摊平:
let mut engine = CpuEngine::serial();
let a_row = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0];
let b_row = vec![1.0_f64, 0.0, 0.0, 1.0, 1.0, 1.0];
let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 3], a_row.clone())?;
let b = TypedTensor::<f64>::from_vec_col_major(vec![3, 2], b_row.clone())?;
let c = a.matmul(&b, &mut engine)?;
wrong: a rows [[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]]
wrong: a*b rows [[1.0, 9.0], [2.0, 12.0]]
(rows 是实验里的小函数,按 d[i + m * j] 把列主序缓冲区按行打印。)两次构造都成功,matmul 也成功,只是 a 已经不是 A:第一列是 [1, 2],第二列是 [3, 4]。四个结果没有一个对。
方阵更隐蔽,形状完全没有机会出错,张量恰好是想要的矩阵的转置:
square: m*[1,1] = [4.0, 6.0] (want [3.0, 7.0])
(m 用 vec![1.0, 2.0, 3.0, 4.0] 构造,本意是 [[1,2],[3,4]],乘全 1 列向量本该得到行和 [3, 7],得到的是列和。)对称矩阵上完全看不出来,测试只用单位阵、全 1 矩阵或对称矩阵时,这个错误能一直留到真实数据上。
出处
TypedTensor::from_vec_col_major 最后落到 try_typed_tensor_from_vec_col_major:
let layout = tenferro_tensor_core::col_major_strides(shape.as_ref())
.map_err(|err| tensor_layout_error(op, err))?;
try_checked_shape_len(shape.as_ref(), data.len(), op)?;
Ok(TypedTensor {
shape,
placement: default_placement(),
layout,
storage: DynamicStorage::Host(HostStorage {
data: HostData::new(data),
}),
})
它只检查「形状之积等于 data.len()」,然后把 Vec 原样收进存储,步长是紧凑列主序。一串数字本身不带「我是按行排的」这个信息,构造函数无从判断。README 的「Conventions you must know before writing code」第一条写的就是这件事:「Row-major data passed to from_vec_col_major is silently reinterpreted as column-major — permuted/wrong values, never rejected.」
修法
0.8.0 有显式的行主序入口 from_vec_row_major,TypedTensor 和动态的 Tensor(Tensor::from_vec_row_major)上都有。它把数据重排一次,再交给列主序构造函数,结果仍是普通的紧凑列主序张量。另一种做法是自己按列写缓冲区:
// 一:显式按行导入
let a1 = TypedTensor::<f64>::from_vec_row_major(vec![2, 3], a_row.clone())?;
let b1 = TypedTensor::<f64>::from_vec_row_major(vec![3, 2], b_row.clone())?;
// 二:直接按列写
let a2 = TypedTensor::<f64>::from_vec_col_major(vec![2, 3], vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0])?;
let b2 = TypedTensor::<f64>::from_vec_col_major(vec![3, 2], vec![1.0, 0.0, 1.0, 0.0, 1.0, 1.0])?;
// 动态张量同样有
let t = Tensor::from_vec_row_major(vec![2, 3], a_row.clone())?;
fix from_vec_row_major: a*b rows [[4.0, 5.0], [10.0, 11.0]]
fix col-major buffer: a*b rows [[4.0, 5.0], [10.0, 11.0]]
Tensor::from_vec_row_major buffer [1.0, 4.0, 2.0, 5.0, 3.0, 6.0]
from_vec_row_major 是单线程的逐元素复制。数据大、手里又已经有引擎时,把行主序缓冲区包成步长 [n, 1] 的视图再 engine.to_contiguous 更快,几种做法的耗时对比和这个入口的来历见列主序与视图。
怎么确认没中招
单元测试里至少放一个非对称、非方阵的输入,并抽查一个元素:
check a1.get([0,1]) = Ok(2.0) (want 2.0), a.get([0,1]) = Ok(3.0)get(&[i, j])按逻辑下标取值,和布局无关;[0, 1]应该是第 0 行第 1 列的 2。搜一遍代码里所有
from_vec_col_major的调用点,看数据从哪里来:嵌套Vec的flatten()、ndarray默认(C 序)数组的into_raw_vec、按行读的 CSV、PyTorch 和 NumPy 导出的缓冲区,都是行主序,应该换成from_vec_row_major。读回结果时同样要按列主序解释:
host_data()和as_slice()给的是列主序缓冲区,按行打印要用i + m·j取下标。
小结
from_vec_col_major只查元素个数,发现不了数据是按行排的;方阵上的结果恰好是转置。- 行主序数据用
from_vec_row_major(TypedTensor和Tensor都有),它重排一次,结果仍是列主序。 - 测试用非对称、非方阵的输入,抽查
get一个已知元素。