Warp 稀疏体数据梯度采样:修复 wp.volume_sample_grad 对 vec4f/vec4d 卷的编译失败,深入解析 4×3 雅可比矩阵约定
【免费下载链接】warpA Python framework for GPU-accelerated simulation, robotics, and machine learning.项目地址: https://gitcode.com/GitHub_Trending/warp/warp
本文聚焦 Warp 项目 changelog 1691 号修复项:wp.volume_sample_grad()与wp.volume_sample_grad_index()对vec4f/vec4d类型体素数据无法编译的问题,及其根因——梯度参数的形状约定。你将理解为什么 N 分量向量的梯度必须是一个「N 行 × 3 列」的雅可比矩阵(每个值分量占一行)、类型检查在编译期如何拦截错误形状,以及底层 CUDA 实现如何通过外积累加生成该矩阵,并学会在wp.Tape自动微分中对梯度本身求导。
问题背景:稀疏卷上的值与梯度联合采样
Warp 支持以 NanoVDB 标准表示的稀疏体(sparse volume),常用于大域网格数据,如复杂物体的符号距离场(SDF)或大规模流场速度。用户指南在 runtime.rst 的 Volumes 小节 中说明了wp.Volume的创建与采样方式:可以从.nvdb文件、内存缓冲或 NumPy 数组加载,也可以用allocate()、allocate_by_tiles()、allocate_by_voxels()直接分配。
在 Warp 内核中对卷采样时,通过卷的id引用它,并选择采样模式:
wp.Volume.CLOSEST:把每个坐标四舍五入到最近体素,适合整数数据,梯度恒为零;wp.Volume.LINEAR:对浮点标量/向量数据做三线性插值,插值结果在整数体素平面之间关于索引空间坐标可导。
wp.volume_sample_grad()就是「值 + 空间梯度」联合采样的内置函数,与只取值的wp.volume_sample()行为一致,额外把采样值对索引空间坐标uvw的梯度写入输出参数grad。其文档字符串(见 builtins.py)明确定义了grad的形状约定:
对标量
dtype,grad是与标量同类型的长度为 3 的向量;对受支持的 N 分量向量类型,grad是一个N×3 的雅可比矩阵,每个值分量占一行(one row per value component)。
受支持的dtype包括int32、int64、uint32、float32、float64、vec3f、vec3d、vec4f、vec4d。
1691 修复了什么:vec4 卷的梯度类型编译期不匹配
changelog 条目 changelog/1691.fixed.md 原文只有两句:
Fix
wp.volume_sample_grad()andwp.volume_sample_grad_index()failing to compile forvec4fandvec4dvolume data. Their gradient argument is a 4-by-3 matrix with one row per value component.
这句话信息量很大,可以拆解为两个事实:
- 症状:当卷存储的值类型是
vec4f/vec4d时,调用这两个内置函数会直接编译失败(Warp 的内置函数在 codegen 阶段做类型检查,类型不匹配即抛RuntimeError,表现为 kernel 无法编译)。 - 约定重申:对 4 分量值类型,梯度参数必须是一个4×3 矩阵——第 r 行存放「第 r 个分量对
(u, v, w)三个索引空间坐标的偏导数」。
结合源码可以推断出此前的失败路径:volume_sample_grad的value_func在解析内核时调用check_volume_value_grad_compatibility()(见 builtins.py),该函数对向量类型期望的梯度类型是matrix(shape=(type_size(dtype), 3), dtype=scalar)。修复前若 vec4 分支的期望形状计算有误(例如按列组织,得到 3×4),调用者传入正确的 4×3 雅可比反而会被判为不兼容,导致编译报错。修复后的检查逻辑是:
def check_volume_value_grad_compatibility(dtype, grad_dtype): if type_is_vector(dtype): expected = matrix(shape=(type_size(dtype), 3), dtype=type_scalar_type(dtype)) else: expected = vector(length=3, dtype=dtype) if not types_equal(grad_dtype, expected): raise RuntimeError(f"Incompatible gradient type, expected {type_repr(expected)}, got {type_repr(grad_dtype)}")它由volume_sample_grad的 value_func(builtins.py)与volume_sample_grad_index的 value_func(builtins.py)共同调用,因此在两个函数上行为一致。检查失败时会给出可定位的错误信息,例如传错形状时会得到Incompatible gradient type, expected mat43f, got mat34f。
形状约定的正确读法:行 = 值分量,列 = 索引空间坐标
「每个值分量一行」这个约定直接决定了 Jacobian 的数学含义。设采样点索引空间坐标为uvw,采样值为 4 分量向量f(uvw),则:
- 正确类型:
mat43f/mat43d(4 行 × 3 列),grad[r][c] = ∂f_r / ∂uvw_c,与标准的「行向量微分(Jacobian 布局)」一致,即df = grad · duvw; - 错误类型:
mat34f/mat34d(转置布局,每个分量占一列),现在会被编译期拒绝。
这一约定在 C++ 侧的类型特征中有对应体现。volume.h 定义了按值类型推导梯度类型的模板:
template <typename T> struct val_traits { using grad_t = vec_t<3, T>; // 标量 → vec3 ... }; template <unsigned Length, typename T> struct val_traits<vec_t<Length, T>> { using grad_t = mat_t<Length, 3, T>; // N 分量向量 → Length×3 矩阵 ... };也就是说,C++ 层面对vec4的grad_t本来就是mat_t<4, 3, T>,与 Python 侧修复后的期望类型严格一致。
底层实现:三线性插值的雅可比如何生成
LINEAR模式下的梯度来自三线性插值函数的解析导数。CUDA 实现位于 volume.h:
- 用
floorf取采样点所在单元格的角点ijk_base,分解出分数部分ijk_frac; - 计算三个轴上的线性权重
wx[2] = {1-fx, fx}等(注意:权重以 float 单精度计算,即使卷本身是vec4d); - 遍历 8 个角点,累加
val += w * v的同时,用外积累加梯度:grad += outer(v, grad_w),其中grad_w是权重w对uvw的导数(一个 vec3,符号项offs*2-1保证只有移动角点方向的权重对导数有贡献)。
8 个角点的outer(v, grad_w)累加完成后,grad自然就是一个 N×3 矩阵:每个分量一行。这也解释了文档中「远离整数体素平面可导、平面上不可导」的说明——三线性插值函数是分段线性的,其雅可比在平面处发生跳变(当前实现取正侧单元格的导数,但文档明确提示调用方不应依赖这一行为)。
另有两个实用细节来自内置函数的文档:
- 梯度是关于索引空间坐标而非世界空间的。若
uvw由wp.volume_world_to_index()从世界坐标换算而来,要得到世界空间梯度还需乘1/voxel_size; CLOSEST模式下grad为零,整数数据应使用CLOSEST;且当前实现不向存储的体素值传播梯度。
volume_sample_grad_index()则是同一套逻辑的「外部数组」版本:卷只提供拓扑与体素线性索引,实际值从独立的voxel_data数组读取(background值用于无体素位置,且二者 dtype 必须一致,见 builtins.py)。它额外支持对voxel_data与background的反向模式求导,这对可微优化场景(如逆渲染、参数拟合)尤为关键。
调用示例:可复制的 vec4 采样内核
下面是一段与测试用例同构的完整用法(对应 test_volume.py 中的内核):
import warp as wp import numpy as np @wp.kernel def sample_vec4( volume: wp.uint64, points: wp.array[wp.vec3], values: wp.array[wp.vec4f], grads: wp.array[Any], # wp.types.matrix(shape=(4, 3), dtype=wp.float32) ): tid = wp.tid() grad = grads.dtype() # 用数组 dtype 直接构造正确的 4x3 矩阵类型 # 从世界坐标换算到索引空间(uvw 为体素坐标,可为分数) p = wp.volume_world_to_index(volume, points[tid]) values[tid] = wp.volume_sample_grad(volume, p, wp.Volume.LINEAR, grad, dtype=wp.vec4f) grads[tid] = grad要点:
- 梯度数组的 dtype 应写成
wp.types.matrix(shape=(4, 3), dtype=wp.float32)(即mat43f);对vec4d卷则用shape=(4, 3), dtype=wp.float64。测试文件中的辅助函数 _vec4_grad_type 正是这样构造期望类型的; - 内核内用
grads.dtype()从数组推导局部变量类型,可以避免手写矩阵类型; - 若误传转置形状
mat34f,wp.launch会在编译期抛出RuntimeError: Incompatible gradient type, expected mat43f, got mat34f,而非静默出错。
标量卷的用法更简单(摘自内置函数的 docstring 示例,builtins.py):
@wp.kernel def sample_grad(vid: wp.uint64, out: wp.array[wp.float32]): grad = wp.vec3() out[0] = wp.volume_sample_grad(vid, wp.vec3(0.5, 0.0, 0.0), wp.Volume.LINEAR, grad, dtype=float) out[1] = grad[0] values = np.zeros((2, 2, 2), dtype=np.float32) values[1, :, :] = 1.0 # f(i, j, k) = i volume = wp.Volume.load_from_numpy(values, voxel_size=1.0, bg_value=0.0) out = wp.zeros(2, dtype=wp.float32) wp.launch(sample_grad, dim=1, inputs=[volume.id], outputs=[out]) print(round(float(out.numpy()[0]), 1), round(float(out.numpy()[1]), 1)) # 0.5 1.0测试证据:vec4 路径被三类测试锁定
修复是否可靠,看回归测试即可确认。test_volume.py 中与本修复直接相关的用例:
- 前向值 + 雅可比正确性——test_volume_sample_grad_v4:为
wp.vec4f和wp.vec4d分别构造承载线性场v(x) = A x的卷(A即_VEC4_FIELD_JACOBIAN),在随机分数点上采样,用np.testing.assert_allclose同时校验采样值与 4×3 雅可比。注意其容差计算 _vec4_field_tolerance:由于三线性权重始终以单精度求值,误差尺度由插值值幅度决定(32 * eps_f32 * |values|_max),而与卷标量类型自身的精度无关——这正是vec4d与vec4f共用一套容差的原因。 - 外部数组 + 反向模式——test_volume_sample_grad_index_v4:用
wp.Tape包裹volume_sample_grad_index的 launch,两次tape.backward():第一次给values.grad填 1,验证sum(values) = Σ(adj_voxel_data · voxel_data) + adj_background · background;第二次给grads.grad填 1,验证对雅可比本身求和后的反向导数。 - 对采样点的二阶梯度——test_volume_sample_grad_index_v4_adjoint:把采样点
points设为requires_grad=True,用中心差分(步长 0.1,且所有扰动点保持在同一三线性单元格内以保证插值函数光滑)与 tape 给出的points.grad交叉验证。 - 拒绝转置形状——test_volume_sample_grad_rejects_transposed_grad:显式构造
mat34f梯度参数并断言wp.launch抛出匹配Incompatible gradient type, expected mat43f, got mat34f的RuntimeError。这个用例把 1691 修复的「形状约定」从文档层面固化成了编译期行为契约。
小结与使用要点
- 对
vec4f/vec4d卷调用wp.volume_sample_grad()/wp.volume_sample_grad_index()时,梯度参数必须声明为4×3 矩阵(mat43f/mat43d),每行对应一个值分量对(u, v, w)的三个偏导;传 3×4 会在编译期被check_volume_value_grad_compatibility()拒绝。 - 梯度是索引空间梯度,世界空间需自行除以
voxel_size;LINEAR在整数体素平面处不可导,跨平面时不要依赖「正侧单元格」这一当前行为。 - 类型推导可在 C++ 层
val_traits<T>::grad_t(volume.h)与 Python 层检查函数之间交叉印证;测试文件 test_volume.py 提供了从值正确性、反传到编译期报错的全套验证范式,可作为自定义调用时的模板。
【免费下载链接】warpA Python framework for GPU-accelerated simulation, robotics, and machine learning.项目地址: https://gitcode.com/GitHub_Trending/warp/warp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考