ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Newton 多世界(Worlds)机制详解:在单个 Model 中组织、隔离与并行仿真多个独立场景

2026/9/17 22:04:09 拓冰建站 浏览量
Newton 多世界(Worlds)机制详解:在单个 Model 中组织、隔离与并行仿真多个独立场景 Newton 多世界Worlds机制详解在单个 Model 中组织、隔离与并行仿真多个独立场景【免费下载链接】newtonAn open-source, GPU-accelerated physics simulation engine built upon NVIDIA Warp, specifically targeting roboticists and simulation researchers.项目地址: https://gitcode.com/GitHub_Trending/newton9/newtonNewton 基于 NVIDIA Warp 构建支持在单个Model对象内同时承载多个相互独立的仿真场景即worlds世界。本文围绕 Newton 官方概念文档docs/concepts/worlds.rst展开系统讲解 world 的创建与分配begin_world/end_world/add_world/replicate、Model中的世界分组数组与*_world_start索引布局、每世界重力配置、按世界选择的重置掩码world_mask以及面向异构多世界仿真的 GPU 二维线程划分模式。读完本文你可以用 Newton 构建“全球面 多个不同机器人/场景”的混合模型理解其底层索引结构并在自定义 Warp kernel 中正确地按世界划分线程网格。1. 为什么需要 WorldGPU 并行仿真中的场景分组Newton 的 GPU 加速操作通常直接对整个模型实体集合bodies、shapes、joints 等做并行处理无需关心实体属于哪个场景。但在以下情形中world 维度的分组带来了实质收益碰撞检测CD过滤基于世界的分组可以有效过滤掉分属不同 world 的 shape 之间的潜在碰撞避免跨场景的无意义窄相位计算线程网格划分GPU 线程网格可以同时按“world 索引”和“每个 world 内的实体数量”划分成二维网格选择性操作masking与数据切分可以只操作某个 world 内的实体并据此切分Model与State的数据。这些能力共同支撑了在单个Model实例中仿真多个、甚至异构heterogeneousworld 的能力。需要注意的是从官方文档标注的experimental提示看完全异构仿真的支持仍在积极开发中ModelBuilder和Model已支持实例化包含不同实体的 world但并非所有求解器都能仿真它们且 selection API 目前仍假设模型同质未来计划扩展。2. World 分配begin_world/end_world上下文World 的分配由ModelBuilder在通过add_body、add_joint、add_shape等方法添加实体时管理。分配方式分为全局world 索引-1或特定 world索引0, 1, 2, ...。官方文档给出两种支持的工作流在第一次调用begin_world之前、或最后一次end_world之后添加的实体归属全局 world索引-1在begin_world与end_world之间添加的实体归属当前特定 world通过ModelBuilder.add_world或ModelBuilder.replicate从子 builder 批量创建 world。在 world 作用域内每个实体被赋予当前的 world 索引ModelBuilder.current_world是一个只读属性反映当前 builder 上下文不允许直接赋值。从源码 builder.py 可以看到current_world的 setter 会直接抛出AttributeError提示必须使用begin_world()、end_world()、add_world()或replicate()来管理 world。begin_world的实现builder.py还揭示了两个文档未强调的细节world 不可嵌套若已在 world 上下文中再次调用begin_world会抛出RuntimeError必须先end_world支持在构建时指定 per-world 重力begin_world(label..., gravity...)可为该 world 单独设定重力向量未指定则继承 builder 的默认重力。下面的示例在单个模型中创建两个不同的 world继承自官方文档的可运行示例import warp as wp import newton builder newton.ModelBuilder() # Global entity at front (world -1) builder.add_ground_plane() # World 0: two free-floating spheres builder.begin_world() b0 builder.add_body(mass1.0) b1 builder.add_body(mass1.0) builder.add_shape_sphere(bodyb0, radius0.1) builder.add_shape_sphere(bodyb1, radius0.1) builder.end_world() # World 1: fixed-base revolute articulation with boxes builder.begin_world() link0 builder.add_link(mass1.0) j0 builder.add_joint_fixed(parent-1, childlink0) link1 builder.add_link(mass2.0) j1 builder.add_joint_revolute(parentlink0, childlink1) builder.add_articulation(joints[j0, j1]) builder.add_shape_box(bodylink0, hx0.1, hy0.1, hz0.1) builder.add_shape_box(bodylink1, hx0.1, hy0.1, hz0.1) builder.end_world() # Global entity at back (world -1) builder.add_shape_box(body-1, hx0.5, hy0.5, hz0.05) model builder.finalize()该模型包含 world0两个自由漂浮的球体与 world1固定基座、含旋转关节的连杆机构以及两个全局实体前置的地面平面和后置的静态盒子world 索引均为-1。对于同质多世界场景官方文档建议优先使用add_world或replicate而不是为每个副本手动重复 world 作用域见第 5 节。3. World 分组Model中的*_world数组ModelBuilder内部维护着记录每个实体 world 归属的列表调用finalize后生成的Model对象会包含存储每类实体 world 索引的数组。目前支持 world 分组的实体类型包括实体类型Model 属性Particlesmodel.particle_worldBodiesmodel.body_worldShapesmodel.shape_worldJointsmodel.joint_worldArticulationsmodel.articulation_world对第 2 节的示例打印这些数组得到print(Body worlds:, model.body_world.numpy().tolist()) print(Shape worlds:, model.shape_world.numpy().tolist()) print(Joint worlds:, model.joint_world.numpy().tolist())Body worlds: [0, 0, 1, 1] Shape worlds: [-1, 0, 0, 1, 1, -1] Joint worlds: [0, 0, 1, 1]从 model.py 的Model类定义可以确认这些数组与下面介绍的*_world_start数组一起作为模型的一等属性维护如 world_start 属性规格 中particle_world_start、body_world_start、shape_world_start、joint_world_start、articulation_world_start等均以AttributeSpec形式声明并采用world_start压缩策略。4.*_world_start数组索引布局与计数规则除分组数组外Model还包含记录每类实体每个 world 起始索引的 Warp 数组Particles:model.particle_world_startBodies:model.body_world_startShapes:model.shape_world_startJoints:model.joint_world_startArticulations:model.articulation_world_start由于 joint 实体在 DOF、坐标和约束数量上各不相同模型还提供了这些维度上的 per-world 起始索引Joint DOFs:model.joint_dof_world_startJoint Coordinates:model.joint_coord_world_startJoint Constraints:model.joint_constraint_world_start所有*_world_start数组采用一种特殊格式便于统计每个 world 的实体总数以及全局 world索引-1的实体数。具体而言每个*_world_start数组包含world_count 2个条目前world_count个条目对应各world 0的起始索引倒数第二个条目对应后置全局实体的起始索引world 索引-1最后一个条目对应模型中实体或维度的总数。由于这些数组本质上是累加和连续条目之差即为该 world 的实体数全局实体总数 第一个条目 最后两个条目之差。一个关键细节*_world_start数组不为全局 world-1提供单一连续区间。因为全局实体可能既在第一个本地 world 之前添加也在最后一个本地 world 之后添加所以它们分布在两个区间[0, world_start[0])前置添加的全局实体[world_start[-2], world_start[-1])后置添加的全局实体。以第 2 节示例中的 shape 数组为例其布局为shape index: 0 1 2 3 4 5 shape_world: -1 0 0 1 1 -1 shape_world_start: [1, 3, 5, 6]World0占据 shape 索引[1, 3)world1占据[3, 5)。两个全局 shape 被拆分在索引0前和索引5后因此没有任何单一区间切片能同时选中它们。要选中所有全局 shape必须按每个 shape 的 world 索引做掩码global_shape_mask model.shape_world.numpy() -1 # [True, False, False, False, False, True]接着可以计算每个 world 的 shape 数量完整示例继承自官方文档print(world_count:, model.world_count) # Shape start indices per world # Entries: [start_world_0, start_world_1, start_global_back, total_shapes] shape_start model.shape_world_start.numpy() print(Shape starts:, shape_start.tolist()) # Compute per-world shape counts world_shape_counts [ int(shape_start[i 1] - shape_start[i]) for i in range(model.world_count) ] # Global shapes: those at the front (before start_world_0) plus at the back global_shape_count int(shape_start[0]) int(shape_start[-1] - shape_start[-2]) print(Shape counts per world:, world_shape_counts) print(Global shape count:, global_shape_count)world_count: 2 Shape starts: [1, 3, 5, 6] Shape counts per world: [2, 2] Global shape count: 2这一布局在 model.py 的particle_world_start文档字符串中有明确说明条目0 .. world_count-1存储各 world 的起始索引num_in_world start[w1] - start[w]而num_global start[-1] - start[-2] start[0]与上述规则完全一致。5. 便捷方法add_world与replicatebegin_world/end_world提供完全的控制权而 Newton 还为最常见的多世界模式提供了两个便捷方法。5.1add_world一次性并入预构建的子 builderadd_world等价于将begin_world/add_builder/end_world合并为一次调用源码见 builder.py。实现上还支持xform对子 builder 所有根 body 施加变换用于视觉排布与label_prefix给实体标签加前缀以区分实例。值得注意的是源码在add_builder抛出异常时会回滚begin_world的效果重置_current_world、world_count与world_gravity保证失败的合并不会遗留未闭合的 world 上下文。import newton # Build a simple two-link arm arm newton.ModelBuilder() link0 arm.add_link(mass1.0) j0 arm.add_joint_fixed(parent-1, childlink0) link1 arm.add_link(mass1.0) j1 arm.add_joint_revolute(parentlink0, childlink1) arm.add_articulation(joints[j0, j1]) arm.add_shape_box(bodylink0, hx0.1, hy0.1, hz0.1) arm.add_shape_box(bodylink1, hx0.1, hy0.1, hz0.1) # Create a scene with two instances of the same arm scene newton.ModelBuilder() scene.add_ground_plane() scene.add_world(arm) scene.add_world(arm) multi_arm_model scene.finalize() print(world_count:, multi_arm_model.world_count)world_count: 25.2replicate批量复制 builder 为 N 个 worldreplicate创建 builder 的N份拷贝每份成为一个独立 world支持空间偏移。其实现builder.py要点spacing参数沿各轴提供规则间距例如(5.0, 5.0, 0.0)会在 XY 平面排成 2D 网格也可以显式传入xforms每个 world 一个变换与label_prefixes每个 world 一个标签前缀批量合并所有副本在单次批处理中合并不会触发add_world的子类重写副本之间可以共享 mesh 引用等对象。官方文档特别提示用物理spacing分隔 world 会把 body 移离原点可能降低数值稳定性若只是为了视觉区分建议保持spacing(0, 0, 0)改用 viewer 层面的世界偏移如viewer.set_world_offsets()让所有 world 在物理仿真中都位于原点。源码 docstring 中的 Note 与之一致。import newton arm newton.ModelBuilder() link0 arm.add_link(mass1.0) j0 arm.add_joint_fixed(parent-1, childlink0) link1 arm.add_link(mass1.0) j1 arm.add_joint_revolute(parentlink0, childlink1) arm.add_articulation(joints[j0, j1]) scene newton.ModelBuilder() scene.add_ground_plane() scene.replicate(arm, world_count4, spacing(2.0, 0.0, 0.0)) replicated_model scene.finalize() print(world_count:, replicated_model.world_count) print(body_count:, replicated_model.body_count)world_count: 4 body_count: 8关于网格mesh的重要注意事项应在把子 builder 传给replicate之前先调用approximate_meshes。因为复制操作拷贝的是 mesh 引用先近似化会得到一份被所有 world 共享的简化副本之后近似化则会为每个被复制的 shape 各分配一份拷贝。import newton arm newton.ModelBuilder() link arm.add_link(mass1.0) mesh newton.Mesh.create_box(0.5, 0.5, 0.5, compute_inertiaFalse) arm.add_shape_mesh(bodylink, meshmesh) arm.approximate_meshes(methodconvex_hull) scene newton.ModelBuilder() scene.replicate(arm, world_count4) replicated_model scene.finalize() print(world_count:, replicated_model.world_count)world_count: 4replicate的批处理逻辑对应 _merge_builder_copies它按实体类型一次性计算所有副本的起始偏移start_arrays[kind] base world_range * counts[kind]并校验 up_axis 一致性、xforms/label_prefixes长度相关行为有 test_builder_replicate.py 覆盖。6. Implicit MPM 求解器的 World 隔离多世界场景下SolverImplicitMPM默认使用单一共享的 FEM 拓扑因此仅靠 world 分配并不能隔离相互重叠的 MPM 粒子。若需要隔离应将SolverImplicitMPM.Config.separate_worlds设为真为每个 world 创建独立的 FEM 环境从而隔离 grid 的质量、动量、应力与 collider 响应。隔离模式的约束条件官方文档标注为 experimental可能随时变化要求每个粒子都归属某个本地 world本地 world 的 collider 只影响该 world全局静态 collider以及由运动学kinematicbody 支撑的全局 collider会影响所有 world隔离模式下由动力学dynamicbody 支撑的全局 collider 会被拒绝。稀疏网格容量与 CUDA graph-capture 相关的要求可参考SolverImplicitMPM的类文档collider 配置与选择性重置行为见SolverImplicitMPM.setup_collider与SolverImplicitMPM.reset方法。7. Per-World 重置掩码SolverBase.reset的world_maskSolverBase.reset接受一个可选的world_mask参数它是位于 solver 设备上的一维 Warp 布尔数组形状为(world_count 1,)条目[0, world_count)选择各本地 world最后一个条目选择 world 索引为-1的全局实体全真掩码选择的实体与传None相同但None仍是“完全重置”形式——求解器可能借此清除一些没有 per-world 归属的记录bookkeeping或缓存不支持全局动态对象的求解器会把最后一个条目当作 no-op 处理。自 1.5 版本起旧形状(world_count,)处于弃用过渡期它只选择本地 world不选中全局实体。这一归一化逻辑在源码 reset.py 的normalize_reset_world_mask中实现会校验 dtype 为bool、维度为一维、设备与 solver 一致并对旧形状发出弃用提示实际判定“某世界是否需要重置”的 kernel 辅助函数 reset_world_selected 同时处理本地 world 与全局条目world_mask[world_count]。碰撞匹配模块contact_match.py同样接受world_mask用于按世界累积重置掩码。8. Per-World 重力构建时与运行时配置每个 world 可以拥有独立的重力向量适合模拟不同环境例如一个 world 用地球重力另一个用月球重力。配置方式有两条路径构建时通过ModelBuilder.begin_world的gravity参数见第 2 节源码说明运行时通过Model.set_gravity(gravity, world...)。ModelBuilder.gravity既是新本地 world 的默认值也是全局实体world-1专用重力。最终Model.gravity数组的最后一个元素存放全局重力因此当 builder 包含显式本地 world 时其形状为(world_count 1,) × vec3而旧的隐式单世界模型保留一个形状为(1,)的共享条目对 world0的更新会继续影响其全局实体。set_gravity的实现model.py会对 world 参数做边界检查world -1或world world_count时抛出IndexError运行时行为有 test_runtime_gravity.py 验证。import warp as wp import newton robot_builder newton.ModelBuilder() robot_builder.add_body(mass1.0) scene newton.ModelBuilder() scene.add_world(robot_builder) scene.add_world(robot_builder) model scene.finalize() # Set different gravity for each world model.set_gravity((0.0, 0.0, -9.81), world0) # Earth model.set_gravity((0.0, 0.0, -1.62), world1) # Moon model.set_gravity((0.0, 0.0, -3.71), world-1) # Global entities print(Gravity shape:, model.gravity.shape)Gravity shape: (3,)9. World-Entity 二维 GPU 线程划分world 分组的另一重要用途是把 GPU 线程网格按“world 索引 × 每 world 实体数”划分成二维 world-entity 网格。官方文档给出的完整示例import warp as wp import newton wp.kernel def world_body_2d_kernel( body_world_start: wp.array[wp.int32], body_qd: wp.array[wp.spatial_vectorf], ): world_id, body_world_id wp.tid() world_start body_world_start[world_id] num_bodies_in_world body_world_start[world_id 1] - world_start if body_world_id num_bodies_in_world: global_body_id world_start body_world_id twist body_qd[global_body_id] # ... perform computations on twist ... # Create model with multiple worlds builder newton.ModelBuilder() # ... add entities to multiple worlds ... model builder.finalize() # Define number of entities per world (e.g., bodies) body_world_start model.body_world_start.numpy() num_bodies_per_world [ body_world_start[i 1] - body_world_start[i] for i in range(model.world_count) ] # Launch kernel with 2D grid: (world_count, max_entities_per_world) state model.state() wp.launch( world_body_2d_kernel, dim(model.world_count, max(num_bodies_per_world)), inputs[model.body_world_start, state.body_qd], )这一划分方式的关键点每个线程可通过world_id唯一确定其操作的 world通过body_world_id得到该 world 内的相对实体索引。相对索引在最大坐标maximal-coordinate表述下访问按 world 连续分块的约束 Jacobian 列时特别有用再通过*_world_start数组加上对应起始索引即可映射回全局实体索引同质模型下max(num_bodies_per_world)退化为常数这实际上就是一个*批处理batched*操作异构模型下各 world 实体数不同二维网格中部分线程对实体较少的 world 是空闲的因此 kernel必须检查相对实体索引是否在当前 world 的范围内如示例中的if body_world_id num_bodies_in_worldsum(num_bodies_per_world)等于world-localbody 的总数即body_world_start[-2] - body_world_start[0]不包含全局实体当存在全局 body 时model.body_count会大于这个和因为它同时包含 world-local 与全局实体max(num_bodies_per_world)决定二维线程网格的第二维。这种“对 per-world 实体计数求sum与max”的模式为 Newton 中异构多世界仿真的内存分配与线程网格维度提供了统一的处理方式。10. 小结与延伸阅读Newton 的 worlds 机制由三条线索构成构建期begin_world/end_world上下文、add_world单次并入、replicate批量复制加上构建期的 per-world 重力参数模型层*_world分组数组与*_world_startworld_count 2条目、全局实体前后双区间索引布局支撑计数、掩码与数据切分运行期per-world 运行时重力Model.set_gravity、per-world 重置掩码SolverBase.reset的world_mask、隐式 MPM 的separate_worlds隔离以及面向自定义 kernel 的二维 world-entity 线程划分。多世界场景的构建行为可由 test_multiworld_body_properties.py 等测试印证例如其中用scene.begin_world()构建多世界场景并验证 per-world 刚体属性与物理一致性replicate行为另有 test_builder_replicate.py 覆盖。进一步阅读可参考官方文档的完整清单ModelBuilder、Model、begin_world、end_world、add_world、replicate与Model.set_gravity的 API 文档以及 docs/concepts/worlds.rst 原文。【免费下载链接】newtonAn open-source, GPU-accelerated physics simulation engine built upon NVIDIA Warp, specifically targeting roboticists and simulation researchers.项目地址: https://gitcode.com/GitHub_Trending/newton9/newton创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考