Skip to content

FEM SpaceRestriction and SpacePartition rebuild do not invalidate cached Arg values #1852

Closed

Description

@pei-tian

Bug Description

Two FEM rebuild() implementations reallocate the arrays referenced by that Arg but didn't call .invalidate():

SpaceRestriction.rebuild(): It reallocates the restriction index arrays when the domain size changes, but the cached NodeArg keeps the old pointers. Issue is reproduced.

NodePartition.rebuild() / EnvironmentSpacePartition.rebuild(): also missing partition_arg_value.invalidate(). Not reproducible because in normal same-device rebuilds, _space_to_partition keeps shape 2 * topology.node_count() and receives the new mapping in place, so the cached Arg still points to current data and no failure was reproduced. It is still worth clearing the cache here too, so both rebuild handle cached Args the same way.

Source:
Missing self.node_arg_value.invalidate(self) (reproduced):

def rebuild(self, device: Optional["wp.DeviceLike"] = None, temporary_store: cache.TemporaryStore | None = None):
"""Rebuild internal indices for the space restriction."""
max_nodes_per_element = self.space_topology.MAX_NODES_PER_ELEMENT

Missing self.partition_arg_value.invalidate(self) (same pattern, not reproduced):

def rebuild(
self,
device: Optional = None,
temporary_store: cache.TemporaryStore | None = None,
):
"""Rebuild partition mappings.
If the partition layout changes between rebuilds, arrays returned by
:meth:`space_node_indices` or :attr:`env_offsets` should be reacquired.
"""
geometry = self.space_topology.geometry

def rebuild(
self,
device: Optional = None,
temporary_store: cache.TemporaryStore | None = None,
):
self._compute_node_indices_from_sides(device, self._with_halo, self._max_node_count, temporary_store)

Repro Script:

import warp as wp
import warp.fem as fem

wp.init()
device = "cuda:0"

with wp.ScopedDevice(device):
    geo = fem.Grid2D(res=wp.vec2i(4, 4))
    space = fem.make_polynomial_space(geo, degree=1)

    mask = wp.zeros(geo.cell_count(), dtype=int)
    mask[:2].fill_(1)
    geo_partition = fem.ExplicitGeometryPartition(geo, mask)
    space_partition = fem.make_space_partition(
        space_topology=space.topology, geometry_partition=geo_partition, with_halo=False
    )
    restriction = fem.make_space_restriction(
        space_partition=space_partition, domain=fem.Cells(geo_partition), device=device
    )

    cached_before = restriction.node_arg_value(device)
    node_count_before = restriction.node_count_sync()
    print("node count before:   ", node_count_before)
    print("cached mapping before:", cached_before.dof_partition_indices.numpy()[:node_count_before])

    mask[:8].fill_(1)
    geo_partition.rebuild(mask)
    space_partition.rebuild(device=device)
    restriction.rebuild(device=device)

    node_count = restriction.node_count_sync()
    cached_arg = restriction.node_arg_value(device)
    print("node count after: ", node_count)
    print("current mapping:", restriction._dof_partition_indices.numpy()[:node_count])
    print("cached mapping: ", cached_arg.dof_partition_indices.numpy()[:node_count])
    assert cached_arg.dof_partition_indices.ptr == restriction._dof_partition_indices.ptr, (
        "stale NodeArg after rebuild(): cached node to partition mapping does not match restriction"
    )
    print("NodeArg is up to date")

Expected output:

node count before:    6
cached mapping before: [0 1 2 3 4 5]
node count after:     15
current mapping:      [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14]
cached mapping:       [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14]
NodeArg is up to date

Actual output:

node count before:    6
cached mapping before: [0 1 2 3 4 5]
node count after:     15
current mapping:      [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14]
cached mapping:       [         0          1          2          3          4          5
 2147483647          0]
Traceback (most recent call last):
  File "run_p16_node_mapping.py", line 37, in <module>
    assert cached_arg.dof_partition_indices.ptr == restriction._dof_partition_indices.ptr, (
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: stale NodeArg after rebuild(): cached node to partition mapping does not match restriction

System Information

Warp: 1.17.0rc1
Python: 3.12
OS: L4T R38.2
Device: NVIDIA Jetson Thor
CUDA: 13.0.0

Activity

  1. added theissue type on Aug 26, 2026
  2. added this to the 1.18.0 milestone on Aug 26, 2026
  3. shi-eric commented on Aug 26, 2026

    @shi-eric
    Contributor

    This is addressed by 808ddbd.

    The change invalidates cached FEM argument structs before rebuilding SpaceRestriction, NodePartition, and EnvironmentSpacePartition. This prevents a previously cached NodeArg or PartitionArg from retaining stale array references when a rebuild reallocates internal mappings.

    Public API behavior

    • Existing Python-facing rebuild methods now refresh their cached device argument structs. No signatures changed and no new API was added.

    Test coverage

    • New test_restriction_rebuild in warp/tests/fem/test_fem_integrate.py, registered on all detected devices, expands an explicit geometry partition and verifies that the restriction's cached NodeArg points to the rebuilt mapping.

    Behavioral verification

    Issue-shaped checks passed on CPU and CUDA: the original restriction scenario refreshed its mapping from 6 to 15 nodes, and NodePartition and EnvironmentSpacePartition both returned fresh cached argument structs after rebuild.

    Follow-up to consider

    • Add direct committed assertions for NodePartition and EnvironmentSpacePartition cache invalidation. The additional issue-shaped verification passed, so this does not block closure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingwarp.femwarp.fem module

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions