@@ -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
112113def 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