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
ServiceServersubclass, the HTTP server that holds the model (a policy plugin's is aPolicyServer); - the caller: a
ServiceProcessorsubclass, which aCalculatedFielddeclares, or a skill driving aPolicyClient; - a
Serviceclass describing the server, advertised under thesolarian.servicesentry-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.servicesentry point to run. Only a skill's managed launch can do without it, falling back to the skill'spolicy_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.0and the service'sdefault_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:
| Route | Does |
|---|---|
POST /predict | {"obs_dict": {...}, "kwargs": {...}} to {"action": [...], "raw_action": ..., "info": ...} |
POST /reset | Clear episode state |
POST /reload | Reload from disk |
GET /model_info, GET /shape_meta | What 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
- Policies: writing a policy server and the skill side of the policy API.
- Solarian Base: Services, Policies & Models: run them on Base's GPUs.
- Server: how the relay computes server-side fields and hosts skills.
- Observations:
CalculatedFieldandExecutionPlace. - Events:
CLICKand the other controller events.