Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(python3 -c ' *)"
]
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# autoware-ml
/data
/work_dirs
/deployment/graphify-out
/graphify-out
.gitignore
CLAUDE.md

*.bin
*.onnx
Expand Down
11 changes: 6 additions & 5 deletions deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ python -m deployment.cli.main <project_name> <deploy_cfg.py> <model_cfg.py> [--l
# Example (CenterPoint)
python -m deployment.cli.main centerpoint \
deployment/projects/centerpoint/config/deploy_config.py \
<model_cfg.py> \
--rot-y-axis-reference
<model_cfg.py>
```

## What to read
Expand Down Expand Up @@ -46,12 +45,14 @@ deployment/
├── cli/ # Unified CLI
├── config/ # Typed deploy config schema
├── io/ # Data-loader base and sample types
├── export/ # exporters/ (ONNX/TensorRT), pipelines/, ExportContext
├── export/ # exporters/ (ONNX/TensorRT), pipelines/
├── inference/ # Shared inference pipeline base and GPU resource helpers
├── evaluation/ # Evaluator, backend executor, verifier, output comparison
├── execution/ # BackendExecutor primitives (shared by evaluation + verification)
├── evaluation/ # Evaluators and metrics scoring
├── verification/ # Cross-backend numerical comparison (BackendVerifier, OutputComparator)
├── metrics/ # Task metrics interfaces (3D/2D detection, classification)
├── runtime/ # BaseDeploymentRunner, orchestrators, ArtifactManager
├── primitives/ # Cross-cutting leaf types: device (DeviceSpec), artifacts (path resolution)
├── primitives/ # Cross-cutting leaf types: device (DeviceSpec), artifacts, evaluator_types
├── projects/ # Per-task bundles
└── tests/ # CPU-only unit tests (pytest)
```
Expand Down
9 changes: 5 additions & 4 deletions deployment/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Single deployment entrypoint.

Usage:
python -m deployment.cli.main <project> <deploy_cfg.py> <model_cfg.py> [project-specific args]
python -m deployment.cli.main <project> <deploy_cfg.py> <model_cfg.py> [--log-level LEVEL]
"""

from __future__ import annotations
Expand Down Expand Up @@ -51,7 +51,7 @@ def build_parser() -> argparse.ArgumentParser:

subparsers = parser.add_subparsers(dest="project", required=True)

# Discover projects and import them so they can contribute args.
# Discover and import project packages so they register their adapters.
failed_projects: list[str] = []
for project_name in _discover_project_packages():
try:
Expand All @@ -61,14 +61,15 @@ def build_parser() -> argparse.ArgumentParser:
failed_projects.append(f"- {project_name}: {e}\n{tb}")
continue

# Only expose a subparser for projects that actually registered an adapter
# (get() raises KeyError if import ran but registration did not happen).
try:
adapter = project_registry.get(project_name)
project_registry.get(project_name)
except KeyError:
continue

sub = subparsers.add_parser(project_name, help=f"{project_name} deployment")
parse_base_args(sub) # adds deploy_cfg, model_cfg, --log-level
adapter.add_args(sub)
sub.set_defaults(_adapter_name=project_name)

if not project_registry.list_projects():
Expand Down
3 changes: 1 addition & 2 deletions deployment/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ def __init__(self, deploy_cfg: Config) -> None:
Args:
deploy_cfg: MMEngine Config object containing deployment settings
"""
self._deploy_cfg = deploy_cfg

checkpoint_path = deploy_cfg.get("checkpoint_path")
self.checkpoint_path = self._validate_checkpoint_path(checkpoint_path)
self.device_config = DeviceConfig.from_dict(deploy_cfg.get("devices", {}))
Expand Down Expand Up @@ -192,4 +190,5 @@ def get_tensorrt_settings(self, component_name: str) -> TensorRTExportConfig:
precision_policy=self._tensorrt_config.precision_policy,
max_workspace_size=self._tensorrt_config.max_workspace_size,
model_input=model_input,
plugin_libraries=self._tensorrt_config.plugin_libraries,
)
91 changes: 47 additions & 44 deletions deployment/config/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,47 @@
from __future__ import annotations

from enum import Enum
from typing import Optional, Union
from typing import Optional, Type, TypeVar, Union

