Skip to content

Commit d5ec396

Browse files
authored
Merge pull request #99 from UT-Austin-RPL/scalar_loss
Inference-Time Value Computation
2 parents db6d5e7 + f75540f commit d5ec396

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

amago/agent.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

amago/experiment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ def load_checkpoint_from_path(
380380
self.policy.load_state_dict(ckpt)
381381
else:
382382
self.accelerator.load_state(path)
383+
self.policy.on_checkpoint_loaded(is_resume=is_accelerate_state)
383384

384385
def load_checkpoint(self, epoch: int, resume_training_state: bool = True) -> None:
385386
"""Load a historical checkpoint from the `ckpts` directory of this experiment.

0 commit comments

Comments
 (0)