include steps for ai-hub
This commit is contained in:
@@ -153,7 +153,7 @@ Or pass the job name explicitly:
|
||||
qc-cli train status qc-cli-YYYYMMDD-HHMMSS
|
||||
```
|
||||
|
||||
## Outputs
|
||||
## SageMaker Outputs
|
||||
|
||||
When the job completes, SageMaker packages the files written under `/opt/ml/model` into `model.tar.gz`.
|
||||
|
||||
@@ -167,6 +167,91 @@ metrics.json
|
||||
|
||||
The archive is stored under the configured `s3.model_prefix`.
|
||||
|
||||
## 6. Configure Qualcomm AI Hub
|
||||
|
||||
Authenticate with Qualcomm AI Hub:
|
||||
|
||||
```bash
|
||||
qai-hub configure --api_token
|
||||
```
|
||||
|
||||
Add AI Hub settings to `config.yaml`. The input name and image size must match the ONNX model exported by this example:
|
||||
|
||||
```yaml
|
||||
aihub:
|
||||
device:
|
||||
name: Dragonwing IQ-9075 EVK
|
||||
target_runtime: tflite
|
||||
input_specs:
|
||||
images: [[1, 3, 640, 640], float32]
|
||||
job_name: meter-detection
|
||||
model_name: meter-detection
|
||||
output_dir: build/qai-hub/meter-detection
|
||||
```
|
||||
|
||||
Use the same image size configured in `sagemaker.training.hyperparameters.imgsz`. For example, a smoke-test model
|
||||
trained with `imgsz: 320` requires `images: [[1, 3, 320, 320], float32]`.
|
||||
|
||||
## 7. Prepare AI Hub Inputs
|
||||
|
||||
Generate calibration samples and a validation input from the downloaded dataset:
|
||||
|
||||
```bash
|
||||
uv run python examples/meter-detection/prepare_aihub_inputs.py --image-size 640
|
||||
```
|
||||
|
||||
This writes:
|
||||
|
||||
```text
|
||||
examples/meter-detection/data/aihub_calibration/*.npy
|
||||
examples/meter-detection/data/inputs.npz
|
||||
```
|
||||
|
||||
The script applies the preprocessing expected by the exported YOLO model: aspect-ratio-preserving letterboxing,
|
||||
RGB channel order, channel-first layout, and pixel values normalized to `[0, 1]`.
|
||||
|
||||
Set `--image-size` to the training `imgsz` value when it is not `640`.
|
||||
|
||||
## 8. Upload To Qualcomm AI Hub
|
||||
|
||||
Use the SageMaker job name printed by `qc-cli train start`:
|
||||
|
||||
```bash
|
||||
qc-cli ai-hub upload \
|
||||
examples/meter-detection/data/aihub_calibration \
|
||||
examples/meter-detection/data/inputs.npz \
|
||||
--from-job qc-cli-YYYYMMDD-HHMMSS
|
||||
```
|
||||
|
||||
The command downloads the job's `model.tar.gz`, finds `model.onnx`, uploads it to AI Hub, and runs quantization,
|
||||
compilation, validation, and profiling. The uploaded source model uses the configured
|
||||
`aihub.model_name`.
|
||||
|
||||
If the meter-detection job is still the last training job in `.qc-cli.json`, `--from-job` can be omitted. Keeping it
|
||||
explicit prevents accidentally uploading an artifact from a different training run.
|
||||
|
||||
To resume after a completed step, use one of:
|
||||
|
||||
```bash
|
||||
qc-cli ai-hub upload \
|
||||
examples/meter-detection/data/aihub_calibration \
|
||||
examples/meter-detection/data/inputs.npz \
|
||||
--from-step compile
|
||||
```
|
||||
|
||||
```bash
|
||||
qc-cli ai-hub upload \
|
||||
examples/meter-detection/data/aihub_calibration \
|
||||
examples/meter-detection/data/inputs.npz \
|
||||
--from-step validate
|
||||
```
|
||||
|
||||
Download the compiled artifact after the workflow completes:
|
||||
|
||||
```bash
|
||||
qc-cli ai-hub download --output build/qai-hub/meter-detection/model.tflite
|
||||
```
|
||||
|
||||
## Training Hyperparameters
|
||||
|
||||
Values under `sagemaker.training.hyperparameters` are passed to `source/train.py` as command-line arguments.
|
||||
|
||||
92
examples/meter-detection/prepare_aihub_inputs.py
Normal file
92
examples/meter-detection/prepare_aihub_inputs.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare Qualcomm AI Hub calibration and validation inputs for the meter detector."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png"}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--dataset-dir",
|
||||
type=Path,
|
||||
default=Path("examples/meter-detection/data/electric-meter-detection"),
|
||||
help="Root of the extracted Roboflow dataset.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--calibration-dir",
|
||||
type=Path,
|
||||
default=Path("examples/meter-detection/data/aihub_calibration"),
|
||||
help="Directory where .npy calibration samples will be written.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-file",
|
||||
type=Path,
|
||||
default=Path("examples/meter-detection/data/inputs.npz"),
|
||||
help="Validation .npz input file for qc-cli ai-hub validate.",
|
||||
)
|
||||
parser.add_argument("--input-name", default="images", help="ONNX input name.")
|
||||
parser.add_argument("--image-size", type=int, default=640, help="Square image size used for ONNX export.")
|
||||
parser.add_argument("--samples", type=int, default=16, help="Number of calibration samples to write.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def preprocess_image(path: Path, image_size: int) -> np.ndarray:
|
||||
"""Apply Ultralytics-style letterboxing and produce an NCHW float32 tensor."""
|
||||
with Image.open(path) as source:
|
||||
image = source.convert("RGB")
|
||||
|
||||
scale = min(image_size / image.width, image_size / image.height)
|
||||
resized_width = round(image.width * scale)
|
||||
resized_height = round(image.height * scale)
|
||||
image = image.resize((resized_width, resized_height), Image.Resampling.BILINEAR)
|
||||
|
||||
canvas = Image.new("RGB", (image_size, image_size), (114, 114, 114))
|
||||
left = round((image_size - resized_width) / 2 - 0.1)
|
||||
top = round((image_size - resized_height) / 2 - 0.1)
|
||||
canvas.paste(image, (left, top))
|
||||
|
||||
array = np.asarray(canvas, dtype=np.float32) / 255.0
|
||||
return np.transpose(array, (2, 0, 1))[None, ...].astype(np.float32)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.image_size < 1:
|
||||
raise SystemExit("--image-size must be at least 1")
|
||||
if args.samples < 1:
|
||||
raise SystemExit("--samples must be at least 1")
|
||||
|
||||
images = sorted(
|
||||
path
|
||||
for path in args.dataset_dir.rglob("*")
|
||||
if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS and path.parent.name == "images"
|
||||
)
|
||||
if not images:
|
||||
raise SystemExit(f"No images found under {args.dataset_dir}")
|
||||
|
||||
args.calibration_dir.mkdir(parents=True, exist_ok=True)
|
||||
args.input_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
for stale_sample in args.calibration_dir.glob("sample_*.npy"):
|
||||
stale_sample.unlink()
|
||||
|
||||
prepared: list[np.ndarray] = []
|
||||
for index, image_path in enumerate(images[: args.samples]):
|
||||
sample = preprocess_image(image_path, args.image_size)
|
||||
np.save(args.calibration_dir / f"sample_{index:03d}.npy", sample)
|
||||
prepared.append(sample)
|
||||
|
||||
np.savez(args.input_file, **{args.input_name: prepared[0]}) # pyright: ignore[reportArgumentType]
|
||||
print(f"Wrote {len(prepared)} calibration samples to {args.calibration_dir}")
|
||||
print(f"Wrote validation input to {args.input_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user