@@ -473,6 +473,18 @@ def edit_critic_mask(
473473 """
474474 return pad_mask
475475
476+ def on_checkpoint_loaded (self , is_resume : bool = False ):
477+ """Called after checkpoint weights have been loaded into this agent.
478+
479+ Override to snapshot parameters, build frozen copies, etc.
480+
481+ Args:
482+ is_resume: True when restoring a full accelerate training state
483+ (i.e. continuing an in-progress run). False when loading raw
484+ policy weights for a new run or inference.
485+ """
486+ pass
487+
476488
477489@gin .configurable
478490@register_agent ("agent" )
@@ -754,6 +766,75 @@ def get_actions(
754766 dtype = torch .uint8 if (self .discrete or self .multibinary ) else torch .float32
755767 return actions .to (dtype = dtype ), hidden_state
756768
769+ @torch .no_grad ()
770+ def get_values (self , batch : Batch ) -> dict [str , torch .Tensor ]:
771+ """Inference-time computation of Q, V, advantage, and filter weights.
772+
773+ Mirrors the advantage computation in `forward` but without computing
774+ any losses or updating any state. Useful for offline analysis of
775+ what the critic thinks about a batch of data.
776+
777+ Args:
778+ batch: A collated Batch of trajectories.
779+
780+ Returns:
781+ Dict with keys:
782+ - q_sa: Q(s, a_observed), mean over critic ensemble. (B, L-1, G, 1)
783+ - v_s: V(s) = E_pi[Q(s, a~pi)], mean over samples and ensemble. (B, L-1, G, 1)
784+ - advantage: A(s,a) = Q(s,a) - V(s). (B, L-1, G, 1)
785+ - filter_weights: f(A(s,a)) from fbc_filter_func. (B, L-1, G, 1)
786+ - state_mask: Valid timestep mask. (B, L-1, 1)
787+ """
788+ o = self .tstep_encoder (obs = batch .obs , rl2s = batch .rl2s )
789+ straight_from_obs = {k : batch .obs [k ] for k in self .pass_obs_keys_to_actor }
790+ B , L , D_o = o .shape
791+ a = batch .actions
792+ a = a .clamp (0 , 1.0 ) if self .discrete else a .clamp (- 1.0 , 1.0 )
793+ _B , _L , D_action = a .shape
794+ G = len (self .gammas )
795+ K_a = self .num_actions_for_value_in_actor_loss if not self .discrete else 1
796+ a_buffer = F .pad (a , (0 , 0 , 0 , 1 ), "replicate" )
797+ a_buffer = repeat (a_buffer , f"b l a -> b l { G } a" )
798+ state_mask = (~ ((batch .rl2s == self .pad_val ).all (- 1 , keepdim = True ))).bool ()[
799+ :, 1 :, ...
800+ ]
801+
802+ s_rep , _ = self .traj_encoder (
803+ seq = o , time_idxs = batch .time_idxs , hidden_state = None
804+ )
805+
806+ a_dist = self .actor (s_rep , straight_from_obs = straight_from_obs )
807+
808+ # Q(s, a_observed) — raw critic output, (B, L, C, G, 1)
809+ q_sa_raw = self .critics (s_rep , a_buffer .unsqueeze (0 )).mean (0 )
810+ # V(s) = E_pi[Q(s, a~pi)] — raw critic output
811+ a_agent = self ._sample_k_actions (a_dist , k = K_a )
812+ v_s_raw = self .critics (s_rep .detach (), a_agent ).mean (0 )
813+
814+ # Filter in popart-normalized space (stable, scale-invariant to reward shifts)
815+ q_sa_norm = self .popart (q_sa_raw )[:, :- 1 , ...].mean (2 )
816+ v_s_norm = self .popart (v_s_raw )[:, :- 1 , ...].mean (2 )
817+ filter_weights = self .fbc_filter_func (q_sa_norm - v_s_norm ).float ()
818+
819+ # Q, V, advantage in denormalized reward scale for interpretability
820+ q_denorm = self .popart (q_sa_raw , normalized = False )[:, :- 1 , ...]
821+ v_denorm = self .popart (v_s_raw , normalized = False )[:, :- 1 , ...]
822+ q_sa = q_denorm .mean (2 )
823+ q_sa_std = q_denorm .std (2 )
824+ v_s = v_denorm .mean (2 )
825+ v_s_std = v_denorm .std (2 )
826+ advantage = q_sa - v_s
827+
828+ return {
829+ "q_sa" : q_sa ,
830+ "v_s" : v_s ,
831+ "advantage" : advantage ,
832+ "filter_weights" : filter_weights ,
833+ "state_mask" : state_mask ,
834+ "q_sa_std" : q_sa_std ,
835+ "v_s_std" : v_s_std ,
836+ }
837+
757838 def _reduce_critic_ensemble (self , q_ensemble : torch .Tensor ) -> torch .Tensor :
758839 B , L , C , G , _ = q_ensemble .shape
759840 # random subset of critic ensemble
@@ -1190,6 +1271,75 @@ def __init__(
11901271 def _sample_k_actions (self , dist , k : int ):
11911272 raise NotImplementedError
11921273
1274+ @torch .no_grad ()
1275+ def get_values (self , batch : Batch ) -> dict [str , torch .Tensor ]:
1276+ """Inference-time computation of Q, V, advantage, and filter weights.
1277+
1278+ MultiTaskAgent override that uses bin-distribution critics
1279+ (NCriticsTwoHot) instead of scalar critics.
1280+
1281+ Args:
1282+ batch: A collated Batch of trajectories.
1283+
1284+ Returns:
1285+ Dict with keys:
1286+ - q_sa: Q(s, a_observed), mean over critic ensemble. (B, L-1, G, 1)
1287+ - v_s: V(s) = E_pi[Q(s, a~pi)], mean over samples and ensemble. (B, L-1, G, 1)
1288+ - advantage: A(s,a) = Q(s,a) - V(s). (B, L-1, G, 1)
1289+ - filter_weights: f(A(s,a)) from fbc_filter_func. (B, L-1, G, 1)
1290+ - state_mask: Valid timestep mask. (B, L-1, 1)
1291+ """
1292+ o = self .tstep_encoder (obs = batch .obs , rl2s = batch .rl2s )
1293+ straight_from_obs = {k : batch .obs [k ] for k in self .pass_obs_keys_to_actor }
1294+ B , L , D_o = o .shape
1295+ a = batch .actions
1296+ a = a .clamp (0 , 1.0 ) if self .discrete else a .clamp (- 1.0 , 1.0 )
1297+ _B , _L , D_action = a .shape
1298+ G = len (self .gammas )
1299+ K_a = self .num_actions_for_value_in_actor_loss
1300+ a_buffer = F .pad (a , (0 , 0 , 0 , 1 ), "replicate" )
1301+ a_buffer = repeat (a_buffer , f"b l a -> b l { G } a" )
1302+ state_mask = (~ ((batch .rl2s == self .pad_val ).all (- 1 , keepdim = True ))).bool ()[
1303+ :, 1 :, ...
1304+ ]
1305+
1306+ s_rep , _ = self .traj_encoder (
1307+ seq = o , time_idxs = batch .time_idxs , hidden_state = None
1308+ )
1309+
1310+ a_dist = self .actor (s_rep , straight_from_obs = straight_from_obs )
1311+ if self .discrete :
1312+ a_dist = DiscreteLikeContinuous (a_dist )
1313+
1314+ # Q(s, a_observed) via bin distribution critics
1315+ q_dist = self .critics (s_rep , a_buffer .unsqueeze (0 ))
1316+ scalar_q = self .critics .bin_dist_to_raw_vals (q_dist ).squeeze (0 )
1317+ # scalar_q: (B, L, C, G, 1) — C is critic ensemble dim
1318+ q_sa = scalar_q [:, :- 1 , ...].mean (2 )
1319+ q_sa_std = scalar_q [:, :- 1 , ...].std (2 )
1320+
1321+ # V(s) = E_pi[Q(s, a~pi)] via K_a sampled actions
1322+ a_agent = a_dist .sample ((K_a ,))
1323+ q_v_dist = self .critics (s_rep .detach (), a_agent )
1324+ val_s = self .critics .bin_dist_to_raw_vals (q_v_dist )
1325+ # val_s: (K_a, B, L, C, G, 1) — mean over samples (0), keep ensemble (3)
1326+ val_s_per_critic = val_s [:, :, :- 1 , ...].mean (0 ) # (B, L-1, C, G, 1)
1327+ v_s = val_s_per_critic .mean (2 )
1328+ v_s_std = val_s_per_critic .std (2 )
1329+
1330+ advantage = q_sa - v_s
1331+ filter_weights = self .fbc_filter_func (advantage ).float ()
1332+
1333+ return {
1334+ "q_sa" : q_sa ,
1335+ "v_s" : v_s ,
1336+ "advantage" : advantage ,
1337+ "filter_weights" : filter_weights ,
1338+ "state_mask" : state_mask ,
1339+ "q_sa_std" : q_sa_std ,
1340+ "v_s_std" : v_s_std ,
1341+ }
1342+
11931343 def forward (self , batch : Batch , log_step : bool ):
11941344 # fmt: off
11951345 self .update_info = {} # holds wandb stats
0 commit comments