streaming-deep-rl 0.2.4__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streaming-deep-rl
3
- Version: 0.2.4
3
+ Version: 0.2.8
4
4
  Summary: Streaming Deep Reinforcement Learning
5
5
  Project-URL: Homepage, https://pypi.org/project/streaming-deep-rl/
6
6
  Project-URL: Repository, https://codeberg.org/lucidrains/streaming-deep-rl
@@ -38,10 +38,9 @@ Requires-Dist: discrete-continuous-embed-readout>=0.2.0
38
38
  Requires-Dist: einops>=0.8.1
39
39
  Requires-Dist: einx>=0.3.0
40
40
  Requires-Dist: ema-pytorch
41
- Requires-Dist: hl-gauss-pytorch
42
41
  Requires-Dist: torch-einops-utils
43
42
  Requires-Dist: torch>=2.4
44
- Requires-Dist: x-mlps-pytorch>=0.2.8
43
+ Requires-Dist: x-mlps-pytorch>=0.3.3
45
44
  Provides-Extra: examples
46
45
  Provides-Extra: test
47
46
  Requires-Dist: gymnasium[box2d]>=1.0; extra == 'test'
@@ -95,7 +94,6 @@ agent = StreamingACLambda(
95
94
  dim_state = 8,
96
95
  dim_actor = 128,
97
96
  num_discrete_actions = 4,
98
- delay_steps = 7 # 7-step TD works well for me. next step TD tends to hit a performance wall
99
97
  )
100
98
 
101
99
  # get action from state and pass to environment or world model
@@ -120,6 +118,12 @@ agent.update(
120
118
  )
121
119
  ```
122
120
 
121
+ ## Cartpole
122
+
123
+ ```bash
124
+ $ uv run train_cartpole.py --spr
125
+ ```
126
+
123
127
  ## Lunar Lander
124
128
 
125
129
  ```bash
@@ -148,18 +152,6 @@ $ uv run train_lunar.py
148
152
  }
