Architecture ============ Overview -------- softs uses a **marketplace** architecture. Clients place orders (requests for a concrete ``product_id`` string), suppliers register one or more **regex patterns** describing what they can serve, and the broker dispatches each order to a ready supplier whose pattern matches the order's ``product_id``. Data flows through a pluggable transfer medium - the broker only routes the opaque ``(address, offset)`` location and never touches the payload. The "products" are defined before hand via configs that form a "contract" between clients and suppliers: defined from a single spec/file. .. mermaid:: graph LR C1[Client 1] & C2[Client 2] <-->|ORDER / FULFILLED| FE subgraph Broker FE[Frontend
ROUTER] <--> Q[Per-product order queues
+ supplier pools] <--> BE[Backend
ROUTER] end BE <-->|WORK / DONE| W1[Supplier A
products: v1] & W2[Supplier B
products: v1, v2] Broker ------ Two ZMQ ROUTER sockets: a frontend for clients and a backend for suppliers. A single background poll thread services both. Orders are the single source of truth, indexed for dispatch by: - a per-product FIFO queue of pending orders, - a single FIFO pool of ready suppliers (each carrying its compiled patterns), - the in-flight order assigned to each busy supplier. Client commands (frontend): ``HELLO``, ``ORDER``, ``CANCEL``, ``DISCARD``, ``STATS``. The broker pushes ``FULFILLED`` (or ``FAILED``, after ``max_order_attempts``) back to the client. Supplier commands (backend): ``HELLO``, ``READY``, ``DONE``, ``GOODBYE``. The broker pushes ``WORK`` to a supplier. .. _routing-by-pattern: Routing by pattern ~~~~~~~~~~~~~~~~~~~ A supplier's ``product_ids`` are **regex patterns**, not literal ids. On HELLO the broker compiles them; to dispatch a queued order it scans the ready pool for the first supplier whose pattern ``re.fullmatch``-es the order's concrete ``product_id``. A pattern with no regex metacharacters (``layer_3``) therefore behaves as an exact match, while ``layer_\d+`` or ``.*`` lets one supplier serve a whole family of ids without enumerating them. Because the ready pool holds each supplier once, a supplier is dispatched a single order at a time and only re-enters the pool when it sends ``READY`` again after ``DONE``. Supplier -------- 1. Connects to the backend (DEALER), sends HELLO with its ``product_ids`` (regex patterns it can serve) 2. Sends READY 3. Receives WORK with ``{order_id, product_id, address, offset}`` 4. Calls ``generator_fn(product_id)`` -> bytes 5. Writes to the medium via ``medium_cls.attach(address).write(offset, data)`` 6. Sends DONE with ``{order_id, success}``, then READY again 7. On failure: ``success=False`` -> broker re-queues to another supplier 8. On exit: sends GOODBYE -> broker removes it immediately Client ------ 1. Creates a transfer medium (one segment, multiple slots) 2. Connects to the frontend (DEALER), sends HELLO 3. Sends ORDER with ``{order_id, product_id, address, offset}`` 4. Receives FULFILLED with ``{order_id}`` when the slot is filled 5. Reads from the medium, releases the slot 6. DISCARD cancels all pending orders, CANCEL cancels one Message Flow ------------ .. mermaid:: sequenceDiagram participant C as Client participant B as Broker participant S as Supplier S->>B: HELLO(product_ids) B->>S: {ok} S->>B: READY C->>B: HELLO B->>C: {ok, peer_id} C->>B: ORDER(order_id, product_id, address, offset) B->>C: {ok} B->>S: WORK(order_id, product_id, address, offset) Note over S: generate + write to medium S->>B: DONE(order_id, success=true) B->>C: FULFILLED(order_id) Note over C: read from medium Cancel Flow ~~~~~~~~~~~ .. mermaid:: sequenceDiagram participant C as Client participant B as Broker C->>B: DISCARD Note over B: cancel client's QUEUED orders,
let DISPATCHED ones finish B->>C: {ok, cancelled=N, cancelled_ids=[...]} C->>B: CANCEL(order_id) Note over B: cancel only if still QUEUED B->>C: {ok} or {ok:false, already dispatched} Fencing model switches ~~~~~~~~~~~~~~~~~~~~~~~~ A switch must never serve a sample generated for the previous model. The client holds a **generation** counter, bumped on every ``discard()``. Each order records the generation it was placed in. On discard the broker cancels only orders it has not yet dispatched (no supplier has touched their slots); orders already dispatched are left to finish. The client keeps those slots quarantined until their ``FULFILLED`` arrives - and because they belong to an old generation, their data is dropped and the slot is freed rather than served. A supplier write can therefore never land in a slot that has been reused for the new model. Supplier Failure ~~~~~~~~~~~~~~~~ .. mermaid:: sequenceDiagram participant C as Client participant B as Broker participant S1 as Supplier 1 participant S2 as Supplier 2 B->>S1: WORK(order_id) Note over S1: generator_fn raises! S1->>B: DONE(order_id, success=false) Note over B: re-queue to another supplier B->>S2: WORK(order_id) Note over S2: generates successfully S2->>B: DONE(order_id, success=true) B->>C: FULFILLED(order_id) Fault Tolerance --------------- - **Supplier disconnect**: broker removes it from product pools, re-queues in-flight work - **Supplier failure**: ``success=False`` -> broker re-queues to another supplier - **Supplier graceful exit**: GOODBYE -> broker handles immediately - **Client disconnect**: broker cancels all its orders - **Broker down**: ``send_timeout_ms`` returns an error instead of hanging - **No shutdown ordering**: each component has its own ZMQ context with ``linger=0`` Transfer Mediums ---------------- The broker routes only the opaque ``(address, offset)`` - it never touches the data. .. mermaid:: graph LR C[Client] -->|"ORDER(address, offset)"| B[Broker] B -->|"WORK(address, offset)"| S[Supplier] C -->|creates| M[Medium] S -->|"attach + write"| M M -->|read| C - ``ShmMedium``: POSIX shared memory. Zero-copy. Default. - ``FilesystemMedium``: memory-mapped file. - ``TCPMedium``: TCP sockets. The client runs a server, suppliers connect. Custom mediums extend the ``Medium`` base class: ``write()``, ``read()``, ``attach()``, ``close()``, ``unlink()``. Capturing activations as products --------------------------------- A common supplier is a *teacher* that serves a neural network's intermediate activations as training samples. :class:`~softs.catcher.ModelIOCatcher` captures the inputs and/or outputs of named submodules during a forward pass, and the order's ``product_id`` selects what to capture. Capture specs ~~~~~~~~~~~~~ A ``product_id`` is a spec string parsed by :func:`~softs.catcher.parse_io_spec`:: inputs[model.layers.0]|outputs[model.layers.0] outputs[model.layers.5] inputs[]|outputs[model.layers.0|model.layers.5] The names are the dotted keys of ``model.named_modules()``. Items inside the brackets are ``|``-separated; the ``|`` between the ``inputs[...]`` and ``outputs[...]`` blocks is matched independently, so the inner and outer separators never clash. Early exit ~~~~~~~~~~ With ``early_exit=True`` the catcher stops the forward the instant every requested value is captured - so serving ``outputs[model.layers.0]`` only runs the model *through layer 0* and skips the rest of the network. Forwards are driven through :meth:`~softs.catcher.ModelIOCatcher.run` (not the model directly), which catches the internal stop signal per call; the model itself is never patched, so it composes with DDP and ``torch.compile``. Teacher supplier ~~~~~~~~~~~~~~~~ Because the broker routes by :ref:`pattern `, one teacher registers a single regex - e.g. ``r"inputs\[.*\]\|outputs\[.*\]"`` - and serves *any* layer spec a client asks for, without enumerating them. The supplier's ``generator_fn`` parses the ``product_id``, arms the catcher, runs one forward, and encodes the captured tensors to bytes:: def generate(product_id: str) -> bytes: with catcher.for_product(product_id, early_exit=True) as buf: catcher.run(**next(batches)) name = parse_io_spec(product_id)[1][0] return config.encode( x=buf[name]["inputs"][0]["args"][0][0].cpu(), y=buf[name]["outputs"][0][0].cpu(), ) See ``examples/distillation/qwen_layer0/`` for a minimal single-layer distillation (broker/supplier/client, fixed layer, no switching), and ``examples/distillation/layerwise/`` for the all-layers version (one regex pattern, ``early_exit``, and ``SoftDataLoader`` with per-layer ``set_model`` switching).