@@ -234,7 +234,8 @@ def fit_glm_fast(
234234 disp_glm = disp_glm , impute = impute , offset = offset ,
235235 shrinkage = shrinkage , alpha = alpha , maxiter = maxiter ,
236236 thres_disp = thres_disp , n_jobs = n_jobs ,
237- random_state = random_state , verbose = verbose , ** kwargs ,
237+ random_state = random_state , verbose = verbose ,
238+ mem_limit_gb = mem_limit_gb , ** kwargs ,
238239 )
239240
240241 # Estimate dispersion for NB — use covariate-only design (X, not X_full)
@@ -261,6 +262,7 @@ def fit_glm_fast(
261262 Y_float , X , A , a , d , p , n , family , disp_glm ,
262263 impute , X_test , offset_arr , offsets , maxiter , verbose ,
263264 mem_limit_gb = mem_limit_gb , offset_test = offset_test ,
265+ random_state = random_state ,
264266 )
265267
266268 B , Yhat , disp_glm_out , resid_deviance = _fit_glm_fast_single (
@@ -371,7 +373,7 @@ def _fit_glm_fast_single(
371373def _fit_glm_fast_per_perturbation (
372374 Y_float , X , A , a , d , p , n , family , disp_glm ,
373375 impute , X_test , offset_arr , offsets , maxiter , verbose ,
374- mem_limit_gb = None , offset_test = None ,
376+ mem_limit_gb = None , offset_test = None , random_state = 0 ,
375377):
376378 """Per-perturbation GLM fitting with global covariate model.
377379
@@ -417,7 +419,7 @@ def _fit_glm_fast_per_perturbation(
417419 if n <= _N_STAGE1 :
418420 stage1_mask = np .ones (n , dtype = bool )
419421 else :
420- rng = np .random .default_rng (0 )
422+ rng = np .random .default_rng (random_state )
421423 stage1_idx = np .sort (rng .choice (n , size = _N_STAGE1 , replace = False ))
422424 stage1_mask = np .zeros (n , dtype = bool )
423425 stage1_mask [stage1_idx ] = True
@@ -510,76 +512,75 @@ def _fit_glm_fast_per_perturbation(
510512 Yhat_0 = np .zeros ((n_test , p , a ), dtype = _impu_dtype )
511513 Yhat_1 = np .zeros ((n_test , p , a ), dtype = _impu_dtype )
512514
513- # ── Stage 2: Joint treatment-effect fit ──────────────────────────────
514- # Single fit with design=A (n, a) over all cells. For one-hot A the
515- # IRLS normal equations A^T W A are exactly block-diagonal: ctrl rows
516- # are all-zero so they contribute nothing to treatment coefficients, and
517- # each pert cell appears in exactly one column. Estimates are therefore
518- # mathematically equivalent to the old sequential per-pert loop, but
519- # Y_float is read from RAM once instead of a times (~8× memory-bandwidth
520- # reduction).
521- A_float = A .astype (np .float64 )
522-
523- if family == "nb" :
524- fitter_joint = NBGLMBatchFitter (
525- design = A_float ,
526- offset = offset_arr ,
527- max_iter = min (maxiter , 50 ),
528- poisson_init_iter = 5 ,
529- dispersion_method = "moments" ,
530- min_mu = _IRLS_MIN_MU ,
531- )
532- result_joint = fitter_joint .fit_batch_with_joint_offsets (
533- Y_float ,
534- covariate_offset = cov_offset_all ,
535- fixed_dispersion = global_alpha ,
536- )
537- elif family == "poisson" :
538- fitter_joint = NBGLMBatchFitter (
539- design = A_float ,
540- offset = offset_arr ,
541- max_iter = min (maxiter , 50 ),
542- poisson_init_iter = 10 ,
543- dispersion_method = "moments" ,
544- min_mu = _IRLS_MIN_MU ,
545- )
546- result_joint = fitter_joint .fit_batch_with_joint_offsets (
547- Y_float ,
548- covariate_offset = cov_offset_all ,
549- )
550-
551- B_trt_all = result_joint .coef # (p, a)
552- B_full [:, d :] = B_trt_all
553- joint_disp = result_joint .dispersion # (p,); equals global_alpha when fixed
554-
555- # Fitted values for all cells: ctrl rows recover Yhat_global exactly.
556- Yhat_joint = np .exp (np .clip (
557- A_float @ B_trt_all .T + cov_offset_all + offset_arr [:, None ], - 20 , 20
558- )) # (n, p)
559-
560- # Per-pert bookkeeping: overwrite treated rows, accumulate dispersion,
561- # fill imputation arrays. This loop is cheap — no GLM fitting.
515+ # ── Stage 2: treatment-effect fit ───────────────────────────────────
516+ # Future-dev note: a single joint fit over all treatment columns is only
517+ # equivalent to this loop when A is raw 0/1 one-hot. Some future callers
518+ # may pass standardized or otherwise transformed treatment designs, where
519+ # detecting one-hot structure from A itself would be unreliable. Keep the
520+ # historical per-perturbation GLM until raw assignment/support masks are
521+ # carried alongside any transformed design.
562522 for k in range (a ):
563523 pert_mask = A [:, k ] == 1
564- n_sub_k = int (ctrl_mask .sum ()) + int (pert_mask .sum ())
524+ cell_mask = ctrl_mask | pert_mask
525+ n_sub = cell_mask .sum ()
565526
566- Yhat_full [pert_mask ] = Yhat_joint [pert_mask ]
527+ A_k = A [cell_mask , k : k + 1 ]
528+ Y_sub = Y_float [cell_mask ]
529+ offset_sub = offset_arr [cell_mask ]
530+ cov_offset_sub = cov_offset_all [cell_mask ]
567531
568532 if family == "nb" :
569- resid_deviance [pert_mask ] = _compute_nb_deviance_residuals (
570- Y_float [pert_mask ], Yhat_joint [pert_mask ], joint_disp
533+ fitter = NBGLMBatchFitter (
534+ design = A_k ,
535+ offset = offset_sub ,
536+ max_iter = min (maxiter , 50 ),
537+ poisson_init_iter = 5 ,
538+ dispersion_method = "moments" ,
539+ min_mu = _IRLS_MIN_MU ,
571540 )
572- disp_alpha += n_sub_k * joint_disp
573- disp_alpha_cells += n_sub_k
574- else :
575- resid_deviance [pert_mask ] = _compute_poisson_deviance_residuals (
576- Y_float [pert_mask ], Yhat_joint [pert_mask ]
541+ result = fitter .fit_batch_with_joint_offsets (
542+ Y_sub ,
543+ covariate_offset = cov_offset_sub ,
544+ fixed_dispersion = global_alpha ,
545+ )
546+ B_trt_k = result .coef [:, 0 ]
547+
548+ eta_sub = A_k @ result .coef .T + cov_offset_sub + offset_sub [:, None ]
549+ Yhat_sub = np .exp (np .clip (eta_sub , - 20 , 20 ))
550+ fitter_disp = result .dispersion
551+ resid_sub = _compute_nb_deviance_residuals (Y_sub , Yhat_sub , fitter_disp )
552+ disp_alpha += n_sub * fitter_disp
553+ disp_alpha_cells += n_sub
554+
555+ elif family == "poisson" :
556+ fitter = NBGLMBatchFitter (
557+ design = A_k ,
558+ offset = offset_sub ,
559+ max_iter = min (maxiter , 50 ),
560+ poisson_init_iter = 10 ,
561+ dispersion_method = "moments" ,
562+ min_mu = _IRLS_MIN_MU ,
563+ )
564+ result = fitter .fit_batch_with_joint_offsets (
565+ Y_sub ,
566+ covariate_offset = cov_offset_sub ,
577567 )
568+ B_trt_k = result .coef [:, 0 ]
569+
570+ eta_sub = A_k @ result .coef .T + cov_offset_sub + offset_sub [:, None ]
571+ Yhat_sub = np .exp (np .clip (eta_sub , - 20 , 20 ))
572+ resid_sub = _compute_poisson_deviance_residuals (Y_sub , Yhat_sub )
573+
574+ B_full [:, d + k ] = B_trt_k
575+
576+ treated_in_sub = np .where (cell_mask )[0 ][A_k [:, 0 ] == 1 ]
577+ resid_deviance [treated_in_sub ] = resid_sub [A_k [:, 0 ] == 1 ]
578+ Yhat_full [treated_in_sub ] = Yhat_sub [A_k [:, 0 ] == 1 ]
578579
579580 if do_impute :
580581 Yhat_0 [:, :, k ] = Yhat_0_base
581- eta_1_test_k = eta_0_test + B_trt_all [:, k ] [None , :]
582- Yhat_1 [:, :, k ] = np .exp (np .clip (eta_1_test_k , - 20 , 20 ))
582+ eta_1_test = eta_0_test + B_trt_k [None , :]
583+ Yhat_1 [:, :, k ] = np .exp (np .clip (eta_1_test , - 20 , 20 ))
583584
584585 if family == "nb" :
585586 # Weighted average of alpha; convert to r = 1/alpha (causarray convention).
0 commit comments