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