Services

A service computes something on a machine that can afford it: a tracked bounding box or a depth map an observation is enriched with, or a policy's next action. An observation carries a service's result as an ordinary CalculatedField, so skills, the recorder and the controller GUI read it like any sensor. A policy is a service with a standard API that a skill drives.

A service plugin ships three pieces:

  • a ServiceServer subclass, the HTTP server that holds the model (a policy plugin's is a PolicyServer);
  • the caller: a ServiceProcessor subclass, which a CalculatedField declares, or a skill driving a PolicyClient;
  • a Service class describing the server, advertised under the solarian.services entry-point group.
[project.entry-points."solarian.services"]
sam2-tracker = "solarian_sam2.service:Sam2TrackerService"

Defined in solarian/services/.

The server: block

A conf that needs a server says which, in a server: block of the same shape whether it is a processor's conf in conf/services/ or a skill's in conf/skills/:

# conf/services/bbox_tracker.yaml
server_url: ${deployment.bbox_tracker_url}
token: ${oc.select:deployment.solarian_base_endpoint_token,null}
server:
  service: sam2-tracker
  port: 8765
  args: [--model, small]
# conf/skills/umi_target.yaml
policy_server_url: ${deployment.umi_target_policy_server_url}
policy_server_token: ${oc.select:deployment.solarian_base_endpoint_token,null}
server:
  service: diffusion-policy
  managed: false
  checkpoint: ${oc.select:deployment.umi_target_policy_checkpoint,null}
  host: 0.0.0.0
  port: 8003
  • Name
    service
    Type
    str
    Description

    The solarian.services entry point to run. Only a skill's managed launch can do without it, falling back to the skill's policy_service.

  • Name
    checkpoint
    Type
    str
    Description

    The checkpoint the server loads, passed with the service's checkpoint_arg. Required by a service that takes one, refused by one that does not.

  • Name
    host, port
    Type
    str, int
    Description

    Where the server binds. Default: 0.0.0.0 and the service's default_port.

  • Name
    args
    Type
    list[str]
    Description

    Passed to the server unchanged.

  • Name
    managed
    Type
    bool
    Description

    Whether a skill launches this server itself, next to it, when it starts. Default: false.

solarian run-service, a skill's managed server and Solarian Base all resolve this one block, so they cannot disagree on what a conf serves.

Running a service

solarian run-service bbox_tracker                     # the conf's server
solarian run-service umi_target --checkpoint models/pick/checkpoints/latest.ckpt
solarian run-service sam2-tracker --model small       # an installed service, with your own args

Name a conf (its name, filename or path) to run the server its server: block describes, or an installed service to run it with your own arguments. --checkpoint, --host and --port override the conf; every other argument goes to the server, after the conf's args. A name that matches both a service conf and a skill conf is an error that lists both. sam2-tracker downloads the chosen SAM 2.1 checkpoint into --checkpoint-dir when the file is not already there.

The server runs in the foreground until you stop it with Ctrl+C. Point the conf's URL key at it, here bbox_tracker_url in your deployment, as http://<host>:8765 (http://localhost:8765 when the robot or relay runs on the same machine).

Every server answers GET /health and GET /info, plus its own routes, and stamps each response with X-Server-Ms, the time it spent, so a client can tell network time from compute.

Auth

With SOLARIAN_ENDPOINT_TOKEN set in its environment, a server answers only requests carrying Authorization: Bearer <token>; without it, the server is open. A client takes the token from its conf: token for a processor, policy_server_token for a skill. Reading it as ${oc.select:deployment.solarian_base_endpoint_token,null} leaves it null on your own network and picks up the token of Base's endpoints from the robot's connection file.

Declaring the field

from solarian.core.data import BoundingBoxData
from solarian.core.observation import CalculatedField, ModuleObs, SensorField
from solarian_sam2 import Sam2BboxProcessor

class RightWristCameraObs(ModuleObs):
    image = SensorField(MyRobot.right_wrist_camera.camera.rgb_image)
    bbox = CalculatedField(
        inputs=[MyRobot.right_wrist_camera.camera.rgb_image],
        processor=Sam2BboxProcessor,
        output=BoundingBoxData(),
        conf="conf/services/bbox_tracker.yaml",
    )

With no execution_place= argument, the field reads where it runs from its conf, so one deployment key moves it between the relay and the robot:

# conf/services/bbox_tracker.yaml
execution_place: ${deployment.umi_target_execution_place}
server_url: ${deployment.bbox_tracker_url}
resize: 224

Every host reads the same conf and deployment, so they always agree on where the field is computed. The keys have no fallback: a deployment that does not set them fails at startup, naming the missing key. A skill that reads the field, such as a UMISkill with targets, takes its own execution_place from the same key, so the two cannot be placed apart. The host that computes it builds the processor. The others carry the field as zeros until it arrives.

Starting and stopping

A service that needs a command to begin, such as a tracker that needs a target, does nothing until it is started. The controller starts it by field, with any parameters the processor takes:

from solarian.core.event import Event, EventType, event_handler

class MyController(Controller):
    @event_handler(EventType.CLICK)
    def track_clicked_object(self, event: Event) -> None:
        if event.data["field"] != "image":
            return
        self.start_service(event.data["module"], "bbox", point=event.data["point"])

start_service and stop_service send START_SERVICE / STOP_SERVICE actions with {module, field, params}. The relay applies them when it computes that field and forwards them to the robot otherwise, where the module that owns the field calls its processor's start(params) / stop(). A second start_service restarts the processor with the new parameters.

CLICK events come from the controller GUI's MouseModule. point is in image pixels, already mapped back from however the frame was scaled for display, and module / field name the observation the image was.

Latency

A processor is called inline, once per observation, on the path that forwards it. ServiceProcessor therefore never calls the server there. A worker thread takes the newest frame from a one-slot buffer, calls the service, and publishes the result stamped with the timestamp of the frame it was computed from. The field lags the image by one round trip, and the observation stream never slows to the service's rate. While stopped, the field is zeros.

Each processor reports <label> call (ms) and <label> age (ms) on the StatusBoard. Comparing those between a relay-computed and a robot-computed run shows what each placement costs.

Policies

A policy server is a ServiceServer with one standard API, so a skill drives diffusion, ACT or a classical planner through the same PolicyClient:

RouteDoes
POST /predict{"obs_dict": {...}, "kwargs": {...}} to {"action": [...], "raw_action": ..., "info": ...}
POST /resetClear episode state
POST /reloadReload from disk
GET /model_info, GET /shape_metaWhat the policy is and the observations it expects

Its Service sets checkpoint_arg, which makes it a service that takes a checkpoint: what run-service and Base call a policy. See Policies for writing one.

Writing a service

from solarian.services import Service, ServiceProcessor, ServiceServer

class MyServer(ServiceServer):
    title = "my-service"

    def add_routes(self, app):
        @app.post("/infer")
        def infer(payload: dict):
            ...

class MyProcessor(ServiceProcessor):
    def on_start(self, frame, params):   # first result, or None
        ...
    def on_frame(self, frame):           # result for frame, or None to keep the last
        ...
    def on_stop(self):
        ...

class MyService(Service):
    server_module = "my_plugin.server"   # calls MyServer.main()
    processor_cls = MyProcessor
    default_port = 8770

ServiceClient wraps the HTTP calls, with the bearer token and connect retries, and solarian.utils.wire encodes numpy arrays in JSON payloads. Service.checkpoint_arg names the server's flag for a checkpoint (--input for a policy). solarian-sam2 is a complete example.

See also

Was this page helpful?