omnius 1.0.627 → 1.0.629

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,335 @@
1
+ #!/usr/bin/env python3
2
+ """Persistent CUDA/TensorRT YAMNet worker for Omnius audio classification.
3
+
4
+ The process deliberately accepts only already-captured WAV paths. It never
5
+ opens an ALSA device and never downloads a model. Omnius bootstrap stages and
6
+ checksums the ONNX model and TensorRT plan before this worker is launched.
7
+
8
+ Protocol: JSON Lines on stdin/stdout. stdout is protocol-only; diagnostics go
9
+ to stderr so callers cannot confuse logs with inference results.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import csv
15
+ import hashlib
16
+ import json
17
+ import os
18
+ import sys
19
+ import time
20
+ import traceback
21
+ import wave
22
+
23
+
24
+ def emit(payload: dict) -> None:
25
+ sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
26
+ sys.stdout.flush()
27
+
28
+
29
+ def fail(message: str) -> None:
30
+ emit({"type": "fatal", "error": message})
31
+ raise RuntimeError(message)
32
+
33
+
34
+ def cuda_check(result, label: str):
35
+ """cuda-python returns either an error enum or (error enum, value)."""
36
+ if isinstance(result, tuple):
37
+ status, value = result[0], result[1:]
38
+ else:
39
+ status, value = result, ()
40
+ if int(status) != 0:
41
+ raise RuntimeError(f"CUDA {label} failed: {status}")
42
+ if len(value) == 0:
43
+ return None
44
+ return value[0] if len(value) == 1 else value
45
+
46
+
47
+ class CudaPythonRuntime:
48
+ """Thin adapter over JetPack's cuda-python bindings."""
49
+ def __init__(self, cudart):
50
+ self.cudart = cudart
51
+
52
+ def stream_create(self):
53
+ return cuda_check(self.cudart.cudaStreamCreate(), "stream create")
54
+
55
+ def alloc(self, size, label):
56
+ return cuda_check(self.cudart.cudaMalloc(size), f"{label} allocation")
57
+
58
+ def h2d(self, device, host, stream, label):
59
+ cuda_check(
60
+ self.cudart.cudaMemcpyAsync(
61
+ device,
62
+ host.ctypes.data,
63
+ host.nbytes,
64
+ self.cudart.cudaMemcpyKind.cudaMemcpyHostToDevice,
65
+ stream,
66
+ ),
67
+ f"{label} copy",
68
+ )
69
+
70
+ def d2h(self, host, device, stream, label):
71
+ cuda_check(
72
+ self.cudart.cudaMemcpyAsync(
73
+ host.ctypes.data,
74
+ device,
75
+ host.nbytes,
76
+ self.cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost,
77
+ stream,
78
+ ),
79
+ f"{label} copy",
80
+ )
81
+
82
+ def synchronize(self, stream):
83
+ cuda_check(self.cudart.cudaStreamSynchronize(stream), "stream synchronize")
84
+
85
+ def free(self, allocation):
86
+ self.cudart.cudaFree(allocation)
87
+
88
+ def device_metadata(self):
89
+ device = cuda_check(self.cudart.cudaGetDevice(), "get device")
90
+ props = cuda_check(self.cudart.cudaGetDeviceProperties(device), "get device properties")
91
+ name = getattr(props, "name", "CUDA device")
92
+ if isinstance(name, bytes):
93
+ name = name.split(b"\0", 1)[0].decode("utf8", "replace")
94
+ return str(name), f"{getattr(props, 'major', 0)}.{getattr(props, 'minor', 0)}"
95
+
96
+
97
+ class PyCudaRuntime:
98
+ """JetPack also ships python3-pycuda on supported L4T images.
99
+
100
+ Keeping this fallback avoids a generic PyPI CUDA runtime wheel: both
101
+ backends call the CUDA libraries supplied by the installed JetPack image.
102
+ """
103
+ def __init__(self, cuda):
104
+ cuda.init()
105
+ self.cuda = cuda
106
+ self.device = cuda.Device(0)
107
+ self.context = self.device.make_context()
108
+
109
+ def stream_create(self):
110
+ return self.cuda.Stream()
111
+
112
+ def alloc(self, size, _label):
113
+ return self.cuda.mem_alloc(size)
114
+
115
+ def h2d(self, device, host, stream, _label):
116
+ self.cuda.memcpy_htod_async(device, host, stream)
117
+
118
+ def d2h(self, host, device, stream, _label):
119
+ self.cuda.memcpy_dtoh_async(host, device, stream)
120
+
121
+ def synchronize(self, stream):
122
+ stream.synchronize()
123
+
124
+ def free(self, allocation):
125
+ allocation.free()
126
+
127
+ def stream_handle(self, stream):
128
+ return stream.handle
129
+
130
+ def device_metadata(self):
131
+ major, minor = self.device.compute_capability()
132
+ return self.device.name(), f"{major}.{minor}"
133
+
134
+
135
+ def read_wav(path: str, np):
136
+ started = time.perf_counter()
137
+ with wave.open(path, "rb") as reader:
138
+ channels = reader.getnchannels()
139
+ sample_width = reader.getsampwidth()
140
+ sample_rate = reader.getframerate()
141
+ frames = reader.getnframes()
142
+ compression = reader.getcomptype()
143
+ raw = reader.readframes(frames)
144
+ if compression != "NONE":
145
+ raise RuntimeError("Only uncompressed RIFF/WAV input is supported")
146
+ if channels != 1:
147
+ raise RuntimeError(f"Expected mono WAV from the caller, received {channels} channels")
148
+ if sample_width != 2:
149
+ raise RuntimeError(f"Expected PCM16 WAV from the caller, received {sample_width * 8}-bit samples")
150
+ if sample_rate != 16000:
151
+ raise RuntimeError(f"Expected 16 kHz WAV from the caller, received {sample_rate} Hz")
152
+ waveform = np.frombuffer(raw, dtype="<i2").astype(np.float32) / 32768.0
153
+ return waveform, sample_rate, (time.perf_counter() - started) * 1000
154
+
155
+
156
+ class YAMNetTensorRT:
157
+ def __init__(self, engine_path: str, class_map_path: str):
158
+ started = time.perf_counter()
159
+ import numpy as np
160
+ import tensorrt as trt
161
+ try:
162
+ from cuda import cudart
163
+ cuda = CudaPythonRuntime(cudart)
164
+ except ImportError:
165
+ import pycuda.driver as pycuda
166
+ cuda = PyCudaRuntime(pycuda)
167
+
168
+ self.np = np
169
+ self.trt = trt
170
+ self.cuda = cuda
171
+ self.logger = trt.Logger(trt.Logger.ERROR)
172
+ with open(engine_path, "rb") as handle:
173
+ serialized = handle.read()
174
+ self.runtime = trt.Runtime(self.logger)
175
+ self.engine = self.runtime.deserialize_cuda_engine(serialized)
176
+ if self.engine is None:
177
+ raise RuntimeError("TensorRT could not deserialize the YAMNet engine")
178
+ self.context = self.engine.create_execution_context()
179
+ if self.context is None:
180
+ raise RuntimeError("TensorRT could not create a YAMNet execution context")
181
+ self.stream = cuda.stream_create()
182
+ self.input_name = None
183
+ self.output_names = []
184
+ for index in range(self.engine.num_io_tensors):
185
+ name = self.engine.get_tensor_name(index)
186
+ if self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT:
187
+ self.input_name = name
188
+ else:
189
+ self.output_names.append(name)
190
+ if self.input_name is None or not self.output_names:
191
+ raise RuntimeError("YAMNet TensorRT engine did not expose input and output tensors")
192
+ with open(class_map_path, newline="", encoding="utf8") as handle:
193
+ self.classes = [row["display_name"] for row in csv.DictReader(handle)]
194
+ if len(self.classes) != 521:
195
+ raise RuntimeError(f"YAMNet class map must contain 521 classes; found {len(self.classes)}")
196
+ self.device_name, self.compute_capability = self._device_metadata()
197
+ self.model_load_ms = (time.perf_counter() - started) * 1000
198
+
199
+ def _device_metadata(self):
200
+ # TensorRT uses the CUDA-visible ordinal. Omnius preflight limits that
201
+ # namespace to exactly the approved Jetson GPU before process launch.
202
+ return self.cuda.device_metadata()
203
+
204
+ def warm(self):
205
+ # YAMNet needs >=0.975 seconds. This is a model-load probe, not a
206
+ # captured-audio classification, and it verifies the TensorRT path is
207
+ # actually executable before readiness is reported.
208
+ self.infer(self.np.zeros(15600, dtype=self.np.float32), 1)
209
+
210
+ def infer(self, waveform, top_k: int):
211
+ started = time.perf_counter()
212
+ np = self.np
213
+ if waveform.ndim != 1 or waveform.size < 15600:
214
+ # YAMNet's framing kernel requires 0.975 s; pad silence only to
215
+ # satisfy the model shape, without altering any supplied samples.
216
+ waveform = np.pad(waveform.reshape(-1), (0, max(0, 15600 - waveform.size)))
217
+ waveform = np.ascontiguousarray(waveform.astype(np.float32, copy=False))
218
+ if not self.context.set_input_shape(self.input_name, tuple(waveform.shape)):
219
+ raise RuntimeError(f"TensorRT rejected YAMNet input shape {tuple(waveform.shape)}")
220
+
221
+ allocations = []
222
+ host_outputs = {}
223
+ try:
224
+ input_bytes = waveform.nbytes
225
+ input_device = self.cuda.alloc(input_bytes, "input")
226
+ allocations.append(input_device)
227
+ self.context.set_tensor_address(self.input_name, int(input_device))
228
+ for name in self.output_names:
229
+ shape = tuple(self.context.get_tensor_shape(name))
230
+ if any(int(dim) < 0 for dim in shape):
231
+ raise RuntimeError(f"TensorRT did not resolve output shape for {name}: {shape}")
232
+ dtype = self.trt.nptype(self.engine.get_tensor_dtype(name))
233
+ host = np.empty(shape, dtype=dtype)
234
+ device = self.cuda.alloc(host.nbytes, name)
235
+ allocations.append(device)
236
+ host_outputs[name] = (host, device)
237
+ self.context.set_tensor_address(name, int(device))
238
+ self.cuda.h2d(input_device, waveform, self.stream, "input")
239
+ stream_handle = self.cuda.stream_handle(self.stream) if hasattr(self.cuda, "stream_handle") else self.stream
240
+ if not self.context.execute_async_v3(stream_handle):
241
+ raise RuntimeError("TensorRT YAMNet execute_async_v3 returned false")
242
+ for name, (host, device) in host_outputs.items():
243
+ self.cuda.d2h(host, device, self.stream, name)
244
+ self.cuda.synchronize(self.stream)
245
+ # The converted Google model exposes output_0 with AudioSet scores.
246
+ scores = next((value[0] for name, value in host_outputs.items() if value[0].shape[-1] == 521), None)
247
+ if scores is None:
248
+ raise RuntimeError("TensorRT YAMNet engine did not produce a 521-class score tensor")
249
+ mean_scores = np.mean(scores, axis=0)
250
+ indices = np.argsort(mean_scores)[-top_k:][::-1]
251
+ return [
252
+ {"label": self.classes[int(index)], "confidence": float(mean_scores[int(index)])}
253
+ for index in indices
254
+ ], (time.perf_counter() - started) * 1000
255
+ finally:
256
+ for allocation in allocations:
257
+ try:
258
+ self.cuda.free(allocation)
259
+ except Exception:
260
+ pass
261
+
262
+
263
+ def file_digest(path: str) -> str:
264
+ digest = hashlib.sha256()
265
+ with open(path, "rb") as handle:
266
+ while True:
267
+ chunk = handle.read(1024 * 1024)
268
+ if not chunk:
269
+ break
270
+ digest.update(chunk)
271
+ return "sha256:" + digest.hexdigest()
272
+
273
+
274
+ def main() -> int:
275
+ parser = argparse.ArgumentParser()
276
+ parser.add_argument("--engine", required=True)
277
+ parser.add_argument("--class-map", required=True)
278
+ parser.add_argument("--model-digest", required=True)
279
+ args = parser.parse_args()
280
+ # CUDA_VISIBLE_DEVICES must be a single approved accelerator before any
281
+ # CUDA/TensorRT import. Jetson's integrated GPU is logical device 0.
282
+ visible = os.environ.get("CUDA_VISIBLE_DEVICES", "").strip()
283
+ if visible != "0":
284
+ fail("Audio TensorRT worker requires exactly CUDA_VISIBLE_DEVICES=0 on Jetson")
285
+ worker = YAMNetTensorRT(args.engine, args.class_map)
286
+ worker.warm()
287
+ emit({
288
+ "type": "ready",
289
+ "pid": os.getpid(),
290
+ "backend": "tensorrt-fp16",
291
+ "device": worker.device_name,
292
+ "compute_capability": worker.compute_capability,
293
+ "cuda_visible_devices": visible,
294
+ "model": "yamnet",
295
+ "model_digest": args.model_digest,
296
+ "taxonomy": "AudioSet-521",
297
+ "model_load_ms": round(worker.model_load_ms, 3),
298
+ "warmed": True,
299
+ })
300
+ for line in sys.stdin:
301
+ try:
302
+ request = json.loads(line)
303
+ if request.get("type") == "shutdown":
304
+ emit({"type": "stopped"})
305
+ return 0
306
+ if request.get("type") != "classify":
307
+ raise RuntimeError("unknown request type")
308
+ request_id = str(request.get("id") or "")
309
+ file_path = str(request.get("file") or "")
310
+ if not request_id or not file_path:
311
+ raise RuntimeError("classify requires id and file")
312
+ waveform, sample_rate, decode_ms = read_wav(file_path, worker.np)
313
+ classifications, inference_ms = worker.infer(waveform, max(1, min(int(request.get("top_k", 5)), 25)))
314
+ emit({
315
+ "type": "result",
316
+ "id": request_id,
317
+ "success": True,
318
+ "sample_rate_hz": sample_rate,
319
+ "duration_seconds": round(float(waveform.size) / sample_rate, 6),
320
+ "classifications": classifications,
321
+ "timings_ms": {"decode": round(decode_ms, 3), "inference": round(inference_ms, 3)},
322
+ })
323
+ except Exception as exc: # keep the persistent worker alive per request
324
+ emit({
325
+ "type": "result",
326
+ "id": str(locals().get("request", {}).get("id", "")),
327
+ "success": False,
328
+ "error": str(exc),
329
+ })
330
+ print(traceback.format_exc(), file=sys.stderr, flush=True)
331
+ return 0
332
+
333
+
334
+ if __name__ == "__main__":
335
+ raise SystemExit(main())