Skip to content

Implementation Roadmap

Re-derived from the code on 2026-08-05, because the phase tables below answer a question nobody is asking any more.

Publishing has never been verified end to end. This is the one item that stands between the workspace and a version a stranger can install. release.yml publishes on a v* tag; until a tag runs green with the crates and the wheels actually landing, “it works” is a claim about a local checkout. Trusted publishing (OIDC) now replaces the stored tokens on both crates.io and PyPI, which means the publisher must be configured registry-side before the next tag.

WhatWhereBehaviour
mode="differentiable" with workerssoma-python/src/graph.rsRefused, and the message names the path that works. That mode is the local loop — the caller drives context/backward/step and owns when the parameters move — and driving it remotely would need distributed autograd. Training a differentiable graph on workers is set_strategy("data_parallel"), which is a complete round
A backward pass on a workersoma-worker/src/python_process.rsFixed 2026-08-05. A remote fit of a DifferentiableFilter runs forward/loss/backward and leaves the gradients on the parameters, so data_parallel trains. Verified against a hand-computed reference: same init, each shard’s gradient taken separately, the two averaged, one SGD step — an exact match, and different from what either shard alone gives
Gradients as an opaque torch blobsoma-worker/src/python_process.rsFixed 2026-08-05. They cross the wire as JSON. The aggregator is in Rust, and the mean of two pickles is not a thing that can be computed: the round died at the aggregation step having done all the work
Targets not shardedsoma-runtime/src/strategy.rsFixed 2026-08-05. shard_pair splits inputs and targets together. Sharding only x sent each replica the whole y — shapes that broadcast rather than fail, so every replica trained on pairs that were never pairs and the round reported success
ModelParallelsoma-runtime/src/strategy.rsWritten 2026-08-05. Partitions tile the graph and each is a stage on its pinned worker, threading the activation. A node claimed twice, claimed by nobody, or interleaved with another stage is refused rather than run
PopulationBasedsoma-runtime/src/strategy.rsRefuses by design, not for want of an implementation: every member needs different hyperparameters applied to the graph, and a worker is sent a plan rather than a way to build one. PBT lives where Study lives — soma.Pbt(...).run(train, evaluate), added 2026-08-05
run_pipeline, run_studysoma-mcp/src/exec.rsWritten 2026-08-05. They build the graph a model described out of the project’s own filters and run it in a Python subprocess rooted there. The claim that “the server cannot load user code” was true of the server and beside the point: soma-worker has always run Python in a subprocess
Seed dropped on the remote pathsoma-worker/src/ws_transport.rsFixed 2026-08-05. Transport::execute now takes the run’s seed and WsTransport puts it on the wire; it was hardcoded None, so a remote sweep shared one cache line across every seed

The empty filters list on that same transport is not a gap: the worker rebuilds a Python filter by unpickling SerializedFilter::pickled_filter, and those bytes live only in the Python layer. A NodeCatalog holds live filters and their states, never the pickle, so this transport cannot supply them and sending empty pickles would be worse than sending none. The path that can supply them builds its own SerializedPlan in soma-python/src/graph.rs.

The strategy layer is no longer a blank: Federated, DataParallel and ModelParallel all run across workers, with the caller in GraphSession::fit, a StrategyContext over one transport per worker, and FedAvg and AllReduce written. PopulationBased stays refused on purpose, with soma.Pbt as the thing that actually evolves a population.

One trap is worth knowing while developing against a worker: it builds an isolated venv per pipeline and installs somatize from PyPI, so a working tree ahead of the last release ran an older Soma on the worker than the one that pickled the filters. soma.Worker(...) now points its environments at the calling interpreter’s own package; the standalone binary takes SOMA_LOCAL_PACKAGE.

Deferred on purpose, with the seam in place

Section titled “Deferred on purpose, with the seam in place”

