Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions seedkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# seedkit

`seedkit` provides deterministic sub-seeding utilities for scientific experiments. A master seed
combined with textual identifiers is hashed using SHA-256 to derive a 64‑bit sub-seed. This
sub-seed feeds NumPy's counter-based `Philox` bit generator which enables many independent and
reproducible streams.

Hashing decorrelates streams derived from related identifiers, but it does **not** offer any
cryptographic guarantees. It simply prevents accidental global correlations when deriving
sub-seeds from structured information.

## Usage

### Library
```python
from seedkit.seeding import make_subseed, philox_generator, python_random

sub = make_subseed(42, "trainer", "R1", 0)
np_gen = philox_generator(sub)
py_gen = python_random(sub)
print(np_gen.random(3))
print(py_gen.random())
```

### Command line
```
$ seedkit demo --master 42 --component trainer --run R1 --stream 0 --n 5
Derived subseed: 11789288199025255931
NumPy samples: [0.0116 ...]
Python samples: [0.2327 ...]
Torch samples: [0.4561 ...] # only if PyTorch is installed
```

Philox is a counter-based generator designed for parallel use. Each stream consumes from an
independent counter, avoiding overlap between streams.
19 changes: 19 additions & 0 deletions seedkit/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "seedkit"
version = "0.1.0"
description = "Deterministic seeding utilities backed by SHA-256 and NumPy's Philox"
authors = [ { name = "SeedKit Developers" } ]
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.10"
dependencies = ["numpy>=1.22"]

[project.optional-dependencies]
torch = ["torch"]

[project.scripts]
seedkit = "seedkit.cli:main"
19 changes: 19 additions & 0 deletions seedkit/src/seedkit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Top level package for seedkit."""

from .seeding import (
SeedStreams,
make_subseed,
philox_generator,
python_random,
torch_generator,
set_torch_deterministic,
)

__all__ = [

Check failure on line 12 in seedkit/src/seedkit/__init__.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

seedkit/src/seedkit/__init__.py:3:1: I001 Import block is un-sorted or un-formatted
"SeedStreams",
"make_subseed",
"philox_generator",
"python_random",
"torch_generator",
"set_torch_deterministic",
]
55 changes: 55 additions & 0 deletions seedkit/src/seedkit/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Console interface for seedkit."""
from __future__ import annotations

import argparse
from typing import Sequence, Optional

Check failure on line 5 in seedkit/src/seedkit/cli.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP035)

seedkit/src/seedkit/cli.py:5:1: UP035 Import from `collections.abc` instead: `Sequence`

from .seeding import (
make_subseed,
philox_generator,
python_random,
torch_generator,
)


def _demo(args: argparse.Namespace) -> None:

Check failure on line 15 in seedkit/src/seedkit/cli.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

seedkit/src/seedkit/cli.py:2:1: I001 Import block is un-sorted or un-formatted
subseed = make_subseed(args.master, args.component, args.run, args.stream)
np_gen = philox_generator(subseed)
py_gen = python_random(subseed)

print(f"Derived subseed: {subseed}")
print("NumPy samples:", np_gen.random(args.n).tolist())
print("Python samples:", [py_gen.random() for _ in range(args.n)])

try:
torch_gen = torch_generator(subseed=subseed)
import torch

print("Torch samples:", torch.rand(args.n, generator=torch_gen).tolist())
except Exception:
pass


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="seedkit")
sub = parser.add_subparsers(dest="command", required=True)

demo = sub.add_parser("demo", help="Showcase deterministic seeding")
demo.add_argument("--master", required=True, help="Master seed")
demo.add_argument("--component", required=True, help="Component identifier")
demo.add_argument("--run", required=True, help="Run identifier")
demo.add_argument("--stream", type=int, default=0, help="Stream identifier")
demo.add_argument("--n", type=int, default=5, help="Number of samples to draw")
demo.set_defaults(func=_demo)

return parser


def main(argv: Optional[Sequence[str]] = None) -> None:

Check failure on line 48 in seedkit/src/seedkit/cli.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP007)

seedkit/src/seedkit/cli.py:48:16: UP007 Use `X | Y` for type annotations
parser = build_parser()
args = parser.parse_args(argv)
args.func(args)


