spikeforge 0.3.0__py3-none-any.whl
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.
- main.py +26 -0
- main_encodings.py +43 -0
- spikeforge/__init__.py +19 -0
- spikeforge/benchmark/__init__.py +44 -0
- spikeforge/benchmark/__main__.py +14 -0
- spikeforge/benchmark/cli.py +192 -0
- spikeforge/benchmark/compare.py +192 -0
- spikeforge/benchmark/config.py +40 -0
- spikeforge/benchmark/energy.py +31 -0
- spikeforge/benchmark/harness.py +239 -0
- spikeforge/benchmark/memory.py +63 -0
- spikeforge/benchmark/serving.py +318 -0
- spikeforge/benchmark/store.py +114 -0
- spikeforge/benchmark/suite.py +74 -0
- spikeforge/benchmark/timing.py +53 -0
- spikeforge/cli/__init__.py +1 -0
- spikeforge/cli/backend_cli.py +117 -0
- spikeforge/cli/extract_cli.py +147 -0
- spikeforge/cli/fixture.py +94 -0
- spikeforge/cli/onnx_cli.py +85 -0
- spikeforge/cli/records_cli.py +151 -0
- spikeforge/cli/verify.py +156 -0
- spikeforge/compression/__init__.py +52 -0
- spikeforge/compression/codec.py +226 -0
- spikeforge/compression/errors.py +19 -0
- spikeforge/compression/pruning.py +234 -0
- spikeforge/compression/report.py +70 -0
- spikeforge/config.py +78 -0
- spikeforge/data/__init__.py +1 -0
- spikeforge/data/data_loader.py +19 -0
- spikeforge/data/dataset_spec.py +35 -0
- spikeforge/data/datasets.py +176 -0
- spikeforge/data/download_cli.py +30 -0
- spikeforge/data/event_errors.py +19 -0
- spikeforge/data/event_geometry.py +17 -0
- spikeforge/data/event_loader.py +138 -0
- spikeforge/data/image_size.py +21 -0
- spikeforge/data/sample_source.py +53 -0
- spikeforge/data/sequence_source.py +59 -0
- spikeforge/encoding/__init__.py +1 -0
- spikeforge/encoding/delta_trainer.py +66 -0
- spikeforge/encoding/latency_trainer.py +115 -0
- spikeforge/encoding/random_spikegen.py +54 -0
- spikeforge/encoding/spike_encoder.py +151 -0
- spikeforge/events/__init__.py +8 -0
- spikeforge/events/dense.py +55 -0
- spikeforge/events/event_bridge.py +109 -0
- spikeforge/events/event_sample.py +124 -0
- spikeforge/events/event_source.py +125 -0
- spikeforge/events/synthetic.py +82 -0
- spikeforge/events/tonic_api.py +79 -0
- spikeforge/exporters/__init__.py +1 -0
- spikeforge/exporters/delta_exporter.py +46 -0
- spikeforge/exporters/exporter.py +31 -0
- spikeforge/exporters/latency_curve_exporter.py +26 -0
- spikeforge/exporters/latency_raster_exporter.py +39 -0
- spikeforge/exporters/latency_video_exporter.py +26 -0
- spikeforge/exporters/plot_utils.py +60 -0
- spikeforge/exporters/presentation_exporter.py +159 -0
- spikeforge/exporters/random_spike_raster_exporter.py +28 -0
- spikeforge/exporters/random_spike_video_exporter.py +23 -0
- spikeforge/exporters/raster_exporter.py +58 -0
- spikeforge/exporters/reconstruction_exporter.py +49 -0
- spikeforge/exporters/spike_gif_exporter.py +41 -0
- spikeforge/exporters/video_exporter.py +24 -0
- spikeforge/introspection/__init__.py +10 -0
- spikeforge/introspection/comparison.py +78 -0
- spikeforge/introspection/decoding.py +114 -0
- spikeforge/introspection/encoding.py +156 -0
- spikeforge/introspection/firing_rate.py +22 -0
- spikeforge/introspection/histogram.py +36 -0
- spikeforge/introspection/isi.py +64 -0
- spikeforge/introspection/metrics.py +46 -0
- spikeforge/introspection/sparsity.py +22 -0
- spikeforge/introspection/surrogate.py +113 -0
- spikeforge/memory/__init__.py +8 -0
- spikeforge/memory/frozen_classifier.py +78 -0
- spikeforge/memory/hebbian_synapse.py +57 -0
- spikeforge/memory/held_out_digit_poc.py +169 -0
- spikeforge/memory/lockstep_runner.py +49 -0
- spikeforge/memory/one_shot_associative_memory.py +83 -0
- spikeforge/network/__init__.py +1 -0
- spikeforge/network/hidden_frames.py +37 -0
- spikeforge/network/inference.py +153 -0
- spikeforge/network/model_diff.py +119 -0
- spikeforge/network/model_search.py +106 -0
- spikeforge/network/model_store.py +119 -0
- spikeforge/network/spiking_net.py +102 -0
- spikeforge/neurons/__init__.py +1 -0
- spikeforge/neurons/alpha.py +74 -0
- spikeforge/neurons/contract.py +135 -0
- spikeforge/neurons/handler.py +35 -0
- spikeforge/neurons/lapicque.py +48 -0
- spikeforge/neurons/leaky.py +48 -0
- spikeforge/neurons/recurrent.py +70 -0
- spikeforge/neurons/registry.py +38 -0
- spikeforge/neurons/spike_grad.py +25 -0
- spikeforge/neurons/synaptic.py +53 -0
- spikeforge/nir_bridge/__init__.py +72 -0
- spikeforge/nir_bridge/api.py +143 -0
- spikeforge/nir_bridge/array_codec.py +60 -0
- spikeforge/nir_bridge/drift.py +76 -0
- spikeforge/nir_bridge/errors.py +110 -0
- spikeforge/nir_bridge/exporter.py +99 -0
- spikeforge/nir_bridge/extract.py +73 -0
- spikeforge/nir_bridge/ingest.py +37 -0
- spikeforge/nir_bridge/interpreter.py +237 -0
- spikeforge/nir_bridge/interpreter_result.py +23 -0
- spikeforge/nir_bridge/jsonable.py +38 -0
- spikeforge/nir_bridge/mapped_node.py +15 -0
- spikeforge/nir_bridge/mapper.py +67 -0
- spikeforge/nir_bridge/neuron_kwargs.py +58 -0
- spikeforge/nir_bridge/neuron_nodes.py +174 -0
- spikeforge/nir_bridge/node_builders.py +206 -0
- spikeforge/nir_bridge/node_names.py +58 -0
- spikeforge/nir_bridge/ops_common.py +22 -0
- spikeforge/nir_bridge/ops_linear.py +94 -0
- spikeforge/nir_bridge/ops_neuron.py +140 -0
- spikeforge/nir_bridge/ops_registry.py +103 -0
- spikeforge/nir_bridge/planner.py +184 -0
- spikeforge/nir_bridge/require.py +18 -0
- spikeforge/nir_bridge/roundtrip.py +100 -0
- spikeforge/nir_bridge/serialization.py +134 -0
- spikeforge/nir_bridge/stage_builders.py +33 -0
- spikeforge/nir_bridge/stage_mapping.py +21 -0
- spikeforge/nir_bridge/stages_unmappable.py +31 -0
- spikeforge/nir_bridge/tolerances.py +30 -0
- spikeforge/nir_bridge/torch_map.py +42 -0
- spikeforge/nir_bridge/validation_report.py +49 -0
- spikeforge/nir_bridge/validator.py +206 -0
- spikeforge/observability/__init__.py +26 -0
- spikeforge/observability/json_formatter.py +36 -0
- spikeforge/observability/logging_setup.py +109 -0
- spikeforge/observability/metrics.py +49 -0
- spikeforge/observability/persistence.py +119 -0
- spikeforge/observability/prometheus.py +173 -0
- spikeforge/observability/registry.py +88 -0
- spikeforge/observability/snapshot.py +48 -0
- spikeforge/observability/store.py +75 -0
- spikeforge/observability/timer.py +30 -0
- spikeforge/onnx_bridge/__init__.py +52 -0
- spikeforge/onnx_bridge/api.py +187 -0
- spikeforge/onnx_bridge/errors.py +68 -0
- spikeforge/onnx_bridge/export.py +106 -0
- spikeforge/onnx_bridge/import_onnx.py +209 -0
- spikeforge/onnx_bridge/metadata.py +47 -0
- spikeforge/onnx_bridge/roundtrip.py +48 -0
- spikeforge/onnx_bridge/step_module.py +28 -0
- spikeforge/py.typed +0 -0
- spikeforge/runtime/__init__.py +1 -0
- spikeforge/runtime/device.py +144 -0
- spikeforge/runtime/execution_mode.py +16 -0
- spikeforge/runtime/system_stats.py +75 -0
- spikeforge/serving/__init__.py +42 -0
- spikeforge/serving/bundle.py +504 -0
- spikeforge/serving/bundle_manifest.py +143 -0
- spikeforge/serving/encode_spec.py +320 -0
- spikeforge/serving/errors.py +93 -0
- spikeforge/serving/prediction.py +35 -0
- spikeforge/serving/preprocess.py +109 -0
- spikeforge/serving/session.py +262 -0
- spikeforge/serving/state_tree.py +45 -0
- spikeforge/serving/step.py +10 -0
- spikeforge/serving/tensor_codec.py +76 -0
- spikeforge/simulator/__init__.py +1 -0
- spikeforge/simulator/compiled_step.py +126 -0
- spikeforge/simulator/execution.py +151 -0
- spikeforge/simulator/frames.py +38 -0
- spikeforge/simulator/grad_policy.py +76 -0
- spikeforge/simulator/input_shape.py +63 -0
- spikeforge/simulator/module_spec.py +16 -0
- spikeforge/simulator/parallel_runner.py +45 -0
- spikeforge/simulator/production.py +31 -0
- spikeforge/simulator/production_result.py +27 -0
- spikeforge/simulator/runner.py +47 -0
- spikeforge/simulator/state.py +37 -0
- spikeforge/simulator/step_stages.py +61 -0
- spikeforge/simulator/trajectory.py +30 -0
- spikeforge/streaming/__init__.py +98 -0
- spikeforge/streaming/encoding.py +102 -0
- spikeforge/streaming/recipe.py +341 -0
- spikeforge/streaming/serving.py +230 -0
- spikeforge/streaming/stream_source.py +242 -0
- spikeforge/streaming/window_spec.py +183 -0
- spikeforge/topology/__init__.py +1 -0
- spikeforge/topology/attention.py +33 -0
- spikeforge/topology/builder.py +9 -0
- spikeforge/topology/edge.py +35 -0
- spikeforge/topology/kinds.py +32 -0
- spikeforge/topology/multihead_attention.py +54 -0
- spikeforge/topology/ordering.py +45 -0
- spikeforge/topology/positional_encoding.py +38 -0
- spikeforge/topology/presets.py +250 -0
- spikeforge/topology/registry.py +192 -0
- spikeforge/topology/sequence_presets.py +143 -0
- spikeforge/topology/sequence_stages.py +75 -0
- spikeforge/topology/spec.py +158 -0
- spikeforge/topology/stage.py +35 -0
- spikeforge/topology/stage_module.py +152 -0
- spikeforge/topology/stage_modules.py +117 -0
- spikeforge/topology/sum_pool.py +45 -0
- spikeforge/topology/validation.py +62 -0
- spikeforge/tracking/__init__.py +1 -0
- spikeforge/tracking/config_hash.py +27 -0
- spikeforge/tracking/determinism.py +128 -0
- spikeforge/tracking/manifest.py +104 -0
- spikeforge/tracking/seed.py +42 -0
- spikeforge/tracking/sink.py +31 -0
- spikeforge/tracking/sink_probe.py +36 -0
- spikeforge/tracking/sink_records.py +33 -0
- spikeforge/tracking/sinks.py +87 -0
- spikeforge/tracking/tensorboard_sink.py +45 -0
- spikeforge/tracking/versions.py +44 -0
- spikeforge/tracking/wandb_sink.py +46 -0
- spikeforge/training/__init__.py +1 -0
- spikeforge/training/amp_controller.py +101 -0
- spikeforge/training/checkpoint_mixin.py +146 -0
- spikeforge/training/encoding_mixin.py +73 -0
- spikeforge/training/eval_mixin.py +51 -0
- spikeforge/training/event_batches.py +70 -0
- spikeforge/training/event_engine.py +207 -0
- spikeforge/training/logger.py +34 -0
- spikeforge/training/multi_device.py +61 -0
- spikeforge/training/scaleup_mixin.py +150 -0
- spikeforge/training/topology_mixin.py +90 -0
- spikeforge/training/trainer.py +166 -0
- spikeforge/training/training_engine.py +256 -0
- spikeforge-0.3.0.dist-info/METADATA +149 -0
- spikeforge-0.3.0.dist-info/RECORD +233 -0
- spikeforge-0.3.0.dist-info/WHEEL +5 -0
- spikeforge-0.3.0.dist-info/entry_points.txt +6 -0
- spikeforge-0.3.0.dist-info/licenses/LICENSE +30 -0
- spikeforge-0.3.0.dist-info/top_level.txt +3 -0
main.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Entry point: train a rate-coded MNIST subset and export all visuals."""
|
|
2
|
+
|
|
3
|
+
from spikeforge.exporters.presentation_exporter import (
|
|
4
|
+
PresentationGifExporter,
|
|
5
|
+
)
|
|
6
|
+
from spikeforge.exporters.raster_exporter import RasterExporter
|
|
7
|
+
from spikeforge.exporters.reconstruction_exporter import (
|
|
8
|
+
ReconstructionExporter,
|
|
9
|
+
)
|
|
10
|
+
from spikeforge.exporters.spike_gif_exporter import SpikeGifExporter
|
|
11
|
+
from spikeforge.exporters.video_exporter import VideoExporter
|
|
12
|
+
from spikeforge.training.logger import SNNTrainerLogger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
"""Build the trainer and write every output artifact."""
|
|
17
|
+
trainer = SNNTrainerLogger(animation_interval=100)
|
|
18
|
+
VideoExporter(trainer).export()
|
|
19
|
+
SpikeGifExporter(trainer).export()
|
|
20
|
+
ReconstructionExporter(trainer).export()
|
|
21
|
+
RasterExporter(trainer).export()
|
|
22
|
+
PresentationGifExporter(trainer).export()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
main_encodings.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Run the additional tutorial-1 encoding demos (latency/delta/random).
|
|
2
|
+
|
|
3
|
+
Produces latency curve + rasters + animation, delta plots, and random
|
|
4
|
+
spike visuals under build/.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from spikeforge.encoding.delta_trainer import DeltaTrainer
|
|
8
|
+
from spikeforge.encoding.latency_trainer import LatencyTrainer
|
|
9
|
+
from spikeforge.encoding.random_spikegen import RandomSpikeGenerator
|
|
10
|
+
from spikeforge.exporters.delta_exporter import DeltaPlotExporter
|
|
11
|
+
from spikeforge.exporters.latency_curve_exporter import (
|
|
12
|
+
LatencyCurveExporter,
|
|
13
|
+
)
|
|
14
|
+
from spikeforge.exporters.latency_raster_exporter import (
|
|
15
|
+
LatencyRasterExporter,
|
|
16
|
+
)
|
|
17
|
+
from spikeforge.exporters.latency_video_exporter import (
|
|
18
|
+
LatencyVideoExporter,
|
|
19
|
+
)
|
|
20
|
+
from spikeforge.exporters.random_spike_raster_exporter import (
|
|
21
|
+
RandomSpikeRasterExporter,
|
|
22
|
+
)
|
|
23
|
+
from spikeforge.exporters.random_spike_video_exporter import (
|
|
24
|
+
RandomSpikeVideoExporter,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
"""Train the latency/delta/random encoders and export their visuals."""
|
|
30
|
+
latency_trainer = LatencyTrainer(animation_interval=100)
|
|
31
|
+
LatencyCurveExporter(latency_trainer).export()
|
|
32
|
+
LatencyRasterExporter(latency_trainer).export()
|
|
33
|
+
LatencyVideoExporter(latency_trainer).export(key="clip")
|
|
34
|
+
|
|
35
|
+
DeltaPlotExporter(DeltaTrainer()).export()
|
|
36
|
+
|
|
37
|
+
random_spikegen = RandomSpikeGenerator(num_steps=100)
|
|
38
|
+
RandomSpikeRasterExporter(random_spikegen).export()
|
|
39
|
+
RandomSpikeVideoExporter(random_spikegen).export()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
spikeforge/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Public exports for the spikeforge package."""
|
|
2
|
+
|
|
3
|
+
from spikeforge.encoding.delta_trainer import DeltaTrainer
|
|
4
|
+
from spikeforge.encoding.latency_trainer import (
|
|
5
|
+
LatencyTrainer,
|
|
6
|
+
convert_to_time,
|
|
7
|
+
)
|
|
8
|
+
from spikeforge.encoding.random_spikegen import RandomSpikeGenerator
|
|
9
|
+
from spikeforge.training.logger import SNNTrainerLogger
|
|
10
|
+
from spikeforge.training.trainer import SSNTrainer
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SSNTrainer",
|
|
14
|
+
"SNNTrainerLogger",
|
|
15
|
+
"LatencyTrainer",
|
|
16
|
+
"DeltaTrainer",
|
|
17
|
+
"RandomSpikeGenerator",
|
|
18
|
+
"convert_to_time",
|
|
19
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Performance and memory benchmarks for the interpreter's execution modes.
|
|
2
|
+
|
|
3
|
+
This package measures wall time and memory of a topology run under
|
|
4
|
+
production and educational modes (and, opt-in, a compiled production path).
|
|
5
|
+
It is deliberately separate from ``introspection``, which describes a
|
|
6
|
+
recorded trajectory rather than the cost of producing one.
|
|
7
|
+
|
|
8
|
+
Use :func:`run_benchmark` from Python, or ``python -m
|
|
9
|
+
spikeforge.benchmark`` from a shell; both return the same
|
|
10
|
+
JSON-serialisable report.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from spikeforge.benchmark.compare import (
|
|
14
|
+
DEFAULT_THRESHOLD,
|
|
15
|
+
compare_runs,
|
|
16
|
+
exit_code,
|
|
17
|
+
)
|
|
18
|
+
from spikeforge.benchmark.config import BenchmarkConfig, default_config
|
|
19
|
+
from spikeforge.benchmark.harness import run_benchmark
|
|
20
|
+
from spikeforge.benchmark.serving import (
|
|
21
|
+
ServingBenchmarkConfig,
|
|
22
|
+
percentile,
|
|
23
|
+
run_serving_benchmark,
|
|
24
|
+
run_serving_suite,
|
|
25
|
+
)
|
|
26
|
+
from spikeforge.benchmark.store import BenchmarkStore, default_directory
|
|
27
|
+
from spikeforge.benchmark.suite import run_suite, with_metadata
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BenchmarkConfig",
|
|
31
|
+
"BenchmarkStore",
|
|
32
|
+
"DEFAULT_THRESHOLD",
|
|
33
|
+
"ServingBenchmarkConfig",
|
|
34
|
+
"compare_runs",
|
|
35
|
+
"default_config",
|
|
36
|
+
"default_directory",
|
|
37
|
+
"exit_code",
|
|
38
|
+
"percentile",
|
|
39
|
+
"run_benchmark",
|
|
40
|
+
"run_serving_benchmark",
|
|
41
|
+
"run_serving_suite",
|
|
42
|
+
"run_suite",
|
|
43
|
+
"with_metadata",
|
|
44
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Module entry point: ``python -m spikeforge.benchmark``.
|
|
2
|
+
|
|
3
|
+
The parser and handlers live in
|
|
4
|
+
:mod:`spikeforge.benchmark.cli`; this module only exposes ``main`` so
|
|
5
|
+
the module runner and the ``spikeforge-benchmark`` console script share
|
|
6
|
+
one path.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from spikeforge.benchmark.cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
sys.exit(main())
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Command-line interface for the benchmark suite.
|
|
2
|
+
|
|
3
|
+
``python -m spikeforge.benchmark`` (or the ``spikeforge-benchmark`` script)
|
|
4
|
+
runs a fixture and prints JSON. ``--save``/``--list`` persist and list runs
|
|
5
|
+
through the :class:`~spikeforge.benchmark.store.BenchmarkStore`, and
|
|
6
|
+
``--compare`` diffs a stored baseline against a fresh or stored run. When
|
|
7
|
+
``--fail-on-regression`` is given the command exits non-zero on a regression,
|
|
8
|
+
so it works as a CI gate.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
16
|
+
|
|
17
|
+
from spikeforge.benchmark.compare import (
|
|
18
|
+
DEFAULT_THRESHOLD,
|
|
19
|
+
compare_runs,
|
|
20
|
+
exit_code,
|
|
21
|
+
)
|
|
22
|
+
from spikeforge.benchmark.config import BenchmarkConfig
|
|
23
|
+
from spikeforge.benchmark.harness import run_benchmark
|
|
24
|
+
from spikeforge.benchmark.serving import (
|
|
25
|
+
ServingBenchmarkConfig,
|
|
26
|
+
run_serving_benchmark,
|
|
27
|
+
run_serving_suite,
|
|
28
|
+
)
|
|
29
|
+
from spikeforge.benchmark.store import BenchmarkStore
|
|
30
|
+
from spikeforge.benchmark.suite import with_metadata
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _add_fixture_args(parser: argparse.ArgumentParser) -> None:
|
|
34
|
+
"""Register the fixture-shaping arguments."""
|
|
35
|
+
parser.add_argument("--topology", action="append", dest="topologies")
|
|
36
|
+
parser.add_argument("--batch-size", type=int, default=2)
|
|
37
|
+
parser.add_argument("--steps", type=int, default=8)
|
|
38
|
+
parser.add_argument("--repeats", type=int, default=3)
|
|
39
|
+
parser.add_argument("--warmup", type=int, default=1)
|
|
40
|
+
parser.add_argument("--seed", type=int, default=0)
|
|
41
|
+
parser.add_argument("--device", default="auto")
|
|
42
|
+
parser.add_argument("--compiled", action="store_true")
|
|
43
|
+
parser.add_argument("--no-backward", action="store_true")
|
|
44
|
+
parser.add_argument("--energy", action="store_true")
|
|
45
|
+
parser.add_argument("--energy-target", dest="energy_target",
|
|
46
|
+
default="reference")
|
|
47
|
+
parser.add_argument("--out", default=None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _add_serving_args(parser: argparse.ArgumentParser) -> None:
|
|
51
|
+
"""Register the serving-mode arguments."""
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--serving",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="benchmark an in-process serving bundle instead of a topology",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--bundle",
|
|
59
|
+
default=None,
|
|
60
|
+
help="path to the .spkf deployment bundle served",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument("--calls", type=int, default=8)
|
|
63
|
+
parser.add_argument("--concurrency", type=int, default=1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _add_store_args(parser: argparse.ArgumentParser) -> None:
|
|
67
|
+
"""Register the store, listing, and comparison arguments."""
|
|
68
|
+
parser.add_argument("--save", action="store_true")
|
|
69
|
+
parser.add_argument("--label", default=None)
|
|
70
|
+
parser.add_argument("--store-dir", dest="store_dir", default=None)
|
|
71
|
+
parser.add_argument("--list", action="store_true", dest="list_runs")
|
|
72
|
+
parser.add_argument("--compare", default=None)
|
|
73
|
+
parser.add_argument("--against", default=None)
|
|
74
|
+
parser.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD)
|
|
75
|
+
parser.add_argument("--fail-on-regression", action="store_true")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parser() -> argparse.ArgumentParser:
|
|
79
|
+
"""Return the argument parser for the benchmark CLI."""
|
|
80
|
+
parser = argparse.ArgumentParser(
|
|
81
|
+
prog="spikeforge-benchmark",
|
|
82
|
+
description="Benchmark interpreter execution modes.",
|
|
83
|
+
)
|
|
84
|
+
_add_fixture_args(parser)
|
|
85
|
+
_add_serving_args(parser)
|
|
86
|
+
_add_store_args(parser)
|
|
87
|
+
return parser
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _config(args: argparse.Namespace) -> BenchmarkConfig:
|
|
91
|
+
"""Build a :class:`BenchmarkConfig` from parsed CLI arguments."""
|
|
92
|
+
return BenchmarkConfig(
|
|
93
|
+
topologies=tuple(args.topologies or ("fc_small",)),
|
|
94
|
+
batch_size=args.batch_size,
|
|
95
|
+
steps=args.steps,
|
|
96
|
+
repeats=args.repeats,
|
|
97
|
+
warmup=args.warmup,
|
|
98
|
+
seed=args.seed,
|
|
99
|
+
device=args.device,
|
|
100
|
+
compiled=args.compiled,
|
|
101
|
+
backward=not args.no_backward,
|
|
102
|
+
energy=args.energy,
|
|
103
|
+
energy_target=args.energy_target,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _serving_config(args: argparse.Namespace) -> ServingBenchmarkConfig:
|
|
108
|
+
"""Build a :class:`ServingBenchmarkConfig` from parsed arguments."""
|
|
109
|
+
if not args.bundle:
|
|
110
|
+
raise SystemExit("--serving requires --bundle PATH")
|
|
111
|
+
return ServingBenchmarkConfig(
|
|
112
|
+
bundle=args.bundle,
|
|
113
|
+
calls=args.calls,
|
|
114
|
+
concurrency=args.concurrency,
|
|
115
|
+
warmup=args.warmup,
|
|
116
|
+
seed=args.seed,
|
|
117
|
+
device=args.device,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _store(args: argparse.Namespace) -> BenchmarkStore:
|
|
122
|
+
"""Return the store configured by ``--store-dir``."""
|
|
123
|
+
return BenchmarkStore(args.store_dir)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _emit(payload: Mapping[str, Any], out: Optional[str]) -> int:
|
|
127
|
+
"""Print ``payload`` or write it to ``out``; return success."""
|
|
128
|
+
text = json.dumps(payload, indent=2)
|
|
129
|
+
if out:
|
|
130
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
131
|
+
else:
|
|
132
|
+
print(text)
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _run_list(args: argparse.Namespace) -> int:
|
|
137
|
+
"""Print the stored run summaries as JSON."""
|
|
138
|
+
return _emit({"runs": _store(args).list_runs()}, args.out)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _run_fixture(args: argparse.Namespace) -> int:
|
|
142
|
+
"""Run the fixture, optionally save it, and emit the report."""
|
|
143
|
+
report = run_benchmark(_config(args))
|
|
144
|
+
if args.save:
|
|
145
|
+
report = with_metadata(report)
|
|
146
|
+
report["run_id"] = _store(args).save(report, label=args.label)
|
|
147
|
+
return _emit(report, args.out)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _run_serving(args: argparse.Namespace) -> int:
|
|
151
|
+
"""Run the serving fixture, optionally save it, and emit the report."""
|
|
152
|
+
config = _serving_config(args)
|
|
153
|
+
if args.save:
|
|
154
|
+
report = run_serving_suite(
|
|
155
|
+
config, store=_store(args), label=args.label or "serving"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
report = run_serving_benchmark(config)
|
|
159
|
+
return _emit(report, args.out)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _candidate(args: argparse.Namespace) -> Dict[str, Any]:
|
|
163
|
+
"""Return a stored run when ``--against`` is set, else a fresh run."""
|
|
164
|
+
if args.against:
|
|
165
|
+
return _store(args).load(args.against)
|
|
166
|
+
if args.serving:
|
|
167
|
+
return run_serving_benchmark(_serving_config(args))
|
|
168
|
+
return run_benchmark(_config(args))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _run_compare(args: argparse.Namespace) -> int:
|
|
172
|
+
"""Compare a baseline run and print the deltas plus a status."""
|
|
173
|
+
baseline = _store(args).load(args.compare)
|
|
174
|
+
result = compare_runs(baseline, _candidate(args), args.threshold)
|
|
175
|
+
print(json.dumps(result, indent=2))
|
|
176
|
+
return exit_code(result) if args.fail_on_regression else 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
180
|
+
"""Parse ``argv`` and dispatch list, compare, or a fixture run."""
|
|
181
|
+
args = _parser().parse_args(argv)
|
|
182
|
+
if args.list_runs:
|
|
183
|
+
return _run_list(args)
|
|
184
|
+
if args.compare:
|
|
185
|
+
return _run_compare(args)
|
|
186
|
+
if args.serving:
|
|
187
|
+
return _run_serving(args)
|
|
188
|
+
return _run_fixture(args)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
sys.exit(main())
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Compare two benchmark records and flag regressions past a threshold.
|
|
2
|
+
|
|
3
|
+
The comparator matches records on ``(topology, mode)`` and reports the
|
|
4
|
+
relative change of the throughput, latency, and memory metrics. A candidate
|
|
5
|
+
regresses when a throughput metric moves the wrong way by more than the
|
|
6
|
+
threshold, or when its peak memory or p99 latency grows by more than it. The
|
|
7
|
+
result is JSON-able, and :func:`exit_code` turns it into a CI gate.
|
|
8
|
+
|
|
9
|
+
Serving records (:mod:`spikeforge.benchmark.serving`) carry an extra
|
|
10
|
+
``serving`` block; its ``p99_ms`` and ``throughput_per_second`` are compared
|
|
11
|
+
here too, and are simply absent (and therefore skipped) on training records.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
#: Default relative change (fraction) that counts as a regression.
|
|
17
|
+
DEFAULT_THRESHOLD = 0.10
|
|
18
|
+
|
|
19
|
+
#: (reported metric, direction) where "lower" means a larger value is worse.
|
|
20
|
+
_METRICS: Tuple[Tuple[str, str], ...] = (
|
|
21
|
+
("ms_per_step", "lower"),
|
|
22
|
+
("steps_per_second", "higher"),
|
|
23
|
+
("p99_ms", "lower"),
|
|
24
|
+
("throughput_per_second", "higher"),
|
|
25
|
+
("memory_bytes", "lower"),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
#: Serving metrics read from the record's ``serving`` block, not ``forward``.
|
|
29
|
+
_SERVING_KEYS: Tuple[str, ...] = ("p99_ms", "throughput_per_second")
|
|
30
|
+
|
|
31
|
+
#: Memory keys probed in order; the first non-zero one is compared.
|
|
32
|
+
_MEMORY_KEYS = (
|
|
33
|
+
"tracemalloc_peak_bytes",
|
|
34
|
+
"process_rss_bytes",
|
|
35
|
+
"cuda_peak_bytes",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _case_key(record: Mapping[str, Any]) -> Tuple[str, str]:
|
|
40
|
+
"""Return the ``(topology, mode)`` key a record is matched on."""
|
|
41
|
+
return str(record.get("topology")), str(record.get("mode"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _memory(record: Mapping[str, Any]) -> Optional[float]:
|
|
45
|
+
"""Return the first non-zero memory metric for a record, or None."""
|
|
46
|
+
block = record.get("memory") or {}
|
|
47
|
+
for key in _MEMORY_KEYS:
|
|
48
|
+
value = block.get(key)
|
|
49
|
+
if value:
|
|
50
|
+
return float(value)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _value(record: Mapping[str, Any], metric: str) -> Optional[float]:
|
|
55
|
+
"""Return the numeric value of ``metric`` for a record, or None."""
|
|
56
|
+
if metric == "memory_bytes":
|
|
57
|
+
return _memory(record)
|
|
58
|
+
if metric in _SERVING_KEYS:
|
|
59
|
+
value = (record.get("serving") or {}).get(metric)
|
|
60
|
+
return None if value is None else float(value)
|
|
61
|
+
key = "mean_ms_per_step" if metric == "ms_per_step" else metric
|
|
62
|
+
value = (record.get("forward") or {}).get(key)
|
|
63
|
+
return None if value is None else float(value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _change(
|
|
67
|
+
candidate: Optional[float], baseline: Optional[float]
|
|
68
|
+
) -> Optional[float]:
|
|
69
|
+
"""Return the relative change from baseline to candidate, or None."""
|
|
70
|
+
if not baseline or candidate is None or baseline is None:
|
|
71
|
+
return None
|
|
72
|
+
return (candidate - baseline) / baseline
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _regressed(
|
|
76
|
+
direction: str, fraction: Optional[float], threshold: float
|
|
77
|
+
) -> bool:
|
|
78
|
+
"""Return True when ``fraction`` breaches ``threshold`` the wrong way."""
|
|
79
|
+
if fraction is None:
|
|
80
|
+
return False
|
|
81
|
+
if direction == "lower":
|
|
82
|
+
return fraction > threshold
|
|
83
|
+
return fraction < -threshold
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _entry(
|
|
87
|
+
metric: str,
|
|
88
|
+
direction: str,
|
|
89
|
+
baseline: Mapping[str, Any],
|
|
90
|
+
candidate: Mapping[str, Any],
|
|
91
|
+
threshold: float,
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
"""Return one metric comparison entry."""
|
|
94
|
+
base_value = _value(baseline, metric)
|
|
95
|
+
cand_value = _value(candidate, metric)
|
|
96
|
+
fraction = _change(cand_value, base_value)
|
|
97
|
+
return {
|
|
98
|
+
"metric": metric,
|
|
99
|
+
"baseline": base_value,
|
|
100
|
+
"candidate": cand_value,
|
|
101
|
+
"change_fraction": fraction,
|
|
102
|
+
"regressed": _regressed(direction, fraction, threshold),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _case(
|
|
107
|
+
baseline: Mapping[str, Any],
|
|
108
|
+
candidate: Mapping[str, Any],
|
|
109
|
+
threshold: float,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
"""Compare one matched topology/mode pair."""
|
|
112
|
+
entries = [
|
|
113
|
+
_entry(metric, direction, baseline, candidate, threshold)
|
|
114
|
+
for metric, direction in _METRICS
|
|
115
|
+
]
|
|
116
|
+
return {
|
|
117
|
+
"topology": baseline.get("topology"),
|
|
118
|
+
"mode": baseline.get("mode"),
|
|
119
|
+
"metrics": entries,
|
|
120
|
+
"regressed": any(entry["regressed"] for entry in entries),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _identity(record: Mapping[str, Any]) -> Dict[str, Any]:
|
|
125
|
+
"""Return the identifying fields of a run."""
|
|
126
|
+
return {
|
|
127
|
+
"run_id": record.get("run_id"),
|
|
128
|
+
"label": record.get("label"),
|
|
129
|
+
"created_at": record.get("created_at"),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _name(case: Mapping[str, Any]) -> str:
|
|
134
|
+
"""Return a ``topology:mode`` label for a regressed case."""
|
|
135
|
+
return f"{case['topology']}:{case['mode']}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _pairs(
|
|
139
|
+
baseline: Mapping[str, Any], candidate: Mapping[str, Any]
|
|
140
|
+
) -> List[Any]:
|
|
141
|
+
"""Return matched ``(baseline, candidate)`` records in baseline order."""
|
|
142
|
+
base_map = {_case_key(r): r for r in baseline.get("results", [])}
|
|
143
|
+
cand_map = {_case_key(r): r for r in candidate.get("results", [])}
|
|
144
|
+
return [
|
|
145
|
+
(base_map[key], cand_map[key])
|
|
146
|
+
for key in base_map
|
|
147
|
+
if key in cand_map
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _result(
|
|
152
|
+
cases: List[Dict[str, Any]],
|
|
153
|
+
threshold: float,
|
|
154
|
+
baseline: Mapping[str, Any],
|
|
155
|
+
candidate: Mapping[str, Any],
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""Assemble the JSON-able comparison result."""
|
|
158
|
+
regressions: List[str] = [
|
|
159
|
+
_name(case) for case in cases if case["regressed"]
|
|
160
|
+
]
|
|
161
|
+
return {
|
|
162
|
+
"threshold": float(threshold),
|
|
163
|
+
"compared": len(cases),
|
|
164
|
+
"cases": cases,
|
|
165
|
+
"regressions": regressions,
|
|
166
|
+
"regressed": bool(regressions),
|
|
167
|
+
"baseline": _identity(baseline),
|
|
168
|
+
"candidate": _identity(candidate),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def compare_runs(
|
|
173
|
+
baseline: Mapping[str, Any],
|
|
174
|
+
candidate: Mapping[str, Any],
|
|
175
|
+
threshold: float = DEFAULT_THRESHOLD,
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
"""Return per-config deltas and whether ``candidate`` regressed.
|
|
178
|
+
|
|
179
|
+
``baseline`` and ``candidate`` are reports from
|
|
180
|
+
:func:`~spikeforge.benchmark.harness.run_benchmark` (or a stored
|
|
181
|
+
run). Only configs present in both are compared.
|
|
182
|
+
"""
|
|
183
|
+
cases = [
|
|
184
|
+
_case(base, cand, threshold)
|
|
185
|
+
for base, cand in _pairs(baseline, candidate)
|
|
186
|
+
]
|
|
187
|
+
return _result(cases, threshold, baseline, candidate)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def exit_code(result: Mapping[str, Any]) -> int:
|
|
191
|
+
"""Return the process exit status: 1 on regression, else 0."""
|
|
192
|
+
return 1 if result.get("regressed") else 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Configuration for a topology benchmark run.
|
|
2
|
+
|
|
3
|
+
The benchmark lives in its own package, not under ``introspection``: it
|
|
4
|
+
measures wall time and memory of an execution run rather than describing a
|
|
5
|
+
recorded trajectory, and it exposes its own tiny, JSON-returning fixture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class BenchmarkConfig:
|
|
14
|
+
"""A small, explicit benchmark fixture.
|
|
15
|
+
|
|
16
|
+
``topologies`` are registry topology names. ``steps`` and ``batch_size``
|
|
17
|
+
shape the random spike input, ``seed`` fixes both the module weights and
|
|
18
|
+
the spikes, and ``warmup`` untimed runs precede ``repeats`` timed samples.
|
|
19
|
+
``compiled`` additionally measures a ``torch.compile`` production path and
|
|
20
|
+
``backward`` measures a backward pass where one is meaningful. ``energy``
|
|
21
|
+
(default off) additionally attaches an event-driven energy estimate for
|
|
22
|
+
``energy_target`` to every record; the fields are additive.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
topologies: Tuple[str, ...] = ("fc_small",)
|
|
26
|
+
batch_size: int = 2
|
|
27
|
+
steps: int = 4
|
|
28
|
+
repeats: int = 2
|
|
29
|
+
warmup: int = 1
|
|
30
|
+
seed: int = 0
|
|
31
|
+
device: str = "auto"
|
|
32
|
+
compiled: bool = False
|
|
33
|
+
backward: bool = True
|
|
34
|
+
energy: bool = False
|
|
35
|
+
energy_target: str = "reference"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_config() -> BenchmarkConfig:
|
|
39
|
+
"""Return the tiny default fixture so tests and CI stay fast."""
|
|
40
|
+
return BenchmarkConfig()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Opt-in event-driven energy block for the benchmark harness.
|
|
2
|
+
|
|
3
|
+
Kept out of :mod:`spikeforge.benchmark.harness` so that module stays
|
|
4
|
+
within the project's file-length limit; the harness calls this only when its
|
|
5
|
+
config asks for energy, and the returned block is additive on the record.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from spikeforge.benchmark.config import BenchmarkConfig
|
|
13
|
+
from spikeforge.topology.stage_module import StageModule
|
|
14
|
+
from spikeforge_targets.energy.accounting import account
|
|
15
|
+
from spikeforge_targets.event_runtime.sparse_runner import sparse_run
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def energy_block(
|
|
19
|
+
module: StageModule, spikes: torch.Tensor, config: BenchmarkConfig
|
|
20
|
+
) -> Optional[Dict[str, Any]]:
|
|
21
|
+
"""Return the opt-in event-driven energy estimate, or None."""
|
|
22
|
+
if not config.energy:
|
|
23
|
+
return None
|
|
24
|
+
with torch.no_grad():
|
|
25
|
+
result = sparse_run(module, spikes)
|
|
26
|
+
report = account(result, config.energy_target)
|
|
27
|
+
return {
|
|
28
|
+
"report": report.to_dict(),
|
|
29
|
+
"counts": dict(result.counts),
|
|
30
|
+
"density": result.density,
|
|
31
|
+
}
|