# Constants
DEFAULT_WORKSPACE_SIZE = 1 << 30 # 1 GB

_E = TypeVar("_E", bound=Enum)


def _enum_from_value(
enum_cls: Type[_E],
value: object,
*,
default: Optional[_E] = None,
label: Optional[str] = None,
) -> _E:
"""Normalize a string or enum member into ``enum_cls`` (shared by the config enums).

Matching is case-insensitive on the member ``value``. ``None`` returns ``default`` when
one is given (for optional config sections) and is otherwise an error, so every enum
parses identically instead of each hand-rolling its own ``from_value``.

Raises:
ValueError: If ``value`` is ``None`` without a default, or is an unknown string.
TypeError: If ``value`` is neither ``None``, a ``str``, nor an ``enum_cls`` member.
"""
label = label or enum_cls.__name__
valid = [member.value for member in enum_cls]
if value is None:
if default is not None:
return default
raise ValueError(f"{label} is required; must be one of {valid}.")
if isinstance(value, enum_cls):
return value
if isinstance(value, str):
normalized = value.strip().lower()
for member in enum_cls:
if member.value == normalized:
return member
raise ValueError(f"Invalid {label} '{value}'. Must be one of {valid}.")
raise TypeError(f"{label} must be a string or {enum_cls.__name__}, got {type(value).__name__}.")


class PrecisionPolicy(str, Enum):
"""Precision policy options for TensorRT.
Expand All @@ -29,16 +65,10 @@ class PrecisionPolicy(str, Enum):
@classmethod
def from_value(cls, value: Optional[Union[str, PrecisionPolicy]]) -> PrecisionPolicy:
"""Parse strings or enum members into PrecisionPolicy (defaults to AUTO)."""
if value is None:
return cls.AUTO
if isinstance(value, cls):
return value
if isinstance(value, str):
normalized = value.strip().lower()
for member in cls:
if member.value == normalized:
return member
raise ValueError(f"Invalid precision_policy '{value}'. Must be one of {[m.value for m in cls]}.")
return _enum_from_value(cls, value, default=cls.AUTO, label="precision_policy")

def __str__(self) -> str: # pragma: no cover - convenience for logging
return self.value


class Backend(str, Enum):
Expand All @@ -50,29 +80,8 @@ class Backend(str, Enum):

@classmethod
def from_value(cls, value: Union[str, Backend]) -> Backend:
"""
Normalize backend identifiers coming from configs or enums.

Args:
value: Backend as string or Backend enum

Returns:
Backend enum instance

Raises:
ValueError: If value cannot be mapped to a supported backend
"""
if isinstance(value, cls):
return value

if isinstance(value, str):
normalized = value.strip().lower()
try:
return cls(normalized)
except ValueError as exc:
raise ValueError(f"Unsupported backend '{value}'. Expected one of {[b.value for b in cls]}.") from exc

raise TypeError(f"Backend must be a string or Backend enum, got {type(value)}")
"""Normalize a backend identifier (string or enum) into a ``Backend`` member."""
return _enum_from_value(cls, value, label="backend")

@property
def requires_cuda(self) -> bool:
Expand All @@ -97,13 +106,7 @@ class ExportMode(str, Enum):
@classmethod
def from_value(cls, value: Optional[Union[str, ExportMode]]) -> ExportMode:
"""Parse strings or enum members into ExportMode (defaults to BOTH)."""
if value is None:
return cls.BOTH
if isinstance(value, cls):
return value
if isinstance(value, str):
normalized = value.strip().lower()
for member in cls:
if member.value == normalized:
return member
raise ValueError(f"Invalid export mode '{value}'. Must be one of {[m.value for m in cls]}.")
return _enum_from_value(cls, value, default=cls.BOTH, label="export mode")

def __str__(self) -> str: # pragma: no cover - convenience for logging
return self.value
19 changes: 18 additions & 1 deletion deployment/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,29 @@ class TensorRTConfig:
Configuration for TensorRT backend-specific settings.

Uses config structure:
tensorrt_config = dict(precision_policy="auto", max_workspace_size=1<<30)
tensorrt_config = dict(precision_policy="auto", max_workspace_size=1<<30,
plugin_libraries=["/opt/plugins/libcustom.so"])

TensorRT profiles are defined in components.*.tensorrt_profile.

Note:
The deploy config key for this section is **`tensorrt_config`**.

``plugin_libraries`` lists custom TensorRT plugin ``.so`` paths to ``dlopen``
before engine build/deserialize (e.g. the BEVFusion spconv ImplicitGemm plugin). Empty
by default, so projects that need no custom plugins (e.g. CenterPoint) are unaffected.
"""

precision_policy: PrecisionPolicy = PrecisionPolicy.AUTO
max_workspace_size: int = DEFAULT_WORKSPACE_SIZE
plugin_libraries: Tuple[str, ...] = ()

@classmethod
def from_dict(cls, config_dict: Mapping[str, Any]) -> TensorRTConfig:
return cls(
precision_policy=PrecisionPolicy.from_value(config_dict.get("precision_policy")),
max_workspace_size=config_dict.get("max_workspace_size", DEFAULT_WORKSPACE_SIZE),
plugin_libraries=tuple(config_dict.get("plugin_libraries") or ()),
)


Expand Down Expand Up @@ -230,6 +237,16 @@ def items(self) -> Iterable[Tuple[str, ComponentCfg]]:
"""Iterate (name, ComponentCfg) pairs."""
return self._components.items()

def with_component(self, component: ComponentCfg) -> ComponentsConfig:
"""Return a new ``ComponentsConfig`` with ``component`` added (replacing any of the same name).

Lets callers derive a layout (e.g. BEVFusion's merged ``bevfusion_merged``) from already
typed components without round-tripping the whole config back through raw dicts.
"""
return ComponentsConfig(
_components=MappingProxyType({**self._components, component.name: component}),
)

@staticmethod
def _validate_dynamic_axes(raw: Any) -> Dict[str, Dict[int, str]]:
"""Validate dynamic_axes schema without coercing types."""
Expand Down
95 changes: 95 additions & 0 deletions deployment/docs/REFACTOR_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Deployment Framework Cleanup & Refactor Plan

Working checklist for the cleanliness pass across the shared framework and the
`bevfusion_l` / `centerpoint` project bundles. Goal: clear responsibilities, clear
naming, high readability, no smells / hard-code / over-engineering, and BEVFusion aligned
with CenterPoint's clean architecture (fixing the shared layer once where both diverge).

Status legend: `[ ]` todo · `[x]` done · `[~]` intentionally skipped / deferred (documented).

Verification note: the host has no torch/CUDA/mmengine (see project memory), so each change is
verified statically — `ast.parse`, `pyflakes`, targeted `grep`, CLI package discovery, and
`exec()` of the (pure-Python) deploy config. A Docker e2e smoke test (`bevfusion_l` / `centerpoint`
export+eval) is still recommended after the pass and is **not** covered here.

---

## Tier A — safe fixes (dead code / one real bug)

- [x] **A-BUG** `OutputComparator._merge_summaries` `inf * 0 = nan` fixed by skipping zero-element
children in the weighted mean. Verified: mixed shape-mismatch + valid child now yields finite
`mean_diff` with `max_diff=inf`. `verification/output_comparator.py`.
- [x] **A1** `VerificationOrchestrator` now logs `verification_results["error"]` (device-validation
failures) and `continue`s instead of counting the scenario as 0/0.
- [x] **A2** Removed dead `TensorDiffDetail.passed` field + both construction sites.
- [x] **A3** Removed unused module `logger` (and the `logging` import) from `primitives/artifacts.py`.
- [x] **A4** `tensorrt_plugins.py`: dropped dead `loaded_now`, collapsed the duplicated `CDLL`
branch to one call + conditional log, and now returns the libraries newly loaded by this call
(matches the docstring) instead of the cumulative global set.

## Tier B — shared consistency (fix once, helps both projects)