if __name__ == "__main__": # pragma: no cover
main()
106 changes: 106 additions & 0 deletions seedkit/src/seedkit/seeding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Deterministic seeding utilities."""
from __future__ import annotations

import hashlib
import random
from dataclasses import dataclass
from typing import Optional, Union

import numpy as np

try: # Optional dependency
import torch # type: ignore
except Exception: # pragma: no cover - import guarded
torch = None # type: ignore


def make_subseed(
master_seed: Union[int, str],

Check failure on line 18 in seedkit/src/seedkit/seeding.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP007)

seedkit/src/seedkit/seeding.py:18:18: UP007 Use `X | Y` for type annotations
component_id: str,
run_id: str,
stream_id: Union[int, str] = 0,

Check failure on line 21 in seedkit/src/seedkit/seeding.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP007)

seedkit/src/seedkit/seeding.py:21:16: UP007 Use `X | Y` for type annotations
) -> int:
"""Derive a 64-bit subseed via SHA-256.

Parameters
----------
master_seed:
Global experiment seed.
component_id, run_id, stream_id:
Identifiers used to create independent streams.

Returns
-------
int
First eight bytes of the SHA-256 hash interpreted as a big-endian integer.
"""

parts = [str(master_seed), component_id, run_id, str(stream_id)]
data = b"\x00".join(p.encode("utf-8") for p in parts)
digest = hashlib.sha256(data).digest()
return int.from_bytes(digest[:8], "big")


def philox_generator(subseed: int) -> np.random.Generator:
"""Create a NumPy Generator backed by Philox."""
bitgen = np.random.Philox(subseed)
return np.random.Generator(bitgen)


def python_random(subseed: int) -> random.Random:
"""Create a Python ``random.Random`` seeded with ``subseed``."""
return random.Random(subseed)


def set_torch_deterministic() -> None:
"""Enable deterministic algorithms in PyTorch if available."""
if torch is None: # pragma: no cover - optional
return
torch.use_deterministic_algorithms(True)


def torch_generator(device: str = "cpu", subseed: Optional[int] = None):

Check failure on line 62 in seedkit/src/seedkit/seeding.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP007)

seedkit/src/seedkit/seeding.py:62:51: UP007 Use `X | Y` for type annotations
"""Create a ``torch.Generator`` seeded with ``subseed``.

Parameters
----------
device:
Device for the generator, e.g. ``"cpu"`` or ``"cuda"``.
subseed:
Optional seed for the generator. If ``None`` an unseeded generator is
returned.
"""
if torch is None: # pragma: no cover - optional
raise RuntimeError("PyTorch not installed")

gen = torch.Generator(device=device)
if subseed is not None:
gen.manual_seed(subseed)
set_torch_deterministic()
return gen


@dataclass
class SeedStreams:
"""Bundle of deterministic generators."""

numpy: np.random.Generator
python: random.Random
torch: Optional["torch.Generator"] = None

Check failure on line 89 in seedkit/src/seedkit/seeding.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP037)

seedkit/src/seedkit/seeding.py:89:21: UP037 Remove quotes from type annotation

Check failure on line 89 in seedkit/src/seedkit/seeding.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP007)

seedkit/src/seedkit/seeding.py:89:12: UP007 Use `X | Y` for type annotations


def seed_streams(
master_seed: Union[int, str],

Check failure on line 93 in seedkit/src/seedkit/seeding.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP007)

seedkit/src/seedkit/seeding.py:93:18: UP007 Use `X | Y` for type annotations
component_id: str,
run_id: str,
stream_id: Union[int, str] = 0,
) -> SeedStreams:
"""Convenience factory creating ``SeedStreams`` from identifiers."""
subseed = make_subseed(master_seed, component_id, run_id, stream_id)
np_gen = philox_generator(subseed)
py_gen = python_random(subseed)
try:
torch_gen = torch_generator(subseed=subseed)
except Exception: # pragma: no cover - optional
torch_gen = None
return SeedStreams(np_gen, py_gen, torch_gen)
29 changes: 29 additions & 0 deletions seedkit/tests/test_seeding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "src"))

import numpy as np

from seedkit.seeding import make_subseed, philox_generator


def test_subseed_determinism():
s1 = make_subseed(123, "component", "run", 0)
s2 = make_subseed(123, "component", "run", 0)
assert s1 == s2

s3 = make_subseed(123, "other", "run", 0)
assert s1 != s3

s4 = make_subseed(123, "component", "run", 1)
assert s1 != s4


def test_stream_independence():
sub0 = make_subseed(123, "component", "run", 0)
sub1 = make_subseed(123, "component", "run", 1)
g0 = philox_generator(sub0)
g1 = philox_generator(sub1)
x = g0.random(1000)
y = g1.random(1000)
r = np.corrcoef(x, y)[0, 1]
assert abs(r) < 0.1