149
153
  ```
150
154
 
151
- ```bibtex
152
- @misc{daley2025averagingnstepreturnsreduces,
153
- title = {Averaging $n$-step Returns Reduces Variance in Reinforcement Learning},
154
- author = {Brett Daley and Martha White and Marlos C. Machado},
155
- year = {2025},
156
- eprint = {2402.03903},
157
- archivePrefix = {arXiv},
158
- primaryClass = {cs.LG},
159
- url = {https://arxiv.org/abs/2402.03903},
160
- }
161
- ```
162
-
163
155
  ```bibtex
164
156
  @misc{chen2026cautiousweightdecay,
165
157
  title = {Cautious Weight Decay},
@@ -232,12 +224,12 @@ $ uv run train_lunar.py
232
224
 
233
225
  ```bibtex
234
226
  @misc{maes2026leworldmodelstableendtoendjointembedding,
235
- title = {LeWorldModel: Stable End-to-End Joint-Embedding Predictive Architecture from Pixels},
227
+ title = {LeWorldModel: Stable End-to-End Joint-Embedding Predictive Architecture from Pixels},
236
228
  author = {Lucas Maes and Quentin Le Lidec and Damien Scieur and Yann LeCun and Randall Balestriero},
237
229
  year = {2026},
238
230
  eprint = {2603.19312},
239
231
  archivePrefix = {arXiv},
240
232
  primaryClass = {cs.LG},
241
- url = {https://arxiv.org/abs/2603.19312},
233
+ url = {https://arxiv.org/abs/2603.19312},
242
234
  }
243
235
  ```
@@ -45,7 +45,6 @@ agent = StreamingACLambda(
45
45
  dim_state = 8,
46
46
  dim_actor = 128,
47
47
  num_discrete_actions = 4,
48
- delay_steps = 7 # 7-step TD works well for me. next step TD tends to hit a performance wall
49
48
  )
50
49
 
51
50
  # get action from state and pass to environment or world model
@@ -70,6 +69,12 @@ agent.update(
70
69
  )
71
70
  ```
72
71
 
72
+ ## Cartpole
73
+
74
+ ```bash
75
+ $ uv run train_cartpole.py --spr
76
+ ```
77
+
73
78
  ## Lunar Lander
74
79
 
75
80
  ```bash
@@ -98,18 +103,6 @@ $ uv run train_lunar.py
98
103
  }
99
104
  ```
100
105
 
101
- ```bibtex
102
- @misc{daley2025averagingnstepreturnsreduces,
103
- title = {Averaging $n$-step Returns Reduces Variance in Reinforcement Learning},
104
- author = {Brett Daley and Martha White and Marlos C. Machado},
105
- year = {2025},
106
- eprint = {2402.03903},
107
- archivePrefix = {arXiv},
108
- primaryClass = {cs.LG},
109
- url = {https://arxiv.org/abs/2402.03903},
110
- }
111
- ```
112
-
113
106
  ```bibtex
114
107
  @misc{chen2026cautiousweightdecay,
115
108
  title = {Cautious Weight Decay},
@@ -182,12 +175,12 @@ $ uv run train_lunar.py
182
175
 
183
176
  ```bibtex
184
177
  @misc{maes2026leworldmodelstableendtoendjointembedding,
185
- title = {LeWorldModel: Stable End-to-End Joint-Embedding Predictive Architecture from Pixels},
178
+ title = {LeWorldModel: Stable End-to-End Joint-Embedding Predictive Architecture from Pixels},
186
179
  author = {Lucas Maes and Quentin Le Lidec and Damien Scieur and Yann LeCun and Randall Balestriero},
187
180
  year = {2026},
188
181
  eprint = {2603.19312},
189
182
  archivePrefix = {arXiv},
190
183
  primaryClass = {cs.LG},
191
- url = {https://arxiv.org/abs/2603.19312},
184
+ url = {https://arxiv.org/abs/2603.19312},
192
185
  }
193
186
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "streaming-deep-rl"
3
- version = "0.2.4"
3
+ version = "0.2.8"
4
4
  description = "Streaming Deep Reinforcement Learning"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
@@ -28,10 +28,9 @@ dependencies = [
28
28
  "einx>=0.3.0",
29
29
  "einops>=0.8.1",
30
30
  "ema-pytorch",
31
- "hl-gauss-pytorch",
32
31
  "torch>=2.4",
33
32
  "torch-einops-utils",
34
- "x-mlps-pytorch>=0.2.8",
33
+ "x-mlps-pytorch>=0.3.3",
35
34
  ]
36
35
 
37
36
  [project.urls]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
  import math
3
+ from pathlib import Path
3
4
  from collections import deque
4
5
  from contextlib import nullcontext
5
6
  from typing import Callable, NamedTuple
@@ -11,6 +12,7 @@ from torch.nn import Module, Linear, Sequential
11
12
 
12
13
  import einx
13
14
  from einops import rearrange, reduce
15
+ from einops.layers.torch import Rearrange
14
16
 
15
17
  from discrete_continuous_embed_readout import Readout, Embed
16
18
 
@@ -20,6 +22,8 @@ from ema_pytorch import EMA
20
22
 
21
23
  from hl_gauss_pytorch import HLGaussLayer
22
24
 
25
+ from x_mlps_pytorch import MLP
26
+
23
27
  from streaming_deep_rl.buffer_dict import BufferDict
24
28
 
25
29
  # helpers
@@ -30,6 +34,9 @@ def exists(v):
30
34
  def identity(t):
31
35
  return t
32
36
 
37
+ def first(t):
38
+ return t[0]
39
+
33
40
  def divisible_by(num, den):
34
41
  return (num % den) == 0
35
42
 
@@ -42,8 +49,9 @@ def no_grad_detach(fn):
42
49
  return fn(t).detach()
43
50
  return inner
44
51
 
45
- def cast_tensor(t):
46
- return tensor(t) if not is_tensor(t) else t
52
+ def cast_tensor(t, dtype = None, device = None):
53
+ t = tensor(t, dtype = dtype) if not is_tensor(t) else t
54
+ return t if not exists(device) else t.to(device)
47
55
 
48
56
  def to_device(tree, device):
49
57
  return tree_map_tensor(lambda t: t.to(device), tree)
@@ -96,18 +104,24 @@ def sparse_init_(
96
104
  # orthogonal projection for SPR in streaming setting
97
105
  # https://arxiv.org/abs/2602.09396
98
106
 
99
- def orthogonal_project(x, y):
100
- x, inverse = pack_with_inverse(x, '*')
107
+ def orthog_project(x, y):
108
+ x, inverse_pack = pack_with_inverse(x, '*')
101
109
  y, _ = pack_with_inverse(y, '*')
102
110
 
103
111
  dtype = x.dtype
104
- x, y = x.double(), y.double()
112
+
113
+ if x.device.type != 'mps':
114
+ x, y = x.double(), y.double()
115
+
105
116
  unit = l2norm(y)
106
117
 
107
118
  parallel = (x * unit).sum(dim = -1, keepdim = True) * unit
108
- orthogonal = x - parallel
119
+ orthog = x - parallel
120
+
121
+ orthog = inverse_pack(orthog, '*')
122
+
123
+ return orthog.to(dtype)
109
124
 
110
- return inverse(orthogonal).to(dtype)
111
125
 
112
126
  class SelfPredictRepr(Module):
113
127
  def __init__(
@@ -117,12 +131,14 @@ class SelfPredictRepr(Module):
117
131
  num_continuous_actions = 0,
118
132
  dim_predict = None,
119
133
  target_embed_from_ema = True, # whether the target embed is from an EMA, or from the model itself (traditional vs sigreg from lejepa)
120
- sigreg_weight = 0.
134
+ sigreg_weight = 0.,
135
+ dim_hidden_expand_factor = 4
121
136
  ):
122
137
  super().__init__()
123
138
  assert num_discrete_actions > 0 or num_continuous_actions > 0
124
139
 
125
140
  dim_predict = default(dim_predict, dim_embed)
141
+ dim_mlp_hidden = int(dim_embed * dim_hidden_expand_factor)
126
142
 
127
143
  self.to_action_embed = Embed(dim_embed, num_discrete = num_discrete_actions, num_continuous = num_continuous_actions)
128
144
 
@@ -130,12 +146,10 @@ class SelfPredictRepr(Module):
130
146
 
131
147
  self.to_projection = nn.Sequential(
132
148
  nn.RMSNorm(dim_embed),
133
- nn.Linear(dim_embed, dim_predict, bias = False)
149
+ MLP(dim_embed, dim_mlp_hidden, dim_predict)
134
150
  )
135
151
 
136
- self.to_prediction = nn.Sequential(
137
- nn.Linear(dim_predict + dim_embed, dim_predict, bias = False)
138
- )
152
+ self.to_prediction = MLP(dim_predict + dim_embed, dim_mlp_hidden, dim_predict)
139
153
 
140
154
  self.apply_sigreg = sigreg_weight > 0.
141
155
  self.sigreg_weight = sigreg_weight
@@ -386,34 +400,22 @@ class StreamingACLambda(Module):
386
400
  critic_self_predict_repr = False,
387
401
  spr_target_embed_from_ema = True,
388
402
  spr_sigreg_weight = 0.,
403
+ spr_dim_hidden_expand_factor = 4,
404
+ spr_lr = 1e-3,
405
+ spr_orth_beta = 0.9,
389
406
  entropy_weight = 0.01,
390
407
  init_sparsity = 0.9,
391
408
  dim_critic = 128,
392
- val_min = -3.,
393
- val_max = 3.,
394
- num_bins = 64,
395
409
  l2_weight_decay = 0.,
396
410
  l1_weight_decay = 0.,
397
411
  cautious_wd = False,
398
412
  wd_towards_init = False,
399
- delay_steps = 1,
400
- enable_pilar = False,
401
- pilar_mixing_param = 0.5,
402
413
  use_critic_ema = True,
403
414
  use_minto = False,
404
415
  use_delightful_pg = False,
405
416
  delightful_eta = 1.0
406
417
  ):
407
418
  super().__init__()
408
- assert delay_steps > 0, 'delay steps must be greater than 0'
409
-
410
- # delay buffer and pilar
411
-
412
- self.delay_steps = delay_steps
413
- self.delay_buffer = deque()
414
-
415
- self.enable_pilar = enable_pilar
416
- self.pilar_mixing_param = pilar_mixing_param
417
419
 
418
420
  # minto (use online network if you can)
419
421
 
@@ -435,17 +437,6 @@ class StreamingACLambda(Module):
435
437
  assert actor_embed.shape[-1] == dim_actor
436
438
  assert critic_embed.shape[-1] == dim_critic
437
439
 
438
- # hl-gauss
439
-
440
- self.hl_gauss_layer = HLGaussLayer(
441
- dim = dim_critic,
442
- hl_gauss_loss = dict(
443
- min_value = val_min,
444
- max_value = val_max,
445
- num_bins = num_bins,
446
- )
447
- )
448
-
449
440
  # state and reward normalization
450
441
 
451
442
  self.state_norm = ObservationNormalizer(dim_state)
@@ -458,40 +449,19 @@ class StreamingACLambda(Module):
458
449
  self.actor_self_predict_repr = actor_self_predict_repr
459
450
  self.critic_self_predict_repr = critic_self_predict_repr
460
451
 
461
- if actor_self_predict_repr:
462
-
463
- if spr_target_embed_from_ema:
464
- actor_use_ema |= True
465
-
466
- self.actor_spr = SelfPredictRepr(
467
- dim_actor,
468
- num_discrete_actions = num_discrete_actions,
469
- num_continuous_actions = num_continuous_actions,
470
- target_embed_from_ema = spr_target_embed_from_ema,
471
- sigreg_weight = spr_sigreg_weight
472
- )
473
-
474
- if critic_self_predict_repr:
475
-
476
- if spr_target_embed_from_ema:
477
- use_critic_ema |= True
478
-
479
- self.critic_spr = SelfPredictRepr(
480
- dim_critic,
481
- num_discrete_actions = num_discrete_actions,
482
- num_continuous_actions = num_continuous_actions,
483
- target_embed_from_ema = spr_target_embed_from_ema,
484
- sigreg_weight = spr_sigreg_weight
485
- )
452
+ self.spr_lr = spr_lr
453
+ self.spr_orth_beta = spr_orth_beta
486
454
 
487
455
  # actor
488
456
 
457
+ dim_readout_input = dim_actor
458
+
489
459
  self.actor = actor
490
460
 
491
461
  assert num_discrete_actions > 0 or num_continuous_actions > 0
492
462
 
493
463
  self.readout = Readout(
494
- dim_actor,
464
+ dim_readout_input,
495
465
  num_discrete = num_discrete_actions,
496
466
  num_continuous = num_continuous_actions
497
467
  )
@@ -499,16 +469,47 @@ class StreamingACLambda(Module):
499
469
  self.actor_with_readout = Sequential(actor, self.readout)
500
470
 
501
471
  self.actor_use_ema = actor_use_ema
502
- self.actor_with_readout_ema = EMA(self.actor_with_readout, beta = actor_ema_beta) if actor_use_ema else None
472
+ actor_has_ema = actor_use_ema or (actor_self_predict_repr and spr_target_embed_from_ema)
473
+ self.actor_with_readout_ema = EMA(self.actor_with_readout, beta = actor_ema_beta) if actor_has_ema else None
474
+
475
+ # linear layer for critic
476
+
477
+ self.critic_readout = Sequential(
478
+ Linear(dim_critic, 1),
479
+ Rearrange('... 1 -> ...')
480
+ )
503
481
 
504
482
  # critic
505
483
 
506
484
  self.critic = critic
507
485
 
508
- self.critic_full = Sequential(critic, self.hl_gauss_layer)
486
+ self.critic_full = Sequential(critic, self.critic_readout)
509
487
 
510
488
  self.use_critic_ema = use_critic_ema
511
- self.critic_ema = EMA(self.critic_full, beta = critic_ema_beta) if self.use_critic_ema else None
489
+ critic_has_ema = use_critic_ema or (critic_self_predict_repr and spr_target_embed_from_ema)
490
+ self.critic_ema = EMA(self.critic_full, beta = critic_ema_beta) if critic_has_ema else None
491
+
492
+ # self-predictive representations (Nilaksh et al. 2026)
493
+
494
+ if actor_self_predict_repr:
495
+ self.actor_spr = SelfPredictRepr(
496
+ dim_readout_input,
497
+ num_discrete_actions = num_discrete_actions,
498
+ num_continuous_actions = num_continuous_actions,
499
+ target_embed_from_ema = spr_target_embed_from_ema,
500
+ sigreg_weight = spr_sigreg_weight,
501
+ dim_hidden_expand_factor = spr_dim_hidden_expand_factor
502
+ )
503
+
504
+ if critic_self_predict_repr:
505
+ self.critic_spr = SelfPredictRepr(
506
+ dim_critic,
507
+ num_discrete_actions = num_discrete_actions,
508
+ num_continuous_actions = num_continuous_actions,
509
+ target_embed_from_ema = spr_target_embed_from_ema,
510
+ sigreg_weight = spr_sigreg_weight,
511
+ dim_hidden_expand_factor = spr_dim_hidden_expand_factor
512
+ )
512
513
 
513
514
  # td related
514
515
 
@@ -530,14 +531,21 @@ class StreamingACLambda(Module):
530
531
 
531
532
  if adaptive:
532
533
  self.register_buffer('step', tensor(0))
533
- self.actor_m = BufferDict({name: torch.zeros_like(param) for name, param in self.actor_with_readout.named_parameters()})
534
- self.critic_m = BufferDict({name: torch.zeros_like(param) for name, param in self.critic_full.named_parameters()})
535
-
536
534
  self.actor_v = BufferDict({name: torch.zeros_like(param) for name, param in self.actor_with_readout.named_parameters()})
537
535
  self.critic_v = BufferDict({name: torch.zeros_like(param) for name, param in self.critic_full.named_parameters()})
538
536
 
539
537
  self.eligibility_trace_decay = eligibility_trace_decay # lambda in paper
540
538
 
539
+ # spr gradient momentum for orth¹ decorrelation (Nilaksh et al. 2026)
540
+
541
+ if actor_self_predict_repr:
542
+ self.actor_spr_momentum = BufferDict({name: torch.zeros_like(param) for name, param in self.actor_with_readout.named_parameters()})
543
+ self.actor_spr_aux_momentum = BufferDict({str(i): torch.zeros_like(param) for i, param in enumerate(self.actor_spr.parameters())})
544
+
545
+ if critic_self_predict_repr:
546
+ self.critic_spr_momentum = BufferDict({name: torch.zeros_like(param) for name, param in self.critic_full.named_parameters()})
547
+ self.critic_spr_aux_momentum = BufferDict({str(i): torch.zeros_like(param) for i, param in enumerate(self.critic_spr.parameters())})
548
+
541
549
  # adaptive step related (obgd)
542
550
 
543
551
  self.actor_kappa = actor_kappa
@@ -575,6 +583,16 @@ class StreamingACLambda(Module):
575
583
 
576
584
  # set actor and critic lambda if eligibility trace is used
577
585
 
586
+ def save(self, path, overwrite = True):
587
+ path = Path(path)
588
+ assert overwrite or not path.exists(), f"File {path} already exists"
589
+ torch.save(self.state_dict(), str(path))
590
+
591
+ def load(self, path):
592
+ path = Path(path)
593
+ assert path.exists(), f"File {path} does not exist"
594
+ self.load_state_dict(torch.load(str(path)))
595
+
578
596
  def init_(self, module):
579
597
  if not isinstance(module, Linear):
580
598
  return
@@ -591,79 +609,50 @@ class StreamingACLambda(Module):
591
609
  if self.adaptive:
592
610
  self.step.zero_()
593
611
 
594
- for m in self.actor_m.values():
595
- m.zero_()
596
-
597
- for m in self.critic_m.values():
598
- m.zero_()
599
-
600
612
  for v in self.actor_v.values():
601
613
  v.zero_()
602
614
 
603
615
  for v in self.critic_v.values():
604
616
  v.zero_()
605
617
 
618
+ if self.actor_self_predict_repr:
619
+ for m in self.actor_spr_momentum.values():
620
+ m.zero_()
621
+ for m in self.actor_spr_aux_momentum.values():
622
+ m.zero_()
623
+
624
+ if self.critic_self_predict_repr:
625
+ for m in self.critic_spr_momentum.values():
626
+ m.zero_()
627
+ for m in self.critic_spr_aux_momentum.values():
628
+ m.zero_()
629
+
606
630
  def update(
607
631
  self,
608
632
  state,
609
633
  action,
610
634
  next_state,
611
635
  reward,
612
- is_terminal = False,
613
- delay_steps = None,
614
- drain = False
636
+ is_terminal = False
615
637
  ):
616
- reward = cast_tensor(reward)
617
- is_terminal = cast_tensor(is_terminal)
638
+ device = state.device
639
+ action = cast_tensor(action, device = device)
640
+ reward = cast_tensor(reward, dtype = torch.float32, device = device)
641
+ is_terminal = cast_tensor(is_terminal, dtype = torch.bool, device = device)
618
642
 
619
643
  state = self.state_norm(state, update = True)
620
644
  next_state = self.state_norm(next_state, update = False)
621
645
 
622
646
  normed_reward = self.reward_norm(reward, is_terminal = is_terminal, update = True)
623
647
 
624
- self.delay_buffer.append((state, action, normed_reward, next_state, is_terminal))
625
-
626
- # process all pending transitions in the buffer
627
-
628
- delay_steps = default(delay_steps, self.delay_steps)
629
-
630
- should_learn = len(self.delay_buffer) >= delay_steps or is_terminal.item() or drain
631
-
632
- if not should_learn:
633
- return UpdateMetrics(0., 0., 0., 0., 0., 0., 0., 0.)
634
-
635
- # process all pending transitions in the buffer
636
-
637
- metrics = None
638
-
639
- while len(self.delay_buffer) > 0:
640
- metrics = self._learn_step(list(self.delay_buffer))
641
- self.delay_buffer.popleft()
642
-
643
- if not is_terminal.item() and not drain:
644
- break
648
+ metrics = self._learn_step((state, action, normed_reward, next_state, is_terminal))
645
649
 
646
650
  return metrics
647
651
 
648
- def _learn_step(self, buffer_slice):
649
- """single learning step for the oldest transition in buffer_slice, using n-step return"""
650
-
651
- oldest_state, oldest_action, oldest_reward, oldest_next_state, oldest_is_term = buffer_slice[0]
652
- last_next_state = buffer_slice[-1][3]
653
-
654
- # n-step discounted return
652
+ def _learn_step(self, transition):
653
+ """single learning step for a transition, using 1-step return"""
655
654
 
656
- n_step_return = 0.
657
- discount = 1.0
658
-
659
- for _, _, reward, _, is_term in buffer_slice:
660
- n_step_return = n_step_return + discount * reward
661
-
662
- if is_term.item():
663
- discount = 0.
664
- break
665
-
666
- discount *= self.discount_factor
655
+ state, action, reward, next_state, is_term = transition
667
656
 
668
657
  # all gradient related
669
658
 
@@ -671,8 +660,7 @@ class StreamingACLambda(Module):
671
660
 
672
661
  # critic
673
662
 
674
- embed = self.critic(oldest_state)
675
- value_pred = self.hl_gauss_layer(embed)
663
+ value_pred = self.critic_full(state)
676
664
 
677
665
  # getting the bootstrapped value
678
666
 
@@ -682,53 +670,40 @@ class StreamingACLambda(Module):
682
670
  if not self.use_minto:
683
671
  return next_value
684
672
 
685
- # Hendawy et al. https://openreview.net/forum?id=rFLuaG9Yq6
686
-
687
673
  online_next_value = self.critic_full(state)
688
674
  return torch.min(next_value, online_next_value)
689
675
 
690
- # n-step td target
676
+ # 1-step td target
691
677
 
692
- if discount > 0.:
693
- td_target = (n_step_return + discount * get_next_value_pred(last_next_state)).detach()
678
+ if not is_term.item():
679
+ td_target = (reward + self.discount_factor * get_next_value_pred(next_state)).detach()
694
680
  else:
695
- td_target = n_step_return.detach()
696
-
697
- # pilar - average of 1-step and n-step td targets
698
- # https://arxiv.org/abs/2402.03903
699
-
700
- if self.enable_pilar and len(buffer_slice) > 1:
701
- if oldest_is_term.item():
702
- td_target_1 = oldest_reward.detach()
703
- else:
704
- td_target_1 = (oldest_reward + self.discount_factor * get_next_value_pred(oldest_next_state)).detach()
681
+ td_target = reward.detach()
705
682
 
706
- td_target = torch.lerp(td_target_1, td_target, self.pilar_mixing_param)
707
-
708
- value_loss = self.hl_gauss_layer(embed, td_target)
683
+ # critic mse
709
684
 
685
+ # The critic trace accumulates the gradient of the value prediction (scalar output).
686
+ # We differentiate +value_pred so that when added with delta, it moves the value towards target.
710
687
  critic_params = list(self.critic_full.parameters())
711
- critic_loss_grads = torch.autograd.grad(value_loss, critic_params)
688
+ critic_trace_grads = torch.autograd.grad(value_pred, critic_params, retain_graph=True)
712
689
 
713
690
  td_error = td_target - value_pred
714
691
  td_error_sign = td_error.detach().sign()
715
692
 
716
- # ObGD pseudo-gradient
717
-
718
- safe_delta = torch.where(td_error.detach() >= 0, 1.0, -1.0) * td_error.detach().abs().clamp(min = 1e-6)
719
-
720
693
  value_grad = {
721
- name: -grad / safe_delta
722
- for (name, _), grad in zip(self.critic_full.named_parameters(), critic_loss_grads)
694
+ name: grad
695
+ for (name, _), grad in zip(self.critic_full.named_parameters(), critic_trace_grads)
723
696
  }
724
697
 
725
698
  # actor with entropy regularization
726
699
 
727
- action_logits = self.actor_with_readout(oldest_state)
728
- log_prob = self.readout.log_prob(action_logits, oldest_action).mean()
700
+ action_logits = self.actor_with_readout(state)
701
+ log_prob = self.readout.log_prob(action_logits, action).mean()
729
702
  entropy = self.readout.entropy(action_logits).mean()
730
703
 
731
- actor_loss = log_prob
704
+ # Combine entropy into the actor trace, modulated by sign(td_error)
705
+
706
+ actor_loss = log_prob + self.entropy_weight * td_error_sign * entropy
732
707
 
733
708
  actor_params = list(self.actor_with_readout.parameters())
734
709
 
@@ -741,54 +716,85 @@ class StreamingACLambda(Module):
741
716
 
742
717
  actor_spr_grad = None
743
718
  if self.actor_self_predict_repr:
744
- actor_embed = self.actor(oldest_state)
719
+ actor_embed = self.actor(state)
745
720
 
746
- if self.actor_use_ema:
747
- actor_next_embed = self.actor_with_readout_ema.ema_model[0](last_next_state)
721
+ if self.actor_spr.target_embed_from_ema:
722
+ assert exists(self.actor_with_readout_ema), 'actor ema must be available if target_embed_from_ema is True'
723
+ actor_next_embed = first(self.actor_with_readout_ema.ema_model)(next_state)
748
724
  else:
749
- actor_next_embed = self.actor(last_next_state)
725
+ actor_next_embed = self.actor(next_state)
750
726
 
751
- spr_loss = self.actor_spr(actor_embed, oldest_action, actor_next_embed)
727
+ spr_loss = self.actor_spr(actor_embed, action, actor_next_embed)
752
728
 
753
729
  spr_params = list(self.actor_spr.parameters())
754
730
  spr_grads_tup = torch.autograd.grad(spr_loss, actor_params + spr_params, retain_graph = True, allow_unused = True)
755
731
  spr_grads_tup = tuple(default(g, torch.zeros_like(p)) for g, p in zip(spr_grads_tup, actor_params + spr_params))
756
732
 
757
733
  actor_spr_grad = {name: grad for (name, _), grad in zip(self.actor_with_readout.named_parameters(), spr_grads_tup[:len(actor_params)])}
758
- spr_only_grad = spr_grads_tup[len(actor_params):]
734
+ spr_only_grad = list(spr_grads_tup[len(actor_params):])
735
+
736
+ # orth¹: project SPR encoder grads orthogonal to their own EMA momentum
737
+ # this decorrelates the highly correlated streaming data
738
+
739
+ actor_names = list(actor_spr_grad.keys())
740
+ all_grads = [actor_spr_grad[n] for n in actor_names] + spr_only_grad
741
+ all_moms = [self.actor_spr_momentum[n] for n in actor_names] + list(self.actor_spr_aux_momentum.values())
759
742
 
760
- # Update SPR networks via SGD
743
+ all_grads_proj = [orthog_project(g, m) for g, m in zip(all_grads, all_moms)]
744
+
745
+ for grad, mom in zip(all_grads, all_moms):
746
+ mom.mul_(self.spr_orth_beta).add_(grad, alpha = 1. - self.spr_orth_beta)
747
+
748
+ for name, proj_grad in zip(actor_names, all_grads_proj[:len(actor_names)]):
749
+ actor_spr_grad[name] = proj_grad
750
+
751
+ spr_only_grad = all_grads_proj[len(actor_names):]
752
+
753
+ # Update SPR auxiliary networks via SGD with dedicated spr_lr
761
754
  for param, grad in zip(spr_params, spr_only_grad):
762
- param.data.add_(grad, alpha = -self.actor_lr)
755
+ param.data.add_(grad, alpha = -self.spr_lr)
763
756
 
764
757
  # critic SPR loss
765
758
 
766
759
  critic_spr_grad = None
767
760
  if self.critic_self_predict_repr:
768
- critic_embed = self.critic(oldest_state)
761
+ critic_embed = self.critic(state)
769
762
 
770
- if self.use_critic_ema:
771
- critic_next_embed = self.critic_ema.ema_model[0](last_next_state)
763
+ if self.critic_spr.target_embed_from_ema:
764
+ assert exists(self.critic_ema), 'critic ema must be available if target_embed_from_ema is True'
765
+ critic_next_embed = first(self.critic_ema.ema_model)(next_state)
772
766
  else:
773
- critic_next_embed = self.critic(last_next_state)
767
+ critic_next_embed = self.critic(next_state)
774
768
 
775
- critic_spr_loss = self.critic_spr(critic_embed, oldest_action, critic_next_embed)
769
+ critic_spr_loss = self.critic_spr(critic_embed, action, critic_next_embed)
776
770
 
777
771
  critic_spr_params = list(self.critic_spr.parameters())
778
772
  critic_spr_grads_tup = torch.autograd.grad(critic_spr_loss, critic_params + critic_spr_params, retain_graph = True, allow_unused = True)
779
773
  critic_spr_grads_tup = tuple(default(g, torch.zeros_like(p)) for g, p in zip(critic_spr_grads_tup, critic_params + critic_spr_params))
780
774
 
781
775
  critic_spr_grad = {name: grad for (name, _), grad in zip(self.critic_full.named_parameters(), critic_spr_grads_tup[:len(critic_params)])}
782
- critic_spr_only_grad = critic_spr_grads_tup[len(critic_params):]
776
+ critic_spr_only_grad = list(critic_spr_grads_tup[len(critic_params):])
783
777
 
784
- # Update SPR networks via SGD
785
- for param, grad in zip(critic_spr_params, critic_spr_only_grad):
786
- param.data.add_(grad, alpha = -self.critic_lr)
778
+ # project SPR encoder grads orthogonal to their own EMA momentum
779
+
780
+ critic_names = list(critic_spr_grad.keys())
781
+ all_grads = [critic_spr_grad[n] for n in critic_names] + critic_spr_only_grad
782
+ all_moms = [self.critic_spr_momentum[n] for n in critic_names] + list(self.critic_spr_aux_momentum.values())
783
+
784
+ all_grads_proj = [orthog_project(g, m) for g, m in zip(all_grads, all_moms)]
787
785
 
788
- # entropy gradient decoupled from td_error
786
+ for grad, mom in zip(all_grads, all_moms):
787
+ mom.mul_(self.spr_orth_beta).add_(grad, alpha = 1. - self.spr_orth_beta)
789
788
 
790
- entropy_grads_tup = torch.autograd.grad(entropy, actor_params, retain_graph = True)
791
- entropy_grad = {name: grad for (name, _), grad in zip(self.actor_with_readout.named_parameters(), entropy_grads_tup)}
789
+ for name, proj_grad in zip(critic_names, all_grads_proj[:len(critic_names)]):
790
+ critic_spr_grad[name] = proj_grad
791
+
792
+ critic_spr_only_grad = all_grads_proj[len(critic_names):]
793
+
794
+ # Update SPR auxiliary networks via SGD with dedicated spr_lr
795
+
796
+ for param, grad in zip(critic_spr_params, critic_spr_only_grad):
797
+ param.data.add_(grad, alpha = -self.spr_lr)
792
798
 
793
799
  td_error = td_error.detach().squeeze()
794
800
  td_error_sign = td_error_sign.detach().squeeze()
@@ -799,7 +805,7 @@ class StreamingACLambda(Module):
799
805
  gate = 1.
800
806
  if self.use_delightful_pg:
801
807
  surprisal = -log_prob.detach()
802
- delight = td_error * surprisal
808
+ delight = td_error.detach() * surprisal
803
809
  gate = (delight / self.delightful_eta).sigmoid()
804
810
 
805
811
  # update eligibility traces
@@ -816,24 +822,21 @@ class StreamingACLambda(Module):
816
822
 
817
823
  td_error_factor = td_error.abs().clamp(min = 1.)
818
824
 
819
- actor_grad_norm = torch.stack([g.norm(p=1) for g in actor_grad.values()]).mean()
820
- critic_grad_norm = torch.stack([g.norm(p=1) for g in value_grad.values()]).mean()
825
+ actor_grad_norm = torch.stack([g.norm(p = 1) for g in actor_grad.values()]).mean()
826
+ critic_grad_norm = torch.stack([g.norm(p = 1) for g in value_grad.values()]).mean()
821
827
 
822
828
  if self.adaptive:
823
829
  self.step.add_(1)
824
830
  step = self.step.item()
825
- bias_correction1 = 1. - self.adam_beta1 ** step
826
831
  bias_correction2 = 1. - self.adam_beta2 ** step
827
832
 
828
- def update_params(params, init_params, traces, ms, vs, kappa, lr, l2_wd, has_l2_wd, l1_wd, has_l1_wd, has_any_wd, cautious_wd, wd_towards_init, aux_grads = None, spr_grads = None):
833
+ def update_params(params, init_params, traces, vs, kappa, lr, l2_wd, has_l2_wd, l1_wd, has_l1_wd, has_any_wd, cautious_wd, wd_towards_init, spr_grads = None):
829
834
  if self.adaptive:
830
- # update first and second moments of semi-gradients
835
+ # update second moment of semi-gradients
831
836
 
832
837
  for name, trace in traces.items():
833
- m, v = ms[name], vs[name]
838
+ v = vs[name]
834
839
  grad = td_error * trace
835
-
836
- m.lerp_(grad, 1. - self.adam_beta1)
837
840
  v.mul_(self.adam_beta2).add_(grad ** 2, alpha = 1. - self.adam_beta2)
838
841
 
839
842
  # adam update direction + obgd bound (Algorithm 11)
@@ -845,8 +848,7 @@ class StreamingACLambda(Module):
845
848
  v_hat = vs[name] / bias_correction2
846
849
  denom = v_hat.sqrt() + self.eps
847
850
 
848
- m_hat = ms[name] / bias_correction1
849
- adapted_grads[name] = m_hat / denom
851
+ adapted_grads[name] = td_error * trace / denom
850
852
 
851
853
  trace_sum += (trace / denom).abs().sum()
852
854
  else:
@@ -861,12 +863,9 @@ class StreamingACLambda(Module):
861
863
  for name, param in params.named_parameters():
862
864
  update = adapted_grads[name] * global_scale * lr
863
865
 
864
- if exists(aux_grads):
865
- update = update + aux_grads[name] * self.entropy_weight * global_scale * lr
866
-
867
866
  if exists(spr_grads) and name in spr_grads:
868
867
  spr_update = -spr_grads[name] * lr
869
- spr_update = orthogonal_project(spr_update, update)
868
+ spr_update = orthog_project(spr_update, update)
870
869
  update = update + spr_update
871
870
 
872
871
  # add gradient update first
@@ -901,7 +900,6 @@ class StreamingACLambda(Module):
901
900
  self.actor_with_readout,
902
901
  self.init_params['actor'] if exists(self.init_params) else None,
903
902
  self.actor_trace,
904
- self.actor_m if self.adaptive else None,
905
903
  self.actor_v if self.adaptive else None,
906
904
  self.actor_kappa,
907
905
  self.actor_lr,
@@ -912,15 +910,13 @@ class StreamingACLambda(Module):
912
910
  self.has_any_wd,
913
911
  self.cautious_wd,
914
912
  self.wd_towards_init,
915
- entropy_grad,
916
- actor_spr_grad
913
+ spr_grads = actor_spr_grad
917
914
  )
918
915
 
919
916
  critic_trace_norm, critic_scale = update_params(
920
917
  self.critic_full,
921
918
  self.init_params['critic'] if exists(self.init_params) else None,
922
919
  self.critic_trace,
923
- self.critic_m if self.adaptive else None,
924
920
  self.critic_v if self.adaptive else None,
925
921
  self.critic_kappa,
926
922
  self.critic_lr,
@@ -931,8 +927,7 @@ class StreamingACLambda(Module):
931
927
  self.has_any_wd,
932
928
  self.cautious_wd,
933
929
  self.wd_towards_init,
934
- None,
935
- critic_spr_grad
930
+ spr_grads = critic_spr_grad
936
931
  )
937
932
 
938
933
  if self.actor_use_ema: