Jaal vs inference orchestration
Frameworks and servers that run models together: Ray, LangGraph, LlamaIndex, NVIDIA Dynamo and Triton, KServe, BentoML, Modal. Each product checked against its own documentation on 26 October 2026, sources below. Where we could not find a feature in a product's docs we write not documented: that means we did not find it, not that it is missing. For Jaal we say what is built and what is planned. Corrections are welcome.
The short version
A plain loop, checked by the compiler
Ray asks for .remote() futures, LangGraph for Send and edges, Triton for ensemble configs, KServe for YAML graphs. In Jaal you write a loop and mark it parallel; the compiler checks the iterations are independent and refuses the mark by name if not.
A model that has answered is never called again
Every model call is a recorded step: after a crash, or in a replay, the recorded answer stands in. Ray keeps no execution journal; LangGraph's time travel re-executes nodes from a checkpoint.
Every engine in its own process (being built)
vLLM, Triton, llama.cpp and plain CUDA as services of one orchestrator (being built; the CUDA core runs today on our CUDA simulator). A GPU server reached as its own endpoint cannot take the orchestrator down with it. The model servers are partners, not rivals.
Where others are ahead
Serving itself: NVIDIA Dynamo's disaggregated prefill and decode and KV-aware routing, Ray's scale-out, LangGraph's ecosystem. Jaal's inference orchestration is in progress; the four-engine demo is being built.
The same fan-out, two ways
Describe every photo in parallel, then summarize. Both simplified; the LangGraph version follows its own map-reduce pattern with Send (graph API docs).
LangGraph: declare the graph
class State(TypedDict):
photos: list
notes: Annotated[list, operator.add]
summary: str
def fan_out(state):
return [Send("describe", {"photo": p})
for p in state["photos"]]
def describe(item):
return {"notes": [describe_photo(item["photo"])]}
def summarize(state):
return {"summary": write_summary(state["notes"])}
builder = StateGraph(State)
builder.add_node("describe", describe)
builder.add_node("summarize", summarize)
builder.add_conditional_edges(START, fan_out, ["describe"])
builder.add_edge("describe", "summarize")
builder.add_edge("summarize", END)
graph = builder.compile()Jaal: write the loop, mark it parallel, the graph is derived
import jaal.x.Parallel;
public class Album extends jaal.x.Service<Photos, Summary> {
private jaal.x.Service<Photo, Note> describe;
private jaal.x.Service<Notes, Summary> summarize;
@Override public Summary invoke(Photos in) {
int n = in.urls().length;
String[] notes = new String[n];
for (@Parallel int i = 0; i < n; i++) {
Note note = describe.invoke(PhotoBuilder.photo().url(in.urls()[i]).build());
notes[i] = note.text();
}
return summarize.invoke(NotesBuilder.notes().texts(notes).build());
}
}
Every call recorded, a crash finished by another process, the graph drawn for you. Or ask the assistant for it in a sentence (in progress).
AI and inference orchestration
The model servers are partners more than rivals: Jaal is being built to orchestrate them, each in its own process. The orchestration layers are the comparison.
| Product | How you write it | Durability | Parallel work | Compensation | Replay and debugging | Versions | License and hosting |
|---|---|---|---|---|---|---|---|
| Jaal | Plain Java (Python in progress); model servers: being built | Every call recorded; a replay never calls the model again | Independent calls automatic; a parallel loop is one annotation, checked by the compiler | Declared (the effect contract) | Replay after a crash from the record; stepping through a run planned | Yes | Private beta |
| Ray / Ray Serve | @ray.remote tasks and actors; Serve deployments; Python | No execution journal; lineage reconstruction of lost objects | Explicit futures | none | not documented | n/a | Apache-2.0; Anyscale |
| LangGraph | Graph API (nodes and edges) or Functional API (@entrypoint, @task); Python, JS | Checkpointers (Postgres, SQLite) | Fan-out edges, Send, task futures | not documented | Time travel: replay from a checkpoint, fork | Latest code runs on saved state | MIT; LangSmith Deployment |
| LlamaIndex Workflows | Event-driven @step methods; Python | Ephemeral by default; snapshots by hand; optional DBOS runtime | Events, num_workers | not documented | not documented | not documented | MIT |
| NVIDIA Dynamo | Serving layer over vLLM, SGLang, TensorRT-LLM; Kubernetes deployment | Request migration on worker failure; no workflow persistence | Disaggregated prefill and decode, KV-aware routing | n/a | n/a | n/a | Apache-2.0; self-host |
| NVIDIA Triton | Ensembles in config.pbtxt; Business Logic Scripting in Python | none documented | Ensemble steps run when inputs are ready; async_exec | none | not documented | Per-model versions | BSD-3-Clause; self-host |
| KServe InferenceGraph | YAML: Sequence, Switch, Ensemble, Splitter nodes | none documented | Declarative: Ensemble node | none | not documented | not documented | Apache-2.0; Kubernetes |
| BentoML | Decorators @bentoml.service, @bentoml.api; Python | Task results kept 24 hours by default | not documented | none | not documented | not documented | Apache-2.0; part of Modular since Feb 2026 |
| Modal | Decorators @app.function, @app.cls; Python | Retries; crashed containers rescheduled | Explicit: .map(), .starmap() | none | not documented | not documented | Managed; client SDK Apache-2.0 |
Sources
Each product's own documentation, blog or repository, read on 26 October 2026.
- Ray: fault tolerance, model composition
- LangGraph: durable execution, time travel, compatibility
- LlamaIndex Workflows: durable workflows
- NVIDIA Dynamo: repository; Triton: ensembles, BLS
- KServe: InferenceGraph; BentoML: services, joining Modular; Modal: scaling, retries