Documented where each belongs, not here: soma ui and the rest of the visualization deferrals (fANOVA importances, NodeProgress/ParetoUpdated emitters, a Python-implementable EventSink, parquet compaction), and the experiment-pool ones (warm-starting a study from the pool, dedup by cache key, ChronosVector as a real vector index once an Embedder exists).

Notebooks 06–09 are in Spanish — translated 2026-08-05. All fifteen are in English.

Phase 1: LabChain in Rust ← MVP
Phase 2: Distribution & Remote Execution
Phase 3: Memory, Knowledge Base & Agents

Each phase produces a usable, releasable product. Later phases build on earlier ones without rewriting.

Goal: A functional replacement for LabChain, written in Rust, usable from Python. Graphs with caching, optimization, and events.

TaskDescriptionPriority
Value enumTensor, Json, DataFrame, Bytes, VirtualP0
Filter traitfit/forward lifecycle with associated State typeP0
FilterMetakind, cacheable, differentiable, stream_modeP0
Graph, Node, EdgeGraph construction and validationP0
CacheKeyContent-addressable hash computationP0
CacheStore traitK/V interface for cache backendsP0
Event enumAll three levels (Run, Trial, Study)P0
SearchDimensionFloat, Int, Categorical, ConditionalP0
SearchSpaceAggregation, merge, freezeP0
Study, TrialOptimization typesP0
SchemaInput/output type descriptionsP1
VirtualValueLazy references (Materialized, Cached, Deferred)P1
SomaErrorError typesP0
Derive macros#[derive(SomaFilter)]P1
TaskDescriptionPriority
Topological sortKahn’s algorithm, cycle detectionP0
Linear graph compilationSequence of Execute nodesP0
Cache resolutionPer-node, at runtime, with materialized input in handP0
Cascade invalidationUpstream change invalidates downstreamP0
Parallel branch detectionFork-join pattern recognitionP1
Gradient flow analysisWarn on non-differentiable interruptionsP1
Schema validationType compatibility between filtersP1
Loop compilationLoop body extractionP2
Branch compilationConditional armsP2
Cost estimationFrom cache metadataP2
TaskDescriptionPriority
Sequential executorWalk Sequence plansP0
Event busAsync broadcast, subscribeP0
Memory cacheIn-memory HashMapP0
Local cacheFilesystem action store + BLAKE3 CASP0
Tiered cacheMulti-level with promotionP1
Parallel executorTokio JoinSet for Parallel plansP1
ContextStore + event emitter + metric reporterP0
Graph structfit/forward with cachingP0
Study runnerSample → build → execute → record loopP1
Grid samplerExhaustive searchP1
Random samplerRandom searchP1
Bayesian samplerTPE implementationP2
Median prunerMedian stopping ruleP2
HyperbandSuccessive halvingP2
Stream driverChunk processing with modes, through run_node’s primitivesDone
TaskDescriptionPriority
PyO3 module setupmaturin build, basic importsP0
Filter base classPython class with search() descriptorsP0
Graph classfit/forwardP0
Value wrappersTensor ↔ numpy, DataFrame ↔ polarsP0
Study classRun optimization from PythonP1
Event subscriptionPython callbacks for eventsP1
Search space displayPretty-print search spacesP1
from soma import Graph, Filter, Study, Bayesian, search
class MyScaler(Filter):
scale: float = search(0.1, 10.0, scale="log")
def fit(self, x, y=None):
return {"mean": x.mean(0), "std": x.std(0)}
def forward(self, x, state):
return (x - state["mean"]) / state["std"] * self.scale
g = Graph.somatize(MyScaler(scale=2.0) >> MyClassifier(C=1.0))
g.fit(x_train, y_train)
result = g.forward(x_test) # with automatic caching
study = Study(graph=g, strategy=Bayesian(n_trials=50))
study.run(x_train, y_train, x_val, y_val)
print(study.best_trial.params)

Goal: Execute graphs on remote workers. Shared caching across a lab.

