Skip to content

Commit e7c1cbd

Browse files
Add multi-RHS Kamino FK velocity solves (#3916)
Signed-off-by: hanxudong159 <42986190+han-xudong@users.noreply.github.com> Co-authored-by: Guirec-Maloisel <25688871+Guirec-Maloisel@users.noreply.github.com>
1 parent 25d555d commit e7c1cbd

5 files changed

Lines changed: 673 additions & 250 deletions

File tree

newton/_src/solvers/kamino/_src/linalg/factorize/llt_blocked_semi_sparse.py

Lines changed: 113 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,16 @@ def compute_inverse_ordering(ordering):
7676

7777

7878
@wp.kernel
79-
def reorder_rows_kernel(
79+
def reorder_rows_cols_kernel(
8080
src: wp.array3d[float],
8181
dst: wp.array3d[float],
8282
ordering: wp.array2d[int],
8383
n_rows_arr: wp.array[int],
8484
n_cols_arr: wp.array[int],
8585
batch_mask: wp.array[wp.bool],
8686
):
87-
batch_id, i, j = wp.tid() # 2D launch: (n_rows, n_cols)
87+
"""Apply a permutation to each matrix along its rows and columns."""
88+
batch_id, i, j = wp.tid()
8889
n_rows = n_rows_arr[batch_id]
8990
n_cols = n_cols_arr[batch_id]
9091
if i < n_rows and j < n_cols and batch_mask[batch_id]:
@@ -94,19 +95,19 @@ def reorder_rows_kernel(
9495

9596

9697
@wp.kernel
97-
def reorder_rows_kernel_col_vector(
98+
def reorder_rows_kernel(
9899
src: wp.array3d[float],
99100
dst: wp.array3d[float],
100101
ordering: wp.array2d[int],
101102
n_rows_arr: wp.array[int],
102103
batch_mask: wp.array[wp.bool],
103104
):
104-
batch_id, i = wp.tid()
105+
"""Apply a row permutation to each matrix of RHS column vectors."""
106+
batch_id, i, rhs_id = wp.tid()
105107
n_rows = n_rows_arr[batch_id]
106108
if i < n_rows and batch_mask[batch_id]:
107109
src_row = ordering[batch_id, i]
108-
# For column vectors (2d arrays with shape (n, 1)), just copy columns directly
109-
dst[batch_id, i, 0] = src[batch_id, src_row, 0]
110+
dst[batch_id, i, rhs_id] = src[batch_id, src_row, rhs_id]
110111

111112

112113
def to_binary_matrix(M):
@@ -320,7 +321,7 @@ def blocked_cholesky_solve_kernel(
320321
Uses forward/backward substitution with block size optimization.
321322
"""
322323

323-
batch_id, _tid_block = wp.tid()
324+
batch_id, rhs_id, _tid_block = wp.tid()
324325

325326
if not batch_mask[batch_id]:
326327
return
@@ -342,8 +343,7 @@ def blocked_cholesky_solve_kernel(
342343
if L_tile_pattern[tile_i, tile_i] == 0:
343344
continue
344345

345-
i_end = i + block_size
346-
rhs_tile = wp.tile_load(b, shape=(block_size, 1), offset=(i, 0))
346+
rhs_tile = wp.tile_load(b, shape=(block_size, 1), offset=(i, rhs_id))
347347
# Hoist the diagonal load above the j loop so the shared-memory fetch can
348348
# overlap with the gemm pipeline below.
349349
L_diag = wp.tile_load(L, shape=(block_size, block_size), offset=(i, i))
@@ -354,11 +354,11 @@ def blocked_cholesky_solve_kernel(
354354
if L_tile_pattern[tile_i, tile_j] == 0:
355355
continue
356356
L_block = wp.tile_load(L, shape=(block_size, block_size), offset=(i, j))
357-
y_block = wp.tile_load(y, shape=(block_size, 1), offset=(j, 0))
357+
y_block = wp.tile_load(y, shape=(block_size, 1), offset=(j, rhs_id))
358358
wp.tile_matmul(L_block, y_block, rhs_tile, alpha=-1.0)
359359
# equivalent to rhs_tile -= L_block * y_block, without intermediate allocation
360360
wp.tile_lower_solve_inplace(L_diag, rhs_tile) # In-place solve to avoid extra allocation
361-
wp.tile_store(y, rhs_tile, offset=(i, 0))
361+
wp.tile_store(y, rhs_tile, offset=(i, rhs_id))
362362

363363
# Backward substitution: solve L^T x = y
364364
for i in range(n - block_size, -1, -block_size):
@@ -369,7 +369,7 @@ def blocked_cholesky_solve_kernel(
369369

370370
i_start = i
371371
i_end = i_start + block_size
372-
rhs_tile = wp.tile_load(y, shape=(block_size, 1), offset=(i_start, 0))
372+
rhs_tile = wp.tile_load(y, shape=(block_size, 1), offset=(i_start, rhs_id))
373373
# Hoist the diagonal load above the j loop (see forward sub above).
374374
L_diag = wp.tile_load(L, shape=(block_size, block_size), offset=(i_start, i_start))
375375
if i_end < n:
@@ -379,15 +379,15 @@ def blocked_cholesky_solve_kernel(
379379
if L_tile_pattern[tile_j, tile_i] == 0:
380380
continue
381381
L_tile = wp.tile_load(L, shape=(block_size, block_size), offset=(j, i_start))
382-
x_tile = wp.tile_load(x, shape=(block_size, 1), offset=(j, 0))
382+
x_tile = wp.tile_load(x, shape=(block_size, 1), offset=(j, rhs_id))
383383
if wp.static(HAS_TILE_MATMUL_LEFT_TRANSPOSE_UPDATE):
384384
wp.tile_matmul_left_transpose_update(rhs_tile, L_tile, x_tile, alpha=-1.0)
385385
else:
386386
L_T_tile = wp.tile_transpose(L_tile)
387387
wp.tile_matmul(L_T_tile, x_tile, rhs_tile, alpha=-1.0)
388388
# equivalent to rhs_tile -= L_T_tile * x_tile, without intermediate allocation
389389
wp.tile_upper_solve_inplace(wp.tile_transpose(L_diag), rhs_tile) # In-place solve to avoid extra allocation
390-
wp.tile_store(x, rhs_tile, offset=(i_start, 0))
390+
wp.tile_store(x, rhs_tile, offset=(i_start, rhs_id))
391391

392392
return blocked_cholesky_solve_kernel
393393

@@ -399,10 +399,33 @@ class SemiSparseBlockCholeskySolverBatched:
399399
to skip unnecessary computation. Handles multiple systems in parallel with fixed matrix size.
400400
"""
401401

402-
def __init__(self, num_batches: int, max_num_equations: int, block_size=16, device="cuda", enable_reordering=True):
402+
def __init__(
403+
self,
404+
num_batches: int,
405+
max_num_equations: int,
406+
block_size: int = 16,
407+
device: wp.DeviceLike = "cuda",
408+
enable_reordering: bool = True,
409+
rhs_sizes: list[int] | None = None,
410+
):
411+
"""
412+
Initialize the solver given batch and matrix dimensions.
413+
414+
Args:
415+
num_batches: Batch dimension, i.e. number of systems to solve in parallel.
416+
max_num_equations: Maximal matrix size (i.e. n for a n x n matrix), among all systems.
417+
block_size: Block size for the blocked Cholesky factorization.
418+
device: Device on which to allocate the solver's data.
419+
enable_reordering: Whether to enable Cuthill-McKee ordering as part of the factorization.
420+
rhs_sizes: List of right-hand-side sizes for which to enable a solve (i.e. k for a n x k RHS).
421+
Defaults to [1] (i.e. single-column rhs) if not provided.
422+
More rhs sizes can be requested after construction with :meth:`request_rhs_size()`.
423+
Note that only uniform RHS sizes (across the batch dimension) are supported currently.
424+
"""
403425
self.num_batches = num_batches
404426
self.max_num_equations = max_num_equations
405427
self.device = device
428+
self.enable_reordering = enable_reordering
406429

407430
self.num_threads_per_block_factorize = 128
408431
self.num_threads_per_block_solve = 64
@@ -412,34 +435,46 @@ def __init__(self, num_batches: int, max_num_equations: int, block_size=16, devi
412435
self.cholesky_kernel = create_blocked_cholesky_kernel(block_size)
413436
self.solve_kernel = create_blocked_cholesky_solve_kernel(block_size)
414437

415-
# Allocate workspace arrays for factorization and solve
416438
# Compute padded size rounded up to next multiple of block size
417439
self.padded_num_equations = (
418440
(self.max_num_equations + self.block_size - 1) // self.block_size
419441
) * self.block_size
420442

443+
# Allocate workspace arrays for factorization and solve
421444
self.A_swizzled = wp.zeros(
422445
shape=(num_batches, self.padded_num_equations, self.padded_num_equations), dtype=float, device=self.device
423446
)
424447
self.L = wp.zeros(
425448
shape=(num_batches, self.padded_num_equations, self.padded_num_equations), dtype=float, device=self.device
426449
)
427-
self.y = wp.zeros(
428-
shape=(num_batches, self.padded_num_equations, 1), dtype=float, device=self.device
429-
) # temp memory
430-
self.result_swizzled = wp.zeros(
431-
shape=(num_batches, self.padded_num_equations, 1), dtype=float, device=self.device
432-
) # temp memory
433-
self.rhs_swizzled = wp.zeros(
434-
shape=(num_batches, self.padded_num_equations, 1), dtype=float, device=self.device
435-
) # temp memory
436-
437450
self.num_tiles = (self.padded_num_equations + self.block_size - 1) // self.block_size
438451
self.L_tile_pattern = wp.zeros(
439452
shape=(num_batches, self.num_tiles, self.num_tiles), dtype=int, device=self.device
440453
)
441454

442-
self.enable_reordering = enable_reordering
455+
# Allocate temporary buffers for requested RHS sizes
456+
self.temp_buffers: dict[int, tuple[wp.array3d[float], wp.array3d[float], wp.array3d[float]]] = {}
457+
""" Temporary buffers: (rhs_swizzled, result_swizzled, y) for each rhs size """
458+
rhs_sizes = [1] if rhs_sizes is None else rhs_sizes
459+
for rhs_size in rhs_sizes:
460+
self.request_rhs_size(rhs_size)
461+
462+
def request_rhs_size(self, rhs_size: int) -> None:
463+
"""
464+
Preallocate the necessary internal buffers for a solve with specified RHS size.
465+
466+
Must be called before the first call to :meth:`solve()` with this specific RHS size, if
467+
that size was not provided upon construction as part of ``rhs_sizes``.
468+
"""
469+
if rhs_size < 1:
470+
raise ValueError("rhs_size must be positive")
471+
if rhs_size not in self.temp_buffers:
472+
shape = (self.num_batches, self.padded_num_equations, rhs_size)
473+
self.temp_buffers[rhs_size] = (
474+
wp.zeros(shape=shape, dtype=float, device=self.device),
475+
wp.zeros(shape=shape, dtype=float, device=self.device),
476+
wp.zeros(shape=shape, dtype=float, device=self.device),
477+
)
443478

444479
def capture_sparsity_pattern(
445480
self,
@@ -539,7 +574,7 @@ def factorize(
539574
# Reorder A and store in self.A_reordered
540575
if self.enable_reordering:
541576
wp.launch(
542-
reorder_rows_kernel,
577+
reorder_rows_cols_kernel,
543578
dim=[self.num_batches, self.max_num_equations, self.max_num_equations],
544579
inputs=[
545580
A,
@@ -566,43 +601,77 @@ def solve(
566601
rhs: wp.array3d[float],
567602
result: wp.array3d[float],
568603
batch_mask: wp.array[wp.bool],
569-
):
604+
) -> None:
570605
"""
571606
Solves A x = b given the Cholesky factor L (A = L L^T) using
572607
blocked forward and backward substitution.
573608
574609
Args:
575-
rhs: Input right-hand-side matrices of shape (batch_size, n, p).
576-
result: Output solution matrices of shape (batch_size, n, p).
610+
rhs: Input right-hand-side matrices of shape (batch_size, n, rhs_size).
611+
Assumes rhs_size was requested beforehand, upon construction or with :meth:`request_rhs_size`.
612+
result: Output solution matrices of shape (batch_size, n, rhs_size).
577613
batch_mask: Boolean flag for each matrix in the batch, indicating whether to process it (False = skip)
614+
615+
Raises:
616+
ValueError: If shapes are incompatible or ``rhs_size`` was not requested.
578617
"""
618+
if rhs.shape != result.shape:
619+
raise ValueError("rhs and result must have identical shapes")
620+
if rhs.ndim != 3 or rhs.shape[0] != self.num_batches:
621+
raise ValueError("rhs must have shape (batch_size, n, rhs_size)")
622+
if rhs.shape[1] < self.max_num_equations:
623+
raise ValueError(f"rhs must have at least {self.max_num_equations} rows")
624+
625+
# Retrieve temporary buffers
626+
rhs_size = rhs.shape[2]
627+
buffers = self.temp_buffers.get(rhs_size)
628+
if buffers is None:
629+
raise ValueError(
630+
f"RHS size {rhs_size} was not preallocated; call request_rhs_size({rhs_size}) before solve"
631+
)
632+
rhs_swizzled, result_swizzled, y = buffers
579633

580-
R = result
634+
# Apply reordering
635+
solve_rhs = rhs
636+
solve_result = result
581637
if self.enable_reordering:
582-
R = self.result_swizzled
583638
wp.launch(
584-
reorder_rows_kernel_col_vector,
585-
dim=[self.num_batches, self.max_num_equations],
586-
inputs=[rhs, self.rhs_swizzled, self.ordering, self.num_active_equations, batch_mask],
639+
reorder_rows_kernel,
640+
dim=[self.num_batches, self.max_num_equations, rhs_size],
641+
inputs=[rhs, rhs_swizzled, self.ordering, self.num_active_equations, batch_mask],
587642
device=self.device,
588643
)
589-
590-
rhs = self.rhs_swizzled
644+
solve_rhs = rhs_swizzled
645+
solve_result = result_swizzled
591646

592647
# Then solve the system using blocked_cholesky_solve kernel
593648
wp.launch_tiled(
594649
self.solve_kernel,
595-
dim=self.num_batches,
596-
inputs=[self.L, self.L_tile_pattern, rhs, R, self.y, self.num_active_equations, batch_mask],
650+
dim=(self.num_batches, rhs_size),
651+
inputs=[
652+
self.L,
653+
self.L_tile_pattern,
654+
solve_rhs,
655+
solve_result,
656+
y,
657+
self.num_active_equations,
658+
batch_mask,
659+
],
597660
block_dim=self.num_threads_per_block_solve,
598661
device=self.device,
599662
)
600663

664+
# Undo reordering
601665
if self.enable_reordering:
602-
# Undo reordering
603666
wp.launch(
604-
reorder_rows_kernel_col_vector,
605-
dim=[self.num_batches, self.max_num_equations],
606-
inputs=[R, result, self.inverse_ordering, self.num_active_equations, batch_mask],
667+
reorder_rows_kernel,
668+
dim=[self.num_batches, self.max_num_equations, rhs_size],
669+
inputs=[
670+
solve_result,
671+
result,
672+
self.inverse_ordering,
673+
self.num_active_equations,
674+
batch_mask,
675+
],
607676
device=self.device,
608677
)

0 commit comments

Comments
 (0)