x-evolution 0.1.34__tar.gz → 0.1.35__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.
- {x_evolution-0.1.34 → x_evolution-0.1.35}/PKG-INFO +10 -1
- {x_evolution-0.1.34 → x_evolution-0.1.35}/README.md +9 -0
- {x_evolution-0.1.34 → x_evolution-0.1.35}/pyproject.toml +4 -1
- {x_evolution-0.1.34 → x_evolution-0.1.35}/x_evolution/x_evolution.py +110 -23
- {x_evolution-0.1.34 → x_evolution-0.1.35}/.gitignore +0 -0
- {x_evolution-0.1.34 → x_evolution-0.1.35}/LICENSE +0 -0
- {x_evolution-0.1.34 → x_evolution-0.1.35}/x_evolution/__init__.py +0 -0
- {x_evolution-0.1.34 → x_evolution-0.1.35}/x_evolution/update_rules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: x-evolution
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.35
|
|
4
4
|
Summary: x-evolution
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/x-evolution/
|
|
6
6
|
Project-URL: Repository, https://github.com/lucidrains/x-evolution
|
|
@@ -191,4 +191,13 @@ Special thanks to [Pollen Robotics 🤗](https://www.pollen-robotics.com/) for t
|
|
|
191
191
|
}
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
```bibtex
|
|
195
|
+
@misc{dahal2026backprop,
|
|
196
|
+
title = {Dust: Pretraining Transformers Without Backpropagation},
|
|
197
|
+
author = {Dahal, Samip and Mandal, Bishwas and G{\"u}lbahar, Serdar and Vegesna, Akshay},
|
|
198
|
+
year = {2026},
|
|
199
|
+
url = {https://qlabs.sh/research/dust}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
194
203
|
*Nothing makes sense except in the light of evolution* - Theodosius Dobzhansky
|
|
@@ -140,4 +140,13 @@ Special thanks to [Pollen Robotics 🤗](https://www.pollen-robotics.com/) for t
|
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
```bibtex
|
|
144
|
+
@misc{dahal2026backprop,
|
|
145
|
+
title = {Dust: Pretraining Transformers Without Backpropagation},
|
|
146
|
+
author = {Dahal, Samip and Mandal, Bishwas and G{\"u}lbahar, Serdar and Vegesna, Akshay},
|
|
147
|
+
year = {2026},
|
|
148
|
+
url = {https://qlabs.sh/research/dust}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
143
152
|
*Nothing makes sense except in the light of evolution* - Theodosius Dobzhansky
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "x-evolution"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.35"
|
|
4
4
|
description = "x-evolution"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Phil Wang", email = "lucidrains@gmail.com" }
|
|
@@ -52,6 +52,9 @@ test = [
|
|
|
52
52
|
pythonpath = [
|
|
53
53
|
"."
|
|
54
54
|
]
|
|
55
|
+
testpaths = [
|
|
56
|
+
"tests"
|
|
57
|
+
]
|
|
55
58
|
|
|
56
59
|
[build-system]
|
|
57
60
|
requires = ["hatchling"]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Literal, Any
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
|
|
4
5
|
from math import ceil
|
|
5
6
|
from pathlib import Path
|
|
@@ -83,7 +84,7 @@ class EvoStrategy(Module):
|
|
|
83
84
|
self,
|
|
84
85
|
model: Module | list[Module],
|
|
85
86
|
*,
|
|
86
|
-
environment: Callable[[Module],
|
|
87
|
+
environment: Callable[[Module], Any] | dict[str, Callable[[Module], Any]], # reward function, or one per scope
|
|
87
88
|
num_generations,
|
|
88
89
|
noise_population_size = 30,
|
|
89
90
|
learning_rate = 1e-3,
|
|
@@ -135,15 +136,20 @@ class EvoStrategy(Module):
|
|
|
135
136
|
|
|
136
137
|
self.accelerate = accelerator
|
|
137
138
|
|
|
138
|
-
# environment -
|
|
139
|
+
# environment - a single reward function, or a dict of one per scope
|
|
140
|
+
# a single environment can also return one reward per scope, as a dict, tuple, or list
|
|
139
141
|
|
|
140
142
|
self.environment = environment
|
|
143
|
+
self.environments = environment if isinstance(environment, dict) else None
|
|
144
|
+
|
|
145
|
+
environments = list(self.environments.values()) if exists(self.environments) else [environment]
|
|
141
146
|
|
|
142
147
|
if accelerator.is_main_process:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
for one_environment in environments:
|
|
149
|
+
pre_main_callback = getattr(one_environment, 'pre_main_callback', None)
|
|
150
|
+
if callable(pre_main_callback):
|
|
151
|
+
self.print('pre_main_callback detected on environment passed in and is invoked')
|
|
152
|
+
pre_main_callback()
|
|
147
153
|
|
|
148
154
|
self.wait_for_everyone()
|
|
149
155
|
|
|
@@ -172,6 +178,7 @@ class EvoStrategy(Module):
|
|
|
172
178
|
# default to all parameters to optimize with evo strategy
|
|
173
179
|
|
|
174
180
|
self.scopes = None
|
|
181
|
+
self.scope_names = None
|
|
175
182
|
|
|
176
183
|
if isinstance(params_to_optimize, dict):
|
|
177
184
|
self.scopes = dict()
|
|
@@ -179,12 +186,19 @@ class EvoStrategy(Module):
|
|
|
179
186
|
|
|
180
187
|
for scope_name, scope_params in params_to_optimize.items():
|
|
181
188
|
resolved_scope_params = self._resolve_params_to_names(scope_params)
|
|
182
|
-
assert all(
|
|
189
|
+
assert all(name in param_names for name in resolved_scope_params), f'some parameters in scope {scope_name} are not found in the model'
|
|
183
190
|
self.scopes[scope_name] = resolved_scope_params
|
|
184
191
|
all_scoped_params |= resolved_scope_params
|
|
185
192
|
|
|
193
|
+
self.scope_names = list(self.scopes)
|
|
186
194
|
params_to_optimize = list(all_scoped_params)
|
|
187
195
|
|
|
196
|
+
# a dict of environments gives one reward function per scope, so the keys must line up with the scopes
|
|
197
|
+
|
|
198
|
+
if exists(self.environments):
|
|
199
|
+
assert exists(self.scopes), 'when environment is a dict, params_to_optimize must also be a dict with the same scope names'
|
|
200
|
+
assert set(self.environments) == set(self.scopes), f'environment keys {list(self.environments)} must match scope names {list(self.scopes)}'
|
|
201
|
+
|
|
188
202
|
params_to_optimize = default(params_to_optimize, param_names)
|
|
189
203
|
|
|
190
204
|
# convert to names using the helper method
|
|
@@ -341,6 +355,34 @@ class EvoStrategy(Module):
|
|
|
341
355
|
sigma_index = self.param_name_to_sigma_index[param_name]
|
|
342
356
|
return self.sigmas[sigma_index]
|
|
343
357
|
|
|
358
|
+
@staticmethod
|
|
359
|
+
def _to_float(reward):
|
|
360
|
+
if isinstance(reward, Tensor):
|
|
361
|
+
return reward.mean().item()
|
|
362
|
+
|
|
363
|
+
return float(reward)
|
|
364
|
+
|
|
365
|
+
def _resolve_rewards(self, rewards, scope):
|
|
366
|
+
# a reward is a scalar, or one reward per scope as a dict, tuple, or list
|
|
367
|
+
# resolves to a float, or to a list of floats in scope order, with a scope picking out its own reward
|
|
368
|
+
|
|
369
|
+
if isinstance(rewards, (dict, list, tuple)):
|
|
370
|
+
assert exists(self.scope_names), 'environment returned one reward per scope, but no scopes were defined in params_to_optimize'
|
|
371
|
+
|
|
372
|
+
if isinstance(rewards, dict):
|
|
373
|
+
assert set(rewards) == set(self.scope_names), f'reward keys {list(rewards)} must match scope names {self.scope_names}'
|
|
374
|
+
rewards = [rewards[name] for name in self.scope_names]
|
|
375
|
+
else:
|
|
376
|
+
assert len(rewards) == len(self.scope_names), f'environment returned {len(rewards)} rewards, expected one per scope {self.scope_names}'
|
|
377
|
+
rewards = list(rewards)
|
|
378
|
+
else:
|
|
379
|
+
return self._to_float(rewards)
|
|
380
|
+
|
|
381
|
+
if exists(scope):
|
|
382
|
+
return self._to_float(rewards[self.scope_names.index(scope)])
|
|
383
|
+
|
|
384
|
+
return [self._to_float(reward) for reward in rewards]
|
|
385
|
+
|
|
344
386
|
@torch.inference_mode()
|
|
345
387
|
def evolve_(
|
|
346
388
|
self,
|
|
@@ -474,6 +516,11 @@ class EvoStrategy(Module):
|
|
|
474
516
|
):
|
|
475
517
|
verbose = default(verbose, self.verbose)
|
|
476
518
|
|
|
519
|
+
# a scope restricts the population and the update to that scope's parameters and reward, for interleaving scopes phase by phase
|
|
520
|
+
# otherwise a joint population is rolled out, each scope updated with its own reward when one is returned per scope
|
|
521
|
+
|
|
522
|
+
dynamic_params = exists(params_to_optimize)
|
|
523
|
+
|
|
477
524
|
scoped_names = None
|
|
478
525
|
|
|
479
526
|
if exists(scope):
|
|
@@ -493,7 +540,7 @@ class EvoStrategy(Module):
|
|
|
493
540
|
# maybe sigmas
|
|
494
541
|
|
|
495
542
|
if self.learned_noise_scale:
|
|
496
|
-
|
|
543
|
+
self.sigmas = self.sigmas.to(self.device)
|
|
497
544
|
|
|
498
545
|
# get world size, rank, and determine if distributed
|
|
499
546
|
|
|
@@ -545,6 +592,7 @@ class EvoStrategy(Module):
|
|
|
545
592
|
seeds_for_machine = seeds_for_population.chunk(world_size)[rank]
|
|
546
593
|
|
|
547
594
|
fitnesses = []
|
|
595
|
+
rewards_by_scope = False
|
|
548
596
|
|
|
549
597
|
# seeds
|
|
550
598
|
|
|
@@ -559,28 +607,38 @@ class EvoStrategy(Module):
|
|
|
559
607
|
for noise_index, individual_seed in pbar:
|
|
560
608
|
|
|
561
609
|
if noise_index >= pop_size:
|
|
562
|
-
fitnesses.append(
|
|
610
|
+
fitnesses.append(None)
|
|
563
611
|
continue
|
|
564
612
|
|
|
565
613
|
def get_fitness(negate = False):
|
|
614
|
+
nonlocal rewards_by_scope
|
|
615
|
+
|
|
566
616
|
individual_param_seeds = with_seed(individual_seed.item())(randint)(0, MAX_SEED_VALUE, (self.num_params,))
|
|
567
617
|
noise_config = dict(zip(self.param_names_to_optimize, individual_param_seeds.tolist()))
|
|
568
618
|
|
|
569
619
|
if exists(scoped_names):
|
|
570
620
|
noise_config = {k: v for k, v in noise_config.items() if k in scoped_names}
|
|
571
621
|
|
|
572
|
-
noise_config_with_scale =
|
|
573
|
-
for param_name, seed in noise_config.items():
|
|
574
|
-
noise_scale = self._get_noise_scale(param_name)
|
|
575
|
-
noise_config_with_scale[param_name] = (seed, noise_scale)
|
|
622
|
+
noise_config_with_scale = {name: (seed, self._get_noise_scale(name)) for name, seed in noise_config.items()}
|
|
576
623
|
|
|
577
624
|
with model.temp_add_noise_(noise_config_with_scale, negate = negate):
|
|
578
|
-
fitness = with_seed(maybe_rollout_seed)(self.environment)(model)
|
|
579
625
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
626
|
+
# a dict of environments gives one reward function per scope
|
|
627
|
+
# otherwise the single environment is called once, returning a scalar, or one reward per scope
|
|
628
|
+
|
|
629
|
+
rollout = with_seed(maybe_rollout_seed)
|
|
630
|
+
|
|
631
|
+
if exists(self.environments) and not exists(scope):
|
|
632
|
+
fitness = [rollout(self.environments[name])(model) for name in self.scope_names]
|
|
633
|
+
else:
|
|
634
|
+
env = self.environments[scope] if exists(scope) and exists(self.environments) else self.environment
|
|
635
|
+
fitness = rollout(env)(model)
|
|
636
|
+
|
|
637
|
+
fitness = self._resolve_rewards(fitness, scope)
|
|
638
|
+
|
|
639
|
+
if isinstance(fitness, list):
|
|
640
|
+
assert not dynamic_params, 'per-scope rewards were returned, so pass `scope = ...` instead of `params_to_optimize = ...`'
|
|
641
|
+
rewards_by_scope = True
|
|
584
642
|
|
|
585
643
|
return fitness
|
|
586
644
|
|
|
@@ -601,13 +659,30 @@ class EvoStrategy(Module):
|
|
|
601
659
|
fitness_mirrored = get_fitness(negate = True)
|
|
602
660
|
|
|
603
661
|
fitnesses.append([fitness, fitness_mirrored])
|
|
604
|
-
|
|
662
|
+
|
|
663
|
+
if isinstance(fitness, list):
|
|
664
|
+
avg_fitness = [(pos + neg) / 2 for pos, neg in zip(fitness, fitness_mirrored)]
|
|
665
|
+
else:
|
|
666
|
+
avg_fitness = (fitness + fitness_mirrored) / 2
|
|
605
667
|
|
|
606
668
|
if exists(self.on_result):
|
|
607
669
|
self.on_result(avg_fitness, pbar)
|
|
608
670
|
|
|
609
671
|
pbar.close()
|
|
610
672
|
|
|
673
|
+
# whether rewards are per scope is discovered from the rollouts, and needs to be agreed on across machines
|
|
674
|
+
|
|
675
|
+
if is_distributed:
|
|
676
|
+
flag = tensor(float(rewards_by_scope), device = self.device)
|
|
677
|
+
rewards_by_scope = self.accelerate.reduce(flag, reduction = 'sum').item() > 0
|
|
678
|
+
|
|
679
|
+
# pad the population to a multiple of the world size
|
|
680
|
+
|
|
681
|
+
zero = [0.] * len(self.scope_names) if rewards_by_scope else 0.
|
|
682
|
+
padding = [zero, zero] if self.mirror_sampling else zero
|
|
683
|
+
|
|
684
|
+
fitnesses = [default(entry, padding) for entry in fitnesses]
|
|
685
|
+
|
|
611
686
|
# normalize the fitness and weighted sum of all the noise is the update
|
|
612
687
|
|
|
613
688
|
fitnesses = tensor(fitnesses, device = self.device, dtype = torch.float32)
|
|
@@ -632,9 +707,13 @@ class EvoStrategy(Module):
|
|
|
632
707
|
self.print(f'[{generation}] fitnesses rejected')
|
|
633
708
|
continue
|
|
634
709
|
|
|
635
|
-
#
|
|
710
|
+
# one joint population - with per-scope rewards, each scope is updated with its own column
|
|
636
711
|
|
|
637
|
-
|
|
712
|
+
if rewards_by_scope:
|
|
713
|
+
for name, scope_fitnesses in zip(self.scope_names, fitnesses.unbind(dim = -1)):
|
|
714
|
+
self.evolve_(scope_fitnesses, seeds_for_population, scoped_names = self.scopes[name])
|
|
715
|
+
else:
|
|
716
|
+
self.evolve_(fitnesses, seeds_for_population, scoped_names = scoped_names)
|
|
638
717
|
|
|
639
718
|
# on generation callback
|
|
640
719
|
|
|
@@ -643,7 +722,15 @@ class EvoStrategy(Module):
|
|
|
643
722
|
|
|
644
723
|
# log
|
|
645
724
|
|
|
646
|
-
|
|
725
|
+
if rewards_by_scope:
|
|
726
|
+
fitness_msg = ' | '.join(
|
|
727
|
+
f'{name}: {scope_fitnesses.mean():.3f}'
|
|
728
|
+
for name, scope_fitnesses in zip(self.scope_names, fitnesses.unbind(dim = -1))
|
|
729
|
+
)
|
|
730
|
+
else:
|
|
731
|
+
fitness_msg = f'{fitnesses.mean():.3f} | fitness std: {fitnesses.std():.3f}'
|
|
732
|
+
|
|
733
|
+
msg = f'[{generation}] average fitness: {fitness_msg}'
|
|
647
734
|
|
|
648
735
|
if self.learned_noise_scale:
|
|
649
736
|
packed_sigma, _ = pack(list(self.sigmas), '*')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|