Skip to main content

shardes

Sharded evolution strategies for JAX.

Evolution strategies train a model without gradients: perturb the weights, score each perturbed copy, move toward the copies that scored well. shardes is an ask / apply / tell core that splits that population across devices, with the perturbation scheme as a pluggable strategy. The two 2025 results that made ES work at language-model scale, full-rank noise regenerated from seeds (Qiu et al.) and rank-r factored noise (EGGROLL), are one constructor argument apart here, on the same mesh, shaping and update.

Status: 0.1, pre-alpha. The API can still change. What is here is tested, on CPU with eight simulated devices, and device-count invariance has been checked on real hardware too: the GPU suite on two T4s, and one device against eight A100s on a 0.5B-parameter model.

Install

pip install shardes                                                          # the release
pip install "shardes @ git+https://github.com/andreshernandez-spec/shardes"  # main

Python 3.12 or newer, JAX 0.11 or newer. On an accelerator, install JAX for it first (pip install -U "jax[cuda12]" or "jax[tpu]"). The core needs only jax, numpy and scipy. Extras: shardes[models] for loading Qwen2.5 checkpoints, shardes[tasks] for the MuJoCo Playground adapter.

Quickstart

This is examples/quickstart.py, and the test suite runs it. It needs no accelerator: eight simulated CPU devices stand in, and the sharding is the same program either way.

import os
os.environ["XLA_FLAGS"] = "--xla_force_host_platform_device_count=8"   # 8 devices on a CPU

import jax
import shardes
from shardes.problems import transformer_block       # a small model that uses the seams

key = jax.random.key(0)
params = transformer_block.init(key, d_model=64)
batch = transformer_block.make_batch(jax.random.fold_in(key, 1), d_model=64, batch=8, seq=32)

mesh = shardes.make_mesh()                           # every visible device, one "pop" axis
es = shardes.ShardedES(shardes.Mirrored(shardes.LowRank(r=1)),
                       n=256, sigma=1e-2, lr=1e-2, mesh=mesh)
state = es.init(key, params)

@jax.jit
def generation(state):
    pert, state = es.ask(state)                      # a Perturbation, never a batch of params
    fitness = es.apply(transformer_block.loss, state, pert)(batch)   # one loss per member
    return es.tell(state, pert, fitness), fitness

for step in range(30):
    state, fitness = generation(state)
    if step % 10 == 0 or step == 29:
        print(f"step {step:2d}  mean loss {float(fitness.mean()):.4f}")

Jit the whole generation rather than stepping it eagerly; that is what lets JAX settle device placement at trace time. tell descends, so hand it a loss, or the negative of a reward.

Both published algorithms, one argument apart

# Qiu et al. 2025: full-rank noise, regenerated from seeds, small population
shardes.ShardedES(shardes.Mirrored(shardes.SeedRegenerated()), n=30, ...)

# EGGROLL (Sarkar et al. 2025): rank-1 factors, never materialized, huge population
shardes.ShardedES(shardes.Mirrored(shardes.LowRank(r=1)), n=262_144, ...)
strategy the perturbation what it costs
IIDGaussian() full rank, materialized memory: n copies of the noise
SeedRegenerated(chunk=1) full rank, regenerated from each member's seed compute: every perturbation is drawn twice. chunk trades memory for speed
LowRank(r) rank r, two thin factors per matrix, product never formed the noise is not full rank
Mirrored(inner) antithetic pairs around any of the above halves the distinct directions

ask returns a Perturbation, not a batch of parameter trees, and the rest follows from that: under LowRank it is a pair of factors, under SeedRegenerated a key and member ids. Shaping is centered_ranks by default (shardes.shaping has centered, group_relative and none).

Where the update is assembled: how="A" or how="B"

After evaluation each device holds the fitnesses of its own members, and the update sum_i w_i eps_i has to be put together. There are two placements, and which is faster depends on the perturbation and on the interconnect.

  • how="B", the default. Each device contracts its own members, then the partial updates are all-reduced. That moves a buffer the size of the model.
  • how="A". Every device gathers the scalar fitnesses and contracts the whole population itself. Only scalars cross the wire, and the contraction is repeated on every device.