TaskDescription
Worker daemonRegister, heartbeat, receive plans
ProtocolMessage types, serialization
Python loaderPyO3 dynamic loading of user filters
CapabilitiesGPU detection, resource reporting
TaskDescription
Distribution plannerAssign nodes to local/remote targets
Remote plan wrappingExecutionPlan::Remote variant
Serialized planFull plan + filter serialization
TaskDescription
Remote cacheS3 backend for CacheStore
Event relayWebSocket event streaming from workers
Plan coordinatorSchedule plans across workers
TaskDescription
lab.connect()Connect to a Soma lab
lab.run()Submit graphs for remote execution
lab.workers()List available workers
lab = soma.connect("https://my-lab.soma.dev")
lab.run(study, data=train_data) # executes on remote workers

Goal: Temporal experiment tracking and autonomous research agents.

TaskDescription
ChronosVector integrationImport as dependency or subcrate
ExperimentRecordIndexing, embedding generation
Semantic searchQuery by natural language
Trajectory analysisMetric evolution over time
Change point detectionBreakthrough identification
Promising linesTrend analysis and recommendations
TaskDescription
Agent structSoul, skills, hands, memory
Research loopHypothesize → build → execute → analyze → iterate
Graph generationLLM-driven graph construction
Report generationAutomatic documentation of findings
TaskDescription
Graph publishinglab.publish(graph)
Graph editor integrationGraphs as platform nodes
Visual graph editorDrag-and-drop filter composition
from soma.agent import Researcher
agent = Researcher(lab=lab, plan="Investigate normalization for TS classification")
report = agent.investigate(max_iterations=20)
kb = lab.knowledge_base()
kb.promising_lines()
kb.trajectory("rocket_znorm", metric="f1")

The recommended order for Phase 1, following TDD:

Week 1-2: soma-core types
1. SomaError
2. Value enum (without Tensor, just structure)
3. CacheKey (hash computation)
4. FilterMeta, FilterKind, StreamMode
5. Filter trait
6. Graph, Node, Edge
7. Event enum
8. SearchDimension, SearchSpace
9. CacheStore trait
10. Study, Trial, Objective
Week 3-4: soma-compiler
11. Topological sort
12. ExecutionPlan enum
13. Linear graph compilation
14. Cache key computation for graph
15. Cache resolution (runtime, per node)
16. Cascade invalidation
17. Parallel branch detection
Week 5-6: soma-runtime
18. Event bus
19. Context
20. Memory cache (HashMap)
21. Sequential executor
22. Graph (fit/forward)
23. Local cache (FsActionStore + BLAKE3 CAS)
24. Tiered cache
25. Parallel executor
Week 7-8: soma-runtime optimization
26. Grid sampler
27. Random sampler
28. Study runner
29. Metric reporting + pruning
30. Median pruner
Week 9-10: soma-python
31. PyO3 module setup
32. Filter base class
33. Graph class
34. Value wrappers (numpy interop)
35. Study class
36. search() descriptor
Week 11-12: Polish & release
37. Derive macros (#[derive(SomaFilter)])
38. Documentation site
39. Examples and tutorials
40. CI/CD pipeline
41. Publish to crates.io + PyPI
AreaChoiceRationale
Tensor backendCandle or BurnPure Rust, GPU support, autograd
DataFramePolarsFast, lazy evaluation, Rust-native
Async runtimeTokioIndustry standard, JoinSet for parallelism
Serializationserde + bincodeFast binary serialization for plans
Local cacheFsActionStoreBazel-style action cache + content-addressed blobs, no embedded DB to corrupt
Remote cacheS3-compatibleUniversal, works with MinIO locally
Python bindingsPyO3 + maturinStandard Rust-Python bridge
HTTP frameworkAxumFor worker daemon and coordinator
HashingSHA-256Deterministic, collision-resistant
TestingBuilt-in + proptestProperty-based testing for algorithms
Coveragecargo-tarpaulinStandard Rust coverage tool
DocsStarlight (Astro)This site