File size: 6,717 Bytes
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# SPDX-License-Identifier: Apache-2.0
"""ONNX Runtime on the shipped v5.0 ONNX files: the oracle of the CPU reference (research and tests only;
onnxruntime is not a runtime dependency of the bundle or the image).

``OrtPlanner.run(raw)`` is the Autoware ``multi_step`` pipeline with ORT as the backend (a port of the verified
research script ``research/diffusion-planner/scripts/dp_reference.py``): the node's normalization and speed masks,
encoder.onnx, the DPM-Solver++(2M) loop of :mod:`tt_diffusion_planner.host.solver` calling decoder.onnx 11 times,
then turn_indicator.onnx. ``taps=[...]`` exposes intermediate tensors of the encoder / decoder graphs by ONNX tensor
name (the decoder taps are recorded per evaluation), for the per-module PCC test of the reference.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Sequence

import numpy as np

from . import config as C
from ..host.normalize import normalize_inputs, speed_masks
from ..host.solver import apply_prefix_constraint, dpm_solver_sample
from ..host.features import decoder_masks
from ..ttaw import io as tio
from .weights import load_param_json

__all__ = ["OrtPlanner", "OrtResult", "available"]


def available() -> bool:
    import importlib.util

    return importlib.util.find_spec("onnxruntime") is not None


def _session(model: Any, threads: int, optimize: bool = True):
    import onnxruntime as ort

    so = ort.SessionOptions()
    so.graph_optimization_level = (ort.GraphOptimizationLevel.ORT_ENABLE_ALL if optimize
                                   else ort.GraphOptimizationLevel.ORT_DISABLE_ALL)
    so.intra_op_num_threads = threads
    so.inter_op_num_threads = 1
    so.log_severity_level = 3
    src = model.SerializeToString() if hasattr(model, "SerializeToString") else str(model)
    return ort.InferenceSession(src, so, providers=["CPUExecutionProvider"])


def _with_outputs(path: Path, names: Sequence[str]):
    """The model with extra graph outputs (intermediate tensors by name)."""
    import onnx

    m = onnx.load(str(path))
    have = {o.name for o in m.graph.output}
    m.graph.output.extend([onnx.ValueInfoProto(name=n) for n in names if n not in have])
    return m


@dataclass
class OrtResult:
    norm: Dict[str, np.ndarray]
    encoding: np.ndarray                 # [1, 564, 256]
    final_x0: np.ndarray                 # [1, 321, 81, 4]
    logit: np.ndarray                    # [1, 5]
    denoising_steps: List[np.ndarray]
    denoising_timesteps: List[float]
    eval_times: List[float]
    eval_inputs: List[np.ndarray] = field(default_factory=list)
    eval_outputs: List[np.ndarray] = field(default_factory=list)
    taps: Dict[str, np.ndarray] = field(default_factory=dict)


class OrtPlanner:
    """``OrtPlanner(weights_dir, threads=4, encoder_taps=(...), decoder_taps=(...))``. With taps the sessions run
    without graph optimizations (every intermediate kept as computed); without, ``ORT_ENABLE_ALL`` like
    ``onnxruntime_inference.cpp:172``."""

    def __init__(self, weights_dir: Path, *, threads: int = 4, encoder_taps: Sequence[str] = (),
                 decoder_taps: Sequence[str] = (), turn_taps: Sequence[str] = ()):
        wd = Path(weights_dir)
        self.normalization = load_param_json(wd / C.PARAM_JSON)
        self.encoder_taps, self.decoder_taps, self.turn_taps = list(encoder_taps), list(decoder_taps), list(turn_taps)
        opt = not (encoder_taps or decoder_taps or turn_taps)
        self.enc = _session(_with_outputs(wd / C.ENCODER_ONNX, self.encoder_taps), threads, opt)
        self.dec = _session(_with_outputs(wd / C.DECODER_ONNX, self.decoder_taps), threads, opt)
        self.turn = _session(_with_outputs(wd / C.TURN_INDICATOR_ONNX, self.turn_taps), threads, opt)

    def normalize(self, raw: Any) -> Dict[str, np.ndarray]:
        arrays = tio.load_named_arrays(raw, C.INPUT_SCHEMA)
        norm = normalize_inputs(arrays, self.normalization.observation)
        norm.update(speed_masks(norm))
        return norm

    def encode(self, norm: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
        feed = {k: norm[k] for k in C.ENCODER_INPUTS}
        outs = self.enc.run(["encoding"] + self.encoder_taps, feed)
        return dict(zip(["encoding"] + self.encoder_taps, outs))

    def decode(self, encoding: np.ndarray, x: np.ndarray, t, neighbor_agents_past: np.ndarray) -> Dict[str, Any]:
        dt = np.full((1, C.MAX_NUM_AGENTS, C.OUTPUT_T + 1, 1), np.float32(t), np.float32)
        feed = {"encoding": encoding, "sampled_trajectories": np.asarray(x, np.float32).reshape(
            1, C.MAX_NUM_AGENTS, C.OUTPUT_T + 1, C.POSE_DIM), "diffusion_time": dt,
            "neighbor_agents_past": neighbor_agents_past}
        outs = self.dec.run(["model_output"] + self.decoder_taps, feed)
        return dict(zip(["model_output"] + self.decoder_taps, outs))

    def turn_logit(self, encoding: np.ndarray, final_x0: np.ndarray) -> Dict[str, np.ndarray]:
        outs = self.turn.run(["turn_indicator_logit"] + self.turn_taps,
                             {"encoding": encoding, "final_x0": np.asarray(final_x0, np.float32)})
        return dict(zip(["turn_indicator_logit"] + self.turn_taps, outs))

    def run(self, raw: Any, *, steps: int = C.DPM_SOLVER_STEPS, keep_eval_io: bool = False) -> OrtResult:
        norm = self.normalize(raw)
        enc = self.encode(norm)
        encoding = enc["encoding"]
        taps: Dict[str, np.ndarray] = {f"enc:{k}": v for k, v in enc.items() if k != "encoding"}
        cs = decoder_masks(norm).current_states
        nb = norm["neighbor_agents_past"]
        eval_in: List[np.ndarray] = []
        eval_out: List[np.ndarray] = []

        def model_fn(x: np.ndarray, t) -> np.ndarray:
            k = len(eval_out)
            out = self.decode(encoding, x, t, nb)
            for name in self.decoder_taps:
                taps[f"dec{k}:{name}"] = out[name]
            y = out["model_output"][0]
            eval_out.append(y if keep_eval_io else np.empty(0))
            if keep_eval_io:
                eval_in.append(np.array(x, np.float32, copy=True))
            return y

        res = dpm_solver_sample(norm["sampled_trajectories"][0], model_fn,
                                lambda x: apply_prefix_constraint(x, cs), steps)
        tl = self.turn_logit(encoding, res.final_x[None])
        taps.update({f"turn:{k}": v for k, v in tl.items() if k != "turn_indicator_logit"})
        return OrtResult(norm, encoding, res.final_x[None], tl["turn_indicator_logit"], res.denoising_steps,
                         res.denoising_timesteps, res.eval_times, eval_in, eval_out if keep_eval_io else [], taps)