Measured on an 8x A100 node, a TPU v5e-8 and two A100 nodes over sockets: on one host with a fast interconnect, B wins for full-rank and seed-regenerated perturbations, and A wins for low-rank perturbations on large models, by more on the TPU. Across a slow host boundary A wins more widely. The measurements, and the paper they belong to, are in shardes-paper.

Your own model

A low-rank perturbation is never materialized, so a perturbed weight is not an array: it is a base matrix plus two factors. A model therefore routes its parameterized matrix multiplies and embedding lookups through two seams, shardes.nn.dense(x, w) and shardes.nn.embed(table, ids), which do the right thing for a plain array and for a structured weight alike. Direct arithmetic on a structured weight raises rather than silently densifying. shardes.problems.transformer_block is the small worked example and shardes.problems.qwen2 is Qwen2.5 ported this way. shardes.check.check_model(model, params, batch) finds, in a second on CPU, every weight a model reaches without a seam, instead of the strategy raising minutes into a run.

What it guarantees

  • Device count cannot change the result. Member i's noise derives from i alone, so the update contracted on one device and on eight agrees to floating-point tolerance. This is tested on simulated devices and checked on real ones: tests/gpu on two T4s (validation/), and on A100s the update for Qwen2.5-0.5B computed on one device and on eight agrees to 6.3e-6 relative error (shardes-paper:experiments/countdown/results/c6d-a100x8-2026-08-18).
  • The low-rank path never forms an (n, m, n) array. A test inspects the traced program to make sure.
  • Fitness is float32 or wider. In bfloat16 a population's losses collapse to a handful of ties and rank shaping of ties is noise, so tell refuses rather than casts.
  • No global flattening. Parameters keep their shapes from sampling to update, which is what makes per-matrix structure expressible at all.

Development

pip install -e ".[dev]"
pytest --fast        # the inner loop
pytest               # everything, including the statistical tier

The suite pins JAX to the CPU and simulates eight devices, so it needs no accelerator and no network. tools/ holds scripts that check the library itself (mutation testing, compile-cost diagnostics, two probes behind design decisions), validation/ the real-hardware invariance check, and docs/ the design, conventions and diagnoses. CI runs the suite on Python 3.12 and 3.13 and installs the built wheel into a clean environment.

The paper and the experiments

This library is the instrument of Update-contraction placement in sharded evolution strategies on GPUs and TPUs. The manuscript, every experiment and every result live in shardes-paper, which installs this library at a pinned commit.

Until the tag monorepo-final the two were one repository, and that history is kept here on purpose: result records from that period cite commits of this repository, and checking one out gives the driver, its config and the library together, as run.

Non-goals

  • Sharded parameters. Every device holds the model and evaluates independently; sharding parameters would bring back the communication ES avoids.
  • A general replacement for evosax. This targets the sharded, large-population, structured-perturbation regime.
  • Reimplementing either paper's full experimental setup. The papers stand; this is infrastructure.

License

Apache 2.0.

Release files for shardes 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shardes 0.1.1
File Size Uploaded
shardes-0.1.1.tar.gz 274.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for shardes 0.1.1
File Interpreter ABI Platform
shardes-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 365.3 kB

Release files / shardes-0.1.1.tar.gz

Download URL shardes-0.1.1.tar.gz
Size 274.5 kB
Tags Source
SHA-256 checksum
How to use checksums
94f19506f8e049b1aa92b7cb496d14579859a4ef8577b3e24912709153e1334d
BLAKE2b-256 checksum
How to use checksums
1337da361ad732cf1d58af75fefb6ec925818cc5afbf47c0c64bb137442af57b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / shardes-0.1.1-py3-none-any.whl

Download URL shardes-0.1.1-py3-none-any.whl
Size 90.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
406e6e13a05a0b5d9c45203d60a00518bd5f0d95ca902f1bcbdd87acc317acb6
BLAKE2b-256 checksum
How to use checksums
1531ff0679a8195a6d31c56630efb55fa8d13275ed1418495edb203c142cbd52
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page