inferlab-integration-tensorrt-llm 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,686 @@
1
+ import hashlib
2
+ import os
3
+ import tomllib
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from pathlib import Path
6
+ from typing import cast
7
+
8
+ import yaml # type: ignore[import-untyped]
9
+ from inferlab_adapter_sdk import (
10
+ AdapterErrorCode,
11
+ AdapterOperationError,
12
+ EndpointProtocol,
13
+ EndpointRequirement,
14
+ IntegrationIdentity,
15
+ KvTransferMechanism,
16
+ LaunchFileDeclaration,
17
+ Parallelism,
18
+ ParallelismAttention,
19
+ ParallelismExperts,
20
+ ParallelismOuter,
21
+ PlanServeInput,
22
+ PlanServeResult,
23
+ ProcessSpec,
24
+ PublicEndpointRequirement,
25
+ PublicEndpointRequirementBuiltinProxy,
26
+ PublicEndpointRequirementReplica,
27
+ ReadinessProbe,
28
+ ReadinessProbeHttp,
29
+ ReadinessProbeProcessAlive,
30
+ RenderedServeProcess,
31
+ RenderInputDeclaration,
32
+ RenderServeInput,
33
+ RenderServeResult,
34
+ ServeProcessAllocation,
35
+ ServeReplicaRequirement,
36
+ ServeRoleInput,
37
+ ServeRoleKind,
38
+ ServeRoleLink,
39
+ ServeRoleLinkKvTransfer,
40
+ ServeRoleLinkRequestRouting,
41
+ ServeRoleResult,
42
+ ServeTopology,
43
+ SettingValue,
44
+ append_option,
45
+ merge_serve_args,
46
+ plain_setting,
47
+ )
48
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
49
+
50
+ # TensorRT-LLM declares its click options in underscore spellings plus short
51
+ # aliases and does no hyphen/underscore normalization, so the claim list must
52
+ # name every accepted spelling of every inferlab- or settings-owned option.
53
+ _INFERLAB_OPTION_ARITY: dict[str, int | None] = {
54
+ "--cluster_size": 1,
55
+ "--config": 1,
56
+ "--context_parallel_size": 1,
57
+ "--cp_size": 1,
58
+ "--enable_attention_dp": 0,
59
+ "--enable_chunked_prefill": 0,
60
+ "--ep_size": 1,
61
+ "--extra_llm_api_options": 1,
62
+ "--free_gpu_memory_fraction": 1,
63
+ "--host": 1,
64
+ "--kv_cache_dtype": 1,
65
+ "--kv_cache_free_gpu_memory_fraction": 1,
66
+ "--max_batch_size": 1,
67
+ "--max_num_tokens": 1,
68
+ "--max_seq_len": 1,
69
+ "--moe_cluster_parallel_size": 1,
70
+ "--moe_expert_parallel_size": 1,
71
+ "--pipeline_parallel_size": 1,
72
+ "--port": 1,
73
+ "--pp_size": 1,
74
+ "--served_model_name": 1,
75
+ "--tensor_parallel_size": 1,
76
+ "--tp_size": 1,
77
+ "--trust_remote_code": 0,
78
+ }
79
+
80
+ _RUNTIME_CACHE_SUBDIRS = {
81
+ "DG_JIT_CACHE_DIR": "deep_gemm_jit",
82
+ "FLASHINFER_WORKSPACE_BASE": "flashinfer",
83
+ "FLASHINFER_CUBIN_DIR": "flashinfer_cubin",
84
+ "TRITON_CACHE_DIR": "triton",
85
+ "TORCHINDUCTOR_CACHE_DIR": "torchinductor",
86
+ "TORCH_EXTENSIONS_DIR": "torch_extensions",
87
+ }
88
+
89
+ _NATIVE_ROUTING_BACKEND = "trtllm-disaggregated"
90
+ _PREFILL_DECODE_OPTION_ARITY = {**_INFERLAB_OPTION_ARITY, "--backend": 1}
91
+ # Inferlab owns readiness; the router's internal guard must not expire first.
92
+ _ROUTER_WORKER_STARTUP_TIMEOUT_SECS = 2_147_483_647
93
+
94
+
95
+ class TrtllmServeSettings(BaseModel):
96
+ model_config = ConfigDict(extra="forbid")
97
+
98
+ max_batch_size: int | None = Field(default=None, ge=1)
99
+ max_num_tokens: int | None = Field(default=None, ge=1)
100
+ max_seq_len: int | None = Field(default=None, ge=1)
101
+ kv_cache_dtype: str | None = None
102
+ free_gpu_memory_fraction: float | None = Field(default=None, gt=0.0, le=1.0)
103
+ enable_chunked_prefill: bool = False
104
+ trust_remote_code: bool = False
105
+ # Source YAML; P/D composition overrides its transport and cache invariants.
106
+ extra_llm_api_options: str | None = None
107
+ extra_args: list[str] | None = None
108
+ extra_env: dict[str, str] | None = None
109
+
110
+
111
+ def _runtime_cache_env(root: str) -> dict[str, str]:
112
+ cache_root = Path(root)
113
+ return {
114
+ name: str(cache_root / subdirectory)
115
+ for name, subdirectory in _RUNTIME_CACHE_SUBDIRS.items()
116
+ }
117
+
118
+
119
+ def _settings(values: dict[str, SettingValue]) -> TrtllmServeSettings:
120
+ try:
121
+ return TrtllmServeSettings.model_validate(
122
+ {key: plain_setting(value) for key, value in values.items()}
123
+ )
124
+ except ValidationError as error:
125
+ raise AdapterOperationError(AdapterErrorCode.invalid_settings, str(error)) from error
126
+
127
+
128
+ def _merged_settings(
129
+ base: dict[str, SettingValue], overrides: dict[str, SettingValue]
130
+ ) -> TrtllmServeSettings:
131
+ merged = dict(base)
132
+ merged.update(overrides)
133
+ return _settings(merged)
134
+
135
+
136
+ def _effective_settings(settings: TrtllmServeSettings) -> dict[str, SettingValue]:
137
+ return {
138
+ key: SettingValue(root=value)
139
+ for key, value in settings.model_dump(exclude_none=True).items()
140
+ }
141
+
142
+
143
+ def _yaml_mapping(value: object, source: str) -> dict[str, object]:
144
+ if not isinstance(value, dict):
145
+ raise AdapterOperationError(
146
+ AdapterErrorCode.invalid_settings,
147
+ f"TensorRT-LLM YAML {source} must be a mapping",
148
+ )
149
+ mapping = cast(dict[object, object], value)
150
+ if not all(isinstance(key, str) for key in mapping):
151
+ raise AdapterOperationError(
152
+ AdapterErrorCode.invalid_settings,
153
+ f"TensorRT-LLM YAML {source} must use string keys",
154
+ )
155
+ return cast(dict[str, object], mapping)
156
+
157
+
158
+ def _render_source_path(path: str) -> str:
159
+ if Path(path).is_absolute():
160
+ return path
161
+ return os.path.normpath(Path(".inferlab") / path)
162
+
163
+
164
+ def _load_worker_config(input: RenderServeInput, path: str | None) -> dict[str, object]:
165
+ if path is None:
166
+ return {}
167
+ supplied = next(
168
+ (item for item in input.render_inputs if item.source_path == _render_source_path(path)),
169
+ None,
170
+ )
171
+ if supplied is None:
172
+ raise AdapterOperationError(
173
+ AdapterErrorCode.invalid_request,
174
+ f"TensorRT-LLM render input {path!r} was not supplied",
175
+ )
176
+ try:
177
+ value: object = yaml.safe_load(supplied.text)
178
+ except yaml.YAMLError as error:
179
+ raise AdapterOperationError(
180
+ AdapterErrorCode.invalid_settings,
181
+ f"cannot parse TensorRT-LLM extra_llm_api_options {path!r}: {error}",
182
+ ) from error
183
+ if value is None:
184
+ return {}
185
+ return dict(_yaml_mapping(value, repr(path)))
186
+
187
+
188
+ def _nested_mapping(config: dict[str, object], key: str) -> dict[str, object]:
189
+ value = config.get(key)
190
+ if value is None:
191
+ nested: dict[str, object] = {}
192
+ else:
193
+ nested = dict(_yaml_mapping(value, key))
194
+ config[key] = nested
195
+ return nested
196
+
197
+
198
+ def _worker_launch_text(
199
+ input: RenderServeInput, settings: TrtllmServeSettings, kind: ServeRoleKind
200
+ ) -> str:
201
+ config = _load_worker_config(input, settings.extra_llm_api_options)
202
+ config["backend"] = "pytorch"
203
+ transceiver = _nested_mapping(config, "cache_transceiver_config")
204
+ transceiver["backend"] = "NIXL"
205
+ transceiver["transceiver_runtime"] = "PYTHON"
206
+ kv_cache = _nested_mapping(config, "kv_cache_config")
207
+ kv_cache["enable_block_reuse"] = False
208
+ if kind == ServeRoleKind.prefill:
209
+ config["disable_overlap_scheduler"] = True
210
+ return cast(str, yaml.safe_dump(config, sort_keys=False))
211
+
212
+
213
+ def _launch_file(
214
+ runtime_cache_root: str, name: str, text: str
215
+ ) -> tuple[LaunchFileDeclaration, str]:
216
+ digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
217
+ relative_path = f"launch-files/{digest}/{name}"
218
+ declaration = LaunchFileDeclaration(
219
+ relative_path=relative_path,
220
+ sha256=digest,
221
+ text=text,
222
+ )
223
+ return declaration, str(Path(runtime_cache_root) / relative_path)
224
+
225
+
226
+ def _adapter_version() -> str:
227
+ try:
228
+ return version("inferlab-integration-tensorrt-llm")
229
+ except PackageNotFoundError:
230
+ # Raw source-tree execution with no installed distribution
231
+ # metadata: the adjacent pyproject is the version authority.
232
+ # External-image lowering mounts each package's dist-info beside
233
+ # its module, so importlib.metadata resolves there instead.
234
+ pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
235
+ with pyproject.open("rb") as handle:
236
+ project_version: str = tomllib.load(handle)["project"]["version"]
237
+ return project_version
238
+
239
+
240
+ def _identity() -> IntegrationIdentity:
241
+ return IntegrationIdentity(
242
+ adapter_id="inferlab-tensorrt-llm",
243
+ adapter_version=_adapter_version(),
244
+ framework="tensorrt-llm",
245
+ )
246
+
247
+
248
+ def _effective_parallelism(declared: Parallelism) -> Parallelism:
249
+ """The TensorRT-LLM 1.3 algebra: `outer.tensor_parallel_size` is the
250
+ tensor-parallel world (`--tp_size`), which attention data parallelism
251
+ divides all-or-nothing (`--enable_attention_dp` replicates attention on
252
+ every rank) and expert parallelism divides freely (the framework derives
253
+ the MoE tensor split from `moe_tp * moe_ep == tp`). Context parallelism
254
+ multiplies the TensorRT-LLM world instead of dividing it, and MoE data
255
+ and dense-tensor parallelism have no TensorRT-LLM equivalent, so those
256
+ components reject rather than silently reshape the deployment."""
257
+ outer = declared.outer or ParallelismOuter()
258
+ attention = declared.attention or ParallelismAttention()
259
+ experts = declared.experts or ParallelismExperts()
260
+ outer_tp = outer.tensor_parallel_size or 1
261
+ outer_pp = outer.pipeline_parallel_size or 1
262
+ attention_dp = attention.data_parallel_size or 1
263
+ if (attention.context_parallel_size or 1) != 1:
264
+ raise AdapterOperationError(
265
+ AdapterErrorCode.invalid_settings,
266
+ "the TensorRT-LLM integration does not support attention context parallelism",
267
+ )
268
+ if attention_dp not in (1, outer_tp):
269
+ raise AdapterOperationError(
270
+ AdapterErrorCode.invalid_settings,
271
+ f"TensorRT-LLM attention data parallelism is all-or-nothing: "
272
+ f"attention.data_parallel_size must be 1 or equal "
273
+ f"outer.tensor_parallel_size ({outer_tp}), got {attention_dp}",
274
+ )
275
+ effective_attention_tp = outer_tp // attention_dp
276
+ if (
277
+ attention.tensor_parallel_size is not None
278
+ and attention.tensor_parallel_size != effective_attention_tp
279
+ ):
280
+ raise AdapterOperationError(
281
+ AdapterErrorCode.invalid_settings,
282
+ "TensorRT-LLM effective attention.tensor_parallel_size is "
283
+ f"outer.tensor_parallel_size / attention.data_parallel_size "
284
+ f"({effective_attention_tp})",
285
+ )
286
+ if (experts.data_parallel_size or 1) != 1:
287
+ raise AdapterOperationError(
288
+ AdapterErrorCode.invalid_settings,
289
+ "the TensorRT-LLM integration does not support MoE data parallelism",
290
+ )
291
+ if (experts.dense_tensor_parallel_size or 1) != 1:
292
+ raise AdapterOperationError(
293
+ AdapterErrorCode.invalid_settings,
294
+ "the TensorRT-LLM integration does not support dense tensor parallelism",
295
+ )
296
+ expert_ep = experts.expert_parallel_size or 1
297
+ if outer_tp % expert_ep != 0:
298
+ raise AdapterOperationError(
299
+ AdapterErrorCode.invalid_settings,
300
+ f"TensorRT-LLM experts.expert_parallel_size ({expert_ep}) "
301
+ f"must divide outer.tensor_parallel_size ({outer_tp})",
302
+ )
303
+ effective_expert_tp = outer_tp // expert_ep
304
+ if (
305
+ experts.tensor_parallel_size is not None
306
+ and experts.tensor_parallel_size != effective_expert_tp
307
+ ):
308
+ raise AdapterOperationError(
309
+ AdapterErrorCode.invalid_settings,
310
+ "TensorRT-LLM effective experts.tensor_parallel_size is "
311
+ f"outer.tensor_parallel_size / experts.expert_parallel_size "
312
+ f"({effective_expert_tp})",
313
+ )
314
+ return Parallelism(
315
+ outer=ParallelismOuter(
316
+ tensor_parallel_size=outer_tp,
317
+ pipeline_parallel_size=outer_pp,
318
+ ),
319
+ attention=ParallelismAttention(
320
+ tensor_parallel_size=effective_attention_tp,
321
+ data_parallel_size=attention_dp,
322
+ context_parallel_size=1,
323
+ ),
324
+ experts=ParallelismExperts(
325
+ tensor_parallel_size=effective_expert_tp,
326
+ data_parallel_size=1,
327
+ expert_parallel_size=expert_ep,
328
+ dense_tensor_parallel_size=1,
329
+ ),
330
+ )
331
+
332
+
333
+ def _role_for_kind(input: PlanServeInput, kind: ServeRoleKind) -> ServeRoleInput:
334
+ matches = [role for role in input.roles if role.kind == kind]
335
+ if len(matches) != 1:
336
+ raise AdapterOperationError(
337
+ AdapterErrorCode.invalid_settings,
338
+ f"{input.topology.value} topology requires exactly one {kind.value} role",
339
+ )
340
+ return matches[0]
341
+
342
+
343
+ def _replica_id(role: ServeRoleInput, replica_index: int) -> str:
344
+ base = "server" if role.kind == ServeRoleKind.serve else role.id
345
+ if role.replica_count == 1:
346
+ return base
347
+ return f"{base}-{replica_index:03d}"
348
+
349
+
350
+ def _accelerator_count(parallelism: Parallelism) -> int:
351
+ outer = parallelism.outer or ParallelismOuter()
352
+ return (outer.tensor_parallel_size or 1) * (outer.pipeline_parallel_size or 1)
353
+
354
+
355
+ def _plan_role(
356
+ input: PlanServeInput, role: ServeRoleInput
357
+ ) -> tuple[ServeRoleResult, list[ServeReplicaRequirement]]:
358
+ if role.replica_count < 1:
359
+ raise AdapterOperationError(
360
+ AdapterErrorCode.invalid_settings,
361
+ f"role {role.id!r} replica count must be positive",
362
+ )
363
+ settings = _merged_settings(input.settings, role.settings)
364
+ parallelism = _effective_parallelism(role.parallelism)
365
+ replicas = [
366
+ ServeReplicaRequirement(
367
+ id=_replica_id(role, replica_index),
368
+ role_id=role.id,
369
+ replica_index=replica_index,
370
+ accelerator_count=_accelerator_count(parallelism),
371
+ ports=[],
372
+ primary_ports=["master"],
373
+ primary_readiness=ReadinessProbe(root=ReadinessProbeHttp(path="/health")),
374
+ worker_readiness=ReadinessProbe(root=ReadinessProbeProcessAlive()),
375
+ )
376
+ for replica_index in range(role.replica_count)
377
+ ]
378
+ return (
379
+ ServeRoleResult(
380
+ id=role.id,
381
+ kind=role.kind,
382
+ replica_count=role.replica_count,
383
+ effective_settings=_effective_settings(settings),
384
+ effective_parallelism=parallelism,
385
+ ),
386
+ replicas,
387
+ )
388
+
389
+
390
+ def _endpoint_requirement() -> EndpointRequirement:
391
+ return EndpointRequirement(
392
+ protocol=EndpointProtocol(),
393
+ api_path="/v1/completions",
394
+ )
395
+
396
+
397
+ def _plan_single(input: PlanServeInput) -> PlanServeResult:
398
+ if input.kv_transfer is not None:
399
+ raise AdapterOperationError(
400
+ AdapterErrorCode.invalid_settings,
401
+ "single topology does not use a KV-transfer mechanism",
402
+ )
403
+ if input.routing_backend != "builtin":
404
+ raise AdapterOperationError(
405
+ AdapterErrorCode.invalid_settings,
406
+ f"the TensorRT-LLM integration does not support routing backend "
407
+ f"{input.routing_backend!r}",
408
+ )
409
+ role = _role_for_kind(input, ServeRoleKind.serve)
410
+ role_result, replicas = _plan_role(input, role)
411
+ return PlanServeResult(
412
+ integration=_identity(),
413
+ effective_settings=_effective_settings(_settings(input.settings)),
414
+ effective_parallelism=_effective_parallelism(input.parallelism),
415
+ roles=[role_result],
416
+ replicas=replicas,
417
+ links=[],
418
+ public_endpoint=PublicEndpointRequirement(
419
+ root=PublicEndpointRequirementReplica(replica_id=replicas[0].id)
420
+ ),
421
+ endpoint=_endpoint_requirement(),
422
+ )
423
+
424
+
425
+ def _plan_prefill_decode(input: PlanServeInput) -> PlanServeResult:
426
+ if input.kv_transfer != KvTransferMechanism.nixl:
427
+ raise AdapterOperationError(
428
+ AdapterErrorCode.invalid_settings,
429
+ "TensorRT-LLM prefill_decode requires NIXL KV transfer",
430
+ )
431
+ if input.routing_backend not in {"builtin", _NATIVE_ROUTING_BACKEND}:
432
+ raise AdapterOperationError(
433
+ AdapterErrorCode.invalid_settings,
434
+ f"TensorRT-LLM does not support routing backend {input.routing_backend!r}",
435
+ )
436
+ prefill = _role_for_kind(input, ServeRoleKind.prefill)
437
+ decode = _role_for_kind(input, ServeRoleKind.decode)
438
+ prefill_result, prefill_replicas = _plan_role(input, prefill)
439
+ decode_result, decode_replicas = _plan_role(input, decode)
440
+ roles = [prefill_result, decode_result]
441
+ replicas = [*prefill_replicas, *decode_replicas]
442
+ source_paths = dict.fromkeys(
443
+ _render_source_path(path)
444
+ for role in roles
445
+ if (path := _settings(role.effective_settings).extra_llm_api_options) is not None
446
+ )
447
+ links = [
448
+ ServeRoleLink(
449
+ root=ServeRoleLinkRequestRouting(
450
+ source="router",
451
+ targets=[prefill.id, decode.id],
452
+ )
453
+ ),
454
+ ServeRoleLink(
455
+ root=ServeRoleLinkKvTransfer(
456
+ source=prefill.id,
457
+ target=decode.id,
458
+ mechanism=KvTransferMechanism.nixl,
459
+ )
460
+ ),
461
+ ]
462
+ if input.routing_backend == "builtin":
463
+ public_endpoint = PublicEndpointRequirement(
464
+ root=PublicEndpointRequirementBuiltinProxy(
465
+ process_id="proxy",
466
+ role_id="router",
467
+ prefill_role=prefill.id,
468
+ decode_role=decode.id,
469
+ readiness=ReadinessProbe(root=ReadinessProbeHttp(path="/healthcheck")),
470
+ )
471
+ )
472
+ else:
473
+ roles.append(
474
+ ServeRoleResult(
475
+ id="router",
476
+ kind=ServeRoleKind.router,
477
+ replica_count=1,
478
+ effective_settings={},
479
+ effective_parallelism=Parallelism(),
480
+ )
481
+ )
482
+ replicas.append(
483
+ ServeReplicaRequirement(
484
+ id="router",
485
+ role_id="router",
486
+ replica_index=0,
487
+ accelerator_count=0,
488
+ ports=[],
489
+ primary_ports=[],
490
+ primary_readiness=ReadinessProbe(root=ReadinessProbeHttp(path="/health")),
491
+ worker_readiness=ReadinessProbe(root=ReadinessProbeProcessAlive()),
492
+ )
493
+ )
494
+ public_endpoint = PublicEndpointRequirement(
495
+ root=PublicEndpointRequirementReplica(replica_id="router")
496
+ )
497
+ return PlanServeResult(
498
+ integration=_identity(),
499
+ effective_settings=_effective_settings(_settings(input.settings)),
500
+ effective_parallelism=_effective_parallelism(input.parallelism),
501
+ roles=roles,
502
+ replicas=replicas,
503
+ links=links,
504
+ public_endpoint=public_endpoint,
505
+ endpoint=_endpoint_requirement(),
506
+ render_inputs=[
507
+ RenderInputDeclaration(source_path=source_path) for source_path in source_paths
508
+ ],
509
+ )
510
+
511
+
512
+ def plan_serve(input: PlanServeInput) -> PlanServeResult:
513
+ if input.profiling:
514
+ raise AdapterOperationError(
515
+ AdapterErrorCode.invalid_settings,
516
+ "the TensorRT-LLM integration does not support profiling capture yet",
517
+ )
518
+ if input.topology == ServeTopology.single:
519
+ return _plan_single(input)
520
+ return _plan_prefill_decode(input)
521
+
522
+
523
+ def _render_worker(
524
+ input: RenderServeInput,
525
+ role: ServeRoleResult,
526
+ allocation: ServeProcessAllocation,
527
+ ) -> RenderedServeProcess:
528
+ settings = _settings(role.effective_settings)
529
+ outer = role.effective_parallelism.outer or ParallelismOuter()
530
+ attention = role.effective_parallelism.attention or ParallelismAttention()
531
+ experts = role.effective_parallelism.experts or ParallelismExperts()
532
+ argv = [
533
+ "python3",
534
+ "-m",
535
+ "tensorrt_llm.commands.serve",
536
+ allocation.model_locator,
537
+ ]
538
+ inferlab_args = [
539
+ "--host",
540
+ allocation.endpoint.host,
541
+ "--port",
542
+ str(allocation.endpoint.port),
543
+ "--served_model_name",
544
+ input.model.served_name,
545
+ "--tensor_parallel_size",
546
+ str(outer.tensor_parallel_size or 1),
547
+ ]
548
+ if (outer.pipeline_parallel_size or 1) != 1:
549
+ inferlab_args.extend(["--pipeline_parallel_size", str(outer.pipeline_parallel_size)])
550
+ if (attention.data_parallel_size or 1) != 1:
551
+ inferlab_args.append("--enable_attention_dp")
552
+ if (experts.expert_parallel_size or 1) != 1:
553
+ inferlab_args.extend(["--moe_expert_parallel_size", str(experts.expert_parallel_size)])
554
+ append_option(inferlab_args, "--max_batch_size", settings.max_batch_size)
555
+ append_option(inferlab_args, "--max_num_tokens", settings.max_num_tokens)
556
+ append_option(inferlab_args, "--max_seq_len", settings.max_seq_len)
557
+ append_option(inferlab_args, "--kv_cache_dtype", settings.kv_cache_dtype)
558
+ append_option(inferlab_args, "--free_gpu_memory_fraction", settings.free_gpu_memory_fraction)
559
+ launch_files: list[LaunchFileDeclaration] = []
560
+ if input.topology == ServeTopology.prefill_decode:
561
+ if role.kind not in {ServeRoleKind.prefill, ServeRoleKind.decode}:
562
+ raise AdapterOperationError(
563
+ AdapterErrorCode.invalid_request,
564
+ f"prefill_decode allocation has unsupported role {role.id!r}",
565
+ )
566
+ launch_text = _worker_launch_text(input, settings, role.kind)
567
+ launch_file, resolved_path = _launch_file(
568
+ allocation.runtime_cache_root,
569
+ "extra-llm-api-options.yaml",
570
+ launch_text,
571
+ )
572
+ launch_files.append(launch_file)
573
+ inferlab_args.extend(["--extra_llm_api_options", resolved_path])
574
+ inferlab_args.extend(["--backend", "pytorch"])
575
+ else:
576
+ append_option(inferlab_args, "--extra_llm_api_options", settings.extra_llm_api_options)
577
+ if settings.enable_chunked_prefill:
578
+ inferlab_args.append("--enable_chunked_prefill")
579
+ if settings.trust_remote_code:
580
+ inferlab_args.append("--trust_remote_code")
581
+ option_arity = (
582
+ _PREFILL_DECODE_OPTION_ARITY
583
+ if input.topology == ServeTopology.prefill_decode
584
+ else _INFERLAB_OPTION_ARITY
585
+ )
586
+ argv.extend(merge_serve_args(settings.extra_args or [], inferlab_args, option_arity))
587
+ process_env = _runtime_cache_env(allocation.runtime_cache_root)
588
+ process_env.update(settings.extra_env or {})
589
+ return RenderedServeProcess(
590
+ id=allocation.process_id,
591
+ launch_files=launch_files,
592
+ process=ProcessSpec(argv=argv, env=process_env),
593
+ )
594
+
595
+
596
+ def _rank_zero_allocations(
597
+ input: RenderServeInput, kind: ServeRoleKind
598
+ ) -> list[ServeProcessAllocation]:
599
+ role_ids = {role.id for role in input.roles if role.kind == kind}
600
+ return sorted(
601
+ [
602
+ allocation
603
+ for allocation in input.allocations
604
+ if allocation.role_id in role_ids and allocation.rank == 0
605
+ ],
606
+ key=lambda allocation: (allocation.replica_index, allocation.replica_id),
607
+ )
608
+
609
+
610
+ def _render_native_router(
611
+ input: RenderServeInput, allocation: ServeProcessAllocation
612
+ ) -> RenderedServeProcess:
613
+ prefill = _rank_zero_allocations(input, ServeRoleKind.prefill)
614
+ decode = _rank_zero_allocations(input, ServeRoleKind.decode)
615
+ config = {
616
+ "hostname": allocation.endpoint.host,
617
+ "port": allocation.endpoint.port,
618
+ "schedule_style": "context_first",
619
+ "context_servers": {
620
+ "num_instances": len(prefill),
621
+ "urls": [f"{item.endpoint.host}:{item.endpoint.port}" for item in prefill],
622
+ "router": {"type": "round_robin"},
623
+ },
624
+ "generation_servers": {
625
+ "num_instances": len(decode),
626
+ "urls": [f"{item.endpoint.host}:{item.endpoint.port}" for item in decode],
627
+ "router": {"type": "round_robin"},
628
+ },
629
+ }
630
+ text = cast(str, yaml.safe_dump(config, sort_keys=False))
631
+ launch_file, resolved_path = _launch_file(
632
+ allocation.runtime_cache_root,
633
+ "disaggregated.yaml",
634
+ text,
635
+ )
636
+ process_env = _runtime_cache_env(allocation.runtime_cache_root)
637
+ process_env.update(_settings(input.settings).extra_env or {})
638
+ return RenderedServeProcess(
639
+ id=allocation.process_id,
640
+ launch_files=[launch_file],
641
+ process=ProcessSpec(
642
+ argv=[
643
+ "python3",
644
+ "-m",
645
+ "tensorrt_llm.commands.serve",
646
+ "disaggregated",
647
+ "--config",
648
+ resolved_path,
649
+ "--server_start_timeout",
650
+ str(_ROUTER_WORKER_STARTUP_TIMEOUT_SECS),
651
+ ],
652
+ env=process_env,
653
+ ),
654
+ )
655
+
656
+
657
+ def render_serve(input: RenderServeInput) -> RenderServeResult:
658
+ if not input.allocations:
659
+ raise AdapterOperationError(
660
+ AdapterErrorCode.invalid_request, "serve allocation must not be empty"
661
+ )
662
+ roles = {role.id: role for role in input.roles}
663
+ replica_counts: dict[str, int] = {}
664
+ for allocation in input.allocations:
665
+ replica_counts[allocation.replica_id] = replica_counts.get(allocation.replica_id, 0) + 1
666
+ processes: list[RenderedServeProcess] = []
667
+ for allocation in input.allocations:
668
+ role = roles.get(allocation.role_id)
669
+ if role is None:
670
+ raise AdapterOperationError(
671
+ AdapterErrorCode.invalid_request,
672
+ f"allocation references unknown role {allocation.role_id!r}",
673
+ )
674
+ if role.kind == ServeRoleKind.router:
675
+ processes.append(_render_native_router(input, allocation))
676
+ continue
677
+ if replica_counts[allocation.replica_id] > 1:
678
+ raise AdapterOperationError(
679
+ AdapterErrorCode.invalid_request,
680
+ "the TensorRT-LLM integration does not support multi-node serving yet",
681
+ )
682
+ processes.append(_render_worker(input, role, allocation))
683
+ return RenderServeResult(integration=_identity(), processes=processes)
684
+
685
+
686
+ __all__ = ["TrtllmServeSettings", "plan_serve", "render_serve"]
@@ -0,0 +1,11 @@
1
+ from inferlab_adapter_sdk import run_adapter
2
+
3
+ from . import plan_serve, render_serve
4
+
5
+
6
+ def main() -> None:
7
+ raise SystemExit(run_adapter(plan_serve, render_serve=render_serve))
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: inferlab-integration-tensorrt-llm
3
+ Version: 0.2.0
4
+ Summary: Inferlab adapter integration for TensorRT-LLM
5
+ Project-URL: Repository, https://github.com/Infer-Lab/inferlab
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: inferlab-adapter-sdk==0.2.0
10
+ Requires-Dist: pyyaml<7,>=6
@@ -0,0 +1,7 @@
1
+ inferlab_integration_tensorrt_llm/__init__.py,sha256=k2uDaWNxdj56nOe48jDoljbUHdqiVUjsPmGmg0ABhfc,25598
2
+ inferlab_integration_tensorrt_llm/__main__.py,sha256=e1uoS23_cXd5sMrRK_nSuUPgdz37Tx1xBhxqMlCYhBg,220
3
+ inferlab_integration_tensorrt_llm-0.2.0.dist-info/METADATA,sha256=pQKLYf4Xf8YfErWBXDFPaf2eRlC-QkTWI5Q0aLiJ-vA,336
4
+ inferlab_integration_tensorrt_llm-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ inferlab_integration_tensorrt_llm-0.2.0.dist-info/entry_points.txt,sha256=Fs0UfQX0NqdHcivh5ncEDoDs8GRvLakGo1zF1MrwwOM,98
6
+ inferlab_integration_tensorrt_llm-0.2.0.dist-info/licenses/LICENSE,sha256=_sFBKPJJjTLuE3lL3Ir9HDHTTrB9zsZtMBAOvBfBWA4,1065
7
+ inferlab_integration_tensorrt_llm-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ inferlab-adapter-tensorrt-llm = inferlab_integration_tensorrt_llm.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zihua Wu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.