Skip to content

Commit fcb2a8a

Browse files
feat: add KServe and Triton inference adapters (#2908)
* feat: add KServe and Triton inference adapters * fix: allow remote backend without core transformers * test: support Python 3.10 TOML parsing --------- Co-authored-by: Maziyar Panahi <5762953+maziyarpanahi@users.noreply.github.com>
1 parent e54b6df commit fcb2a8a

25 files changed

Lines changed: 2582 additions & 19 deletions

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
optional Outlook MSG input.
3131
- Added committed Android OpenMedKit release-AAR and offline cold-start budgets,
3232
with blocking Gradle/CI gates and measured values in the Android job summary.
33+
- Added a Triton ONNX model-repository generator and configuration-selected
34+
KServe V2 HTTP/gRPC inference backend with local tokenization and decoding,
35+
mocked local/remote span-parity coverage, and no bundled serving runtime.
3336
- Added opt-in, no-PHI OpenTelemetry spans and aggregate histograms for all ten
3437
core privacy-pipeline stages, with lazy optional imports, no exporter by
3538
default, shared `Timer` measurements, synthetic leakage regression tests, and

‎NOTICE‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ License: Apache-2.0
2222
Usage: Planned structured-data profiling and quasi-identifier discovery for
2323
tabular anonymization workflows.
2424

25+
Name: KServe V2 inference protocol
26+
Source: https://github.com/kserve/open-inference-protocol
27+
License: Apache-2.0
28+
Usage: OpenMed ships a minimal, locally namespaced protobuf subset for the
29+
standard ModelInfer request and response wire fields. It provides a client-only
30+
Triton/KServe integration and does not bundle an inference server.
31+
2532
Name: medspaCy ConText
2633
Source: https://github.com/medspacy/medspacy
2734
License: Apache-2.0

‎docs/brand/system/publication.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ classification:
106106
- serving/tracing.md
107107
- serving/resilience.md
108108
- serving/backpressure.md
109+
- serving/kserve-triton.md
109110
- deploy/hardened-image.md
110111
- offline-install.md
111112
- serving/async-jobs.md

‎docs/configuration.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ export HF_TOKEN=hf_xxx
5151
export OPENMED_TORCH_DEVICE=cuda:1
5252
```
5353

54+
An explicitly selected `backend="remote"` adds KServe V2 endpoint, model,
55+
protocol, timeout, TLS, and local-tokenizer settings. It is never selected
56+
automatically and is incompatible with `local_only` or `OPENMED_OFFLINE`.
57+
See [KServe and Triton model repositories](serving/kserve-triton.md) for the
58+
full configuration and deployment boundary.
59+
5460
For Indic personal-name pseudonymization, enable
5561
`transliteration_aware_name_matching` and reuse the same setting when reopening
5662
a file-backed surrogate vault. The collision threshold and optional local

‎docs/model-loader.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ print(encoding["tokens"][:10])
9898

9999
`load_model` lets you access the underlying HF `AutoModel` + `AutoTokenizer` for workflows that outgrow pipelines.
100100

101+
## User-operated KServe and Triton backends
102+
103+
Set `OpenMedConfig.backend="remote"` to preserve the normal
104+
`ModelLoader.create_pipeline` call while running the ONNX graph through a
105+
user-operated KServe V2 or Triton endpoint. OpenMed still tokenizes and decodes
106+
locally, and the remote backend is never part of automatic backend selection.
107+
See [KServe and Triton model repositories](serving/kserve-triton.md) for the
108+
repository generator, complete config, and privacy boundary.
109+
101110
## Releasing cached models
102111

103112
Long-running services can release cached model references when VRAM or RAM should be reclaimed:

‎docs/serving/grpc.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ make grpc-proto
2323

2424
## Contract
2525

26-
The source protobuf contract is committed at
27-
`openmed/service/proto/openmed.proto`. Generated Python modules are committed
28-
under `openmed/service/proto/generated/`.
26+
The OpenMed service contract is committed at
27+
`openmed/service/proto/openmed.proto`. The client-only KServe V2 wire subset
28+
used by the [remote inference backend](kserve-triton.md) is committed at
29+
`openmed/service/proto/kserve_v2.proto`. Generated Python modules for both are
30+
committed under `openmed/service/proto/generated/`.
2931

3032
The `OpenMedService` exposes:
3133

‎docs/serving/kserve-triton.md‎

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# KServe and Triton model repositories
2+
3+
OpenMed can package an exported ONNX token classifier as a Triton model
4+
repository and use a user-operated Triton or KServe endpoint as its inference
5+
backend. Tokenization, source offsets, and entity decoding remain in the
6+
OpenMed process. The server receives numeric tensors and returns `logits`.
7+
8+
OpenMed does not install or start Triton or KServe. Operate the serving system
9+
separately and apply the authentication, network isolation, and transport
10+
controls required for your deployment.
11+
12+
## Install client and export dependencies
13+
14+
Repository generation needs ONNX support. Remote HTTP and gRPC inference use
15+
client libraries in the `triton` extra:
16+
17+
```bash
18+
pip install 'openmed[onnx,triton]'
19+
```
20+
21+
The extra contains no KServe or Triton server distribution. HTTP follows the
22+
KServe V2 JSON protocol. gRPC uses a small, wire-compatible subset of the
23+
KServe V2 protobuf contract and the standard
24+
`inference.GRPCInferenceService/ModelInfer` method.
25+
26+
## 1. Export and build the repository
27+
28+
Start with an OpenMed ONNX token-classification artifact produced by
29+
`openmed.onnx.convert`. For `max_batch_size > 0`, every graph input and output
30+
must have a dynamic leading batch dimension. The graph must expose
31+
`input_ids`, `attention_mask`, and a `logits` output.
32+
33+
```python
34+
from pathlib import Path
35+
36+
from openmed.onnx import convert
37+
from openmed.service.backends import write_triton_model_repository
38+
39+
artifact_dir = Path("build/openmed-pii-onnx")
40+
convert(
41+
"OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1",
42+
artifact_dir,
43+
)
44+
45+
repository = write_triton_model_repository(
46+
artifact_dir / "model.onnx",
47+
"build/model-repository",
48+
model_name="openmed_pii",
49+
version=1,
50+
max_batch_size=8,
51+
)
52+
print(repository.model_dir)
53+
```
54+
55+
The adapter derives tensor names, data types, and non-batch dimensions from
56+
the ONNX graph and renders a deterministic `config.pbtxt` using Triton's
57+
`onnxruntime` backend:
58+
59+
```text
60+
build/model-repository/
61+
└── openmed_pii/
62+
├── config.pbtxt
63+
└── 1/
64+
└── model.onnx
65+
```
66+
67+
For a graph with external tensor data, the adapter uses Triton's documented
68+
multi-file ONNX form and copies every referenced sidecar:
69+
70+
```text
71+
build/model-repository/openmed_pii/1/
72+
└── model.onnx/
73+
├── model.onnx
74+
└── weights.bin
75+
```
76+
77+
Absolute, parent-relative, and symlink-escaping external-data paths are
78+
rejected. Existing numeric model versions are never overwritten, and a new
79+
version must have the same derived schema as the existing config.
80+
81+
Recheck a copied repository before mounting it:
82+
83+
```python
84+
from openmed.service.backends import validate_triton_model_repository
85+
86+
validate_triton_model_repository(
87+
"build/model-repository",
88+
model_name="openmed_pii",
89+
version=1,
90+
)
91+
```
92+
93+
## 2. Serve it in your infrastructure
94+
95+
Mount `build/model-repository` into your own Triton deployment and point
96+
`--model-repository` at that mount. For KServe, configure its Triton runtime or
97+
storage initializer to expose the same repository. Verify the V2 model-ready
98+
endpoint before routing OpenMed traffic:
99+
100+
```bash
101+
curl --fail https://triton.example/v2/models/openmed_pii/ready
102+
```
103+
104+
Server installation, GPU tuning, autoscaling, credentials, and lifecycle
105+
management remain outside OpenMed.
106+
107+
## 3. Select remote inference through configuration
108+
109+
Keep the original artifact or model repository available to the OpenMed
110+
client. Its fast tokenizer and `id2label` metadata are used locally; only the
111+
ONNX graph executes on the remote server.
112+
113+
```python
114+
from openmed.core import ModelLoader, OpenMedConfig
115+
116+
config = OpenMedConfig(
117+
backend="remote",
118+
remote_inference_endpoint="https://triton.example",
119+
remote_inference_protocol="http",
120+
remote_inference_model_name="openmed_pii",
121+
remote_inference_model_version="1",
122+
remote_inference_tokenizer="build/openmed-pii-onnx",
123+
remote_inference_timeout_seconds=30,
124+
remote_inference_verify_tls=True,
125+
)
126+
127+
loader = ModelLoader(config)
128+
pipeline = loader.create_pipeline(
129+
"OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1",
130+
task="token-classification",
131+
aggregation_strategy="simple",
132+
)
133+
entities = pipeline("Synthetic patient Alice Nguyen")
134+
```
135+
136+
No pipeline call-site change is required. `OpenMedConfig.backend` selects the
137+
remote implementation through the normal `ModelLoader.create_pipeline` path.
138+
The pipeline accepts one string or a batch and returns the same entity
139+
dictionary shape and source spans as local ONNX inference.
140+
141+
The settings can also be stored in the normal flat TOML configuration:
142+
143+
```toml
144+
backend = "remote"
145+
remote_inference_endpoint = "https://kserve.example"
146+
remote_inference_protocol = "http"
147+
remote_inference_model_name = "openmed_pii"
148+
remote_inference_model_version = "1"
149+
remote_inference_tokenizer = "/models/openmed-pii-onnx"
150+
remote_inference_timeout_seconds = 30.0
151+
remote_inference_verify_tls = true
152+
```
153+
154+
For Triton gRPC, use a gRPC target and protocol:
155+
156+
```python
157+
config = OpenMedConfig(
158+
backend="remote",
159+
remote_inference_endpoint="grpcs://triton.example:8001",
160+
remote_inference_protocol="grpc",
161+
remote_inference_model_name="openmed_pii",
162+
remote_inference_tokenizer="/models/openmed-pii-onnx",
163+
)
164+
```
165+
166+
`grpc://` and bare `host:port` targets are plaintext. `grpcs://` uses TLS and
167+
does not permit certificate verification to be disabled. HTTP deployments
168+
should likewise keep `remote_inference_verify_tls=true`.
169+
170+
## Privacy and offline boundary
171+
172+
The adapter does not log source text, request tensors, response bodies, or raw
173+
server errors. It sends token IDs, attention masks, and tokenizer-required
174+
token-type IDs rather than the original string. These tensors can still encode
175+
sensitive content, so treat the endpoint as inside the trusted PHI boundary,
176+
use transport encryption, and enforce authentication and network policy in
177+
your serving infrastructure.
178+
179+
Remote inference is explicit only: it is never auto-selected. It is also
180+
rejected when `local_only=True` or `OPENMED_OFFLINE=1`, because either setting
181+
promises a no-egress runtime.
182+
183+
Protocol references:
184+
185+
- [Triton model repository layout](https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/user_guide/model_repository.html)
186+
- [Triton model configuration](https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/user_guide/model_configuration.html)
187+
- [KServe V2 inference protocol](https://kserve.github.io/website/docs/concepts/architecture/data-plane/v2-protocol)

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ nav:
3333
- REST Tracing: serving/tracing.md
3434
- Serving Resilience: serving/resilience.md
3535
- Serving Backpressure: serving/backpressure.md
36+
- KServe & Triton Model Repositories: serving/kserve-triton.md
3637
- Hardened Service Image: deploy/hardened-image.md
3738
- Offline & Air-Gapped Installation: offline-install.md
3839
- Async REST Jobs & Webhooks: serving/async-jobs.md

‎openmed/core/backends.py‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,46 @@ def create_pipeline(
230230
return OnnxTokenClassificationPipeline(model)
231231

232232

233+
class RemoteInferenceBackend:
234+
"""Backend using a user-operated KServe V2 or Triton endpoint."""
235+
236+
def __init__(self, config: Any = None) -> None:
237+
self._config = config
238+
239+
def is_available(self) -> bool:
240+
from openmed.service.backends.remote_inference import (
241+
remote_inference_dependencies_available,
242+
)
243+
244+
return remote_inference_dependencies_available(self._config)
245+
246+
def create_pipeline(
247+
self,
248+
model_name: str,
249+
task: str = "token-classification",
250+
aggregation_strategy: Optional[str] = None,
251+
**kwargs: Any,
252+
) -> Callable:
253+
from openmed.service.backends.remote_inference import (
254+
create_remote_inference_pipeline,
255+
)
256+
257+
return create_remote_inference_pipeline(
258+
model_name,
259+
config=self._config,
260+
task=task,
261+
aggregation_strategy=aggregation_strategy,
262+
**kwargs,
263+
)
264+
265+
233266
# -- Backend registry and auto-detection ------------------------------------
234267

235268
_BACKENDS: Dict[str, type] = {
236269
"hf": HuggingFaceBackend,
237270
"mlx": MLXBackend,
238271
"onnx": OnnxBackend,
272+
"remote": RemoteInferenceBackend,
239273
}
240274

241275

@@ -246,7 +280,8 @@ def get_backend(
246280
"""Return the requested backend, or auto-detect the best available one.
247281
248282
Args:
249-
name: ``"hf"``, ``"mlx"``, ``"onnx"``, or ``None`` for auto-detect.
283+
name: ``"hf"``, ``"mlx"``, ``"onnx"``, ``"remote"``, or ``None``
284+
for auto-detect.
250285
config: OpenMedConfig to pass to the backend.
251286
252287
Auto-detection order:
@@ -271,6 +306,11 @@ def get_backend(
271306
"with: pip install 'openmed[onnx-runtime]'. The low_resource "
272307
"profile does not fall back to a Torch backend."
273308
)
309+
if name == "remote":
310+
raise RuntimeError(
311+
"Backend 'remote' is not available. Install its client-only "
312+
"dependencies with: pip install 'openmed[triton]'."
313+
)
274314
raise RuntimeError(
275315
f"Backend {name!r} is not available. Install its dependencies first."
276316
)

‎openmed/core/capabilities.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
OpenMed ships many optional integrations behind ``pip install openmed[...]``
44
extras (``mlx``, ``coreml``, ``onnx``, ``gliner``, ``spacy``, ``presidio``,
5-
``hf``, ``multimodal``, ``service``, ``mcp``, ...). Historically each seam
5+
``hf``, ``multimodal``, ``service``, ``triton``, ``mcp``, ...). Historically
6+
each seam
67
guarded its optional imports differently: some raised a bare :class:`ImportError`
78
with an ad-hoc message, others raised one of several ``MissingDependencyError``
89
variants, and there was no single, importless way to probe which backends are
@@ -197,6 +198,13 @@ def primary_package(self) -> str:
197198
description="FastAPI REST service surface",
198199
install="fastapi",
199200
),
201+
"triton": BackendSpec(
202+
name="triton",
203+
extra="triton",
204+
modules=("grpc", "httpx", "numpy", "transformers", "google"),
205+
description="KServe V2 HTTP/gRPC remote inference clients",
206+
install="grpcio",
207+
),
200208
"mcp": BackendSpec(
201209
name="mcp",
202210
extra="mcp",

0 commit comments

Comments
 (0)