- [x] **A5** Added `_enum_from_value` helper in `config/enums.py`; `PrecisionPolicy` / `ExportMode`
/ `Backend` all route through it (consistent None-default + `ValueError`/`TypeError`). Behavior
smoke-tested.
- [x] **A7** Added `verification/reporting.py` (`BANNER_WIDTH=80`, `banner()`, `format_verdict()`);
applied in `backend_verifier.py` (was `60` + inline emoji) and `verification_orchestrator.py`
(was `80`). Also unified the counter noun to "samples" and the `policy`→`scenario` terminology.
- [x] **A8** `load_tensorrt_plugin_libraries` no longer takes an injected `logger` (uses a module
logger). Both call sites updated: `bevfusion_l` TRT pipeline **and** the shared
`export/exporters/tensorrt_exporter.py` (the second caller — caught by the grep sweep).
- [x] **A9** Consistent `__str__` on `PrecisionPolicy` / `ExportMode` / `Backend` (all return `.value`).
- [x] **(bonus)** Fixed the garbled `_fmt_finite_diff` docstring in `backend_verifier.py`.

## Tier C — BEVFusion cleanup (align with CenterPoint)

- [x] **P0-2** BEVFusion TRT pipeline now stores `self._engines` / `self._contexts` dicts keyed by
component name (CenterPoint pattern). 6 attributes + `_split` branching → 2 dicts + a uniform
`_load_tensorrt_engines` loop and a single-line `_release_gpu_resources`.
- [x] **P1-2** Runner docstring trimmed from ~20 lines to CenterPoint brevity.
- [x] **P1-3** Dropped `_pick_bound_input_name` + its "mAP=0" warning; the single-input dense engine
now binds `input_names[0]`. `strict=False` output ordering **kept deliberately** (BEVFusion export
had name drift; aligning to CenterPoint's `strict=True` is unsafe without an e2e run).

## Tier D — config cleanliness

- [x] **P1-1** `deploy_config.py` restructured to CenterPoint's numbered-section layout with hoisted
single-source `_` literals (`_CUDA`, `_WORK_DIR`/`_ONNX_DIR`/`_TENSORRT_DIR`, `_LIDAR_BEV_SHAPE`,
voxel-profile literals) and a cleaned/accurate docstring. **All values preserved** — verified by
`exec()`-ing the file and asserting every resolved value (incl. `engine_dir`) matches the original.
- [~] **P0-3 (DEFERRED)** `_base_` dedup of the two variants. The base bakes computed paths
(`_TENSORRT_DIR` → `evaluation.backends.tensorrt.engine_dir`), so a child overriding
`export.work_dir` would silently keep the base's `engine_dir` unless it also overrides that nested
path — fragile, and unverifiable here (no mmengine to run `Config.fromfile`). Do this in Docker
where the resolved dicts can be asserted equal, or after refactoring the base to not bake derived
paths. Left the three explicit configs as-is (they work).

## Tier E — cross-project entrypoint dedup

- [x] **P0-1** Added `runtime/detection3d_entrypoint.py::run_detection3d_deployment(...)`. Both
`bevfusion_l/entrypoint.py` and `centerpoint/entrypoint.py` are now ~30 lines that inject only
`pipeline_name` + `config_factory` + `executor_factory` + `runner_factory`. The ~90% duplicated
wiring lives once.

---

## Intentionally skipped (cosmetic / would be churn, not value)

- [~] `DeviceSpec.to_ort_provider` / `to_torch_device` "leaky primitive" — pragmatic, low ROI.
- [~] 44-line docstring on `resolve_artifact_path`; `# ===` section banners; PEP585-vs-`typing`
generics mixing; `__post_init__` re-validating a `Literal`; `Artifact.exists` (dir-OK) vs
resolver `is_file()`.
- [~] model_loader strategy divergence (CenterPoint type-swap vs BEVFusion wrappers), split/merged
branching, CPU-vs-CUDA load, 2-vs-3 backends, component naming — justified divergences.

---

## Re-audit results

- [x] `ast.parse` clean on all 17 changed `.py`.
- [x] `pyflakes` clean (no unused imports / undefined names) on all changed `.py`.
- [x] CLI package discovery lists exactly `['bevfusion_l', 'centerpoint']`.
- [x] grep: no references to removed symbols (`_pick_bound_input_name`, `_engine_sparse`,
`_apply_spconv_do_sort`, `_get_num_proposals`, 2-arg `load_tensorrt_plugin_libraries`).
- [x] enum + comparator-nan behavior smoke-tested; deploy_config values `exec()`-verified.
- [ ] **Docker e2e smoke test** (`bevfusion_l` + `centerpoint` export/eval) — REQUIRED, not run here.
Loading