Getting Hailo Dataflow Compiler running in Docker on Windows

I have found it very challenging to get Dataflow Compiler running in a way that is aligned with my development pipeline on a Windows machine.
My development pipeleine uses the general philosophy that I should eb able to define everything in code, and re-create the envrionment anywhere with ease. Why? Because the me of tomorrow will forget everything.

Here is a tutorial explaining how I created a Docker image that incoporates Hailo Dataflow Compiler, and integrated it wit VSCode for easy debugging

I hope it helps.

2 Likes

This was helpful, I was struggling enough that I put my Hailo8 on the shelf, thinking it was a waste since I haven’t been able to convert my onnx model to hef.
I may have missed a step in your instructions, was I supposed to create a main.py file in the src directory? I can -it --entrypoint bash into the a container and can create one in VS but I am not sure what was supposed to be in there.
Thanks,
Teig

Just to let you know, that after I got past little hiccup I was able to launch jupyter and convert my model from onnx to hef format. Thanks for the help,

Hi @Teig_Levingston, Thanks for pointing that out. I think I wasnt really clear in the tutoiral. Yes, there should be a src/main.py file. I provided an example at the very end of the tutorial. But it is just an example. The tutorial was aminly focused on getting the environment set-up because that was for me the biggest hurddle (I also but my Hailo8 on the shelf for a few months because of that frustration :sweat_smile:).

The main.py file i am using now looks like this:

# src/main.py
import hydra
from omegaconf import DictConfig
from parse_onnx import parse_onnx
from compile_hef import compile_hef
from make_alls import make_alls

@hydra.main(version_base=None, config_path="conf", config_name="config")
def main(cfg: DictConfig):
    onnx_path = cfg.get("onnx_path")
    calib_folder = cfg.get("calibration_images")
    net_name = cfg.get("net_name")
    build_dir = cfg.get("hef_build_dir")
    hw_arch = cfg.get("hw_arch")
    alls_script = make_alls(cfg)
    end_node_names = cfg.get("end_node_names")
    har_filename = cfg.get("har_filename")
    
    parse_onnx(onnx_path, calib_folder, net_name, hw_arch, end_node_names, alls_script=alls_script)

    compile_hef(har_filename, build_dir, hw_arch)

if __name__ == "__main__":
    main()```

This uses hydra-core for the config. The config file is like this:
```yaml
# src/conf/config.yaml
end_node_names:
  - "/model.22/cv2.0/cv2.0.2/Conv"
  - "/model.22/cv3.0/cv3.0.2/Conv"
  - "/model.22/cv2.1/cv2.1.2/Conv"
  - "/model.22/cv3.1/cv3.1.2/Conv"
  - "/model.22/cv2.2/cv2.2.2/Conv"
  - "/model.22/cv3.2/cv3.2.2/Conv"

onnx_path: "./models/best.onnx"

calibration_images: "./calibration_images"

net_name: "yolov8n"

hef_build_dir: "./models/build"
har_filename: "yolov8n_quantized_model.har"

hw_arch: "hailo8l"

image_size:
  - 640
  - 640

alls:
  normalization:
    min:
      - 0.0
      - 0.0
      - 0.0
    max:
      - 255.0
      - 255.0
      - 255.0
  nms:
    meta_arch: yolov8
    nms_scores_th: 0.25
    nms_iou_th: 0.45
    engine: cpu

Then the imported scripts.
There is this one for making an alls script.

# src/make_alls.py
from textwrap import dedent
from pathlib import Path

def make_alls(cfg):
    img_size = cfg.get("image_size")
    alls_cfg = cfg.get("alls")
    norm_min = alls_cfg.get("normalization", {}).get("min", [0,0,0])
    norm_max = alls_cfg.get("normalization", {}).get("max", [255,255,255])
    nms_cfg = alls_cfg.get("nms", {})
    alls_content = dedent(f"""
    normalization1 = normalization({norm_min}, {norm_max})
    resize_input1 = resize(resize_shapes={img_size})
    nms_postprocess(meta_arch={nms_cfg.get("meta_arch")}, engine={nms_cfg.get("engine")}, nms_scores_th={nms_cfg.get("nms_scores_th")}, nms_iou_th={nms_cfg.get("nms_iou_th")})
    model_optimization_flavor(optimization_level=2, compression_level=1, batch_size=8)
    """)
    out_path = "./script.alls"
    Path(out_path).write_text(alls_content.strip())
    return out_path

This one for parsing the onnx to har

# src/parse_onnx.py
import os
import onnx
from onnxsim import simplify
import numpy as np
from hailo_sdk_client import ClientRunner
from PIL import Image


def load_calibration_dataset(calib_folder, output_har_filename, target_size=(640, 640)):
    """
    Loads and preprocesses images from the specified folder.
    Args:
        calib_folder (str): Path to the folder with calibration images.
        target_size (tuple): Desired image size as (width, height).
    Returns:
        np.ndarray: A numpy array of shape (num_images, height, width, 3) in float32.
    """
    image_files = [
        os.path.join(calib_folder, f)
        for f in os.listdir(calib_folder)
        if f.lower().endswith(('.jpg', '.png', '.jpeg'))
    ]
    if not image_files:
        raise ValueError("No calibration images found in the folder.")
    
    images = []
    for img_file in sorted(image_files):
        img = Image.open(img_file).convert("RGB")
        # Resize using bilinear interpolation
        img = img.resize(target_size, Image.BILINEAR)
        img_np = np.array(img).astype(np.float32)
        images.append(img_np)
    
    calib_dataset = np.stack(images, axis=0)
    print(f"Loaded {calib_dataset.shape[0]} calibration images of size {target_size}.")
    return calib_dataset


def parse_onnx(onnx_path: str, calib_folder: str, net_name: str, hw_arch: str, end_node_names, target_size: tuple = (640, 640), alls_script: str = "./script.alls"):
    # -------------------------------
    # Step 1. Simplify the ONNX model
    # -------------------------------

    # Load the ONNX model
    model = onnx.load(onnx_path)

    # Simplify the model
    model_simp, check = simplify(model)
    if not check:
        raise RuntimeError("Simplified ONNX model validation failed.")
        
    # Save the simplified model to a new file
    simplified_onnx_path = os.path.splitext(onnx_path)[0] + "_simplified.onnx"
    onnx.save(model_simp, simplified_onnx_path)
    print(f"Simplified ONNX model saved to: {simplified_onnx_path}")

    # -----------------------------------------------------
    # Step 2. Translate the simplified ONNX model to Hailo format
    # -----------------------------------------------------

    # Create a ClientRunner instance.
    # The hw_arch parameter should match your target Hailo device (e.g., "hailo8")
    runner = ClientRunner(hw_arch=hw_arch, har=None)

    # Translate the ONNX model. Optionally, you can supply start and end node names if needed.
    hn, params = runner.translate_onnx_model(simplified_onnx_path, net_name, end_node_names=end_node_names)
    print("Model translation to Hailo format completed.")

    # -----------------------------------------------------
    # Step 3. Quantize the model using a calibration dataset
    # -----------------------------------------------------
    # For quantization, you need a calibration dataset.
    # Here we create a dummy calibration dataset.
    # Adjust the shape (batch, height, width, channels) as required by your model.
    # For many YOLO models the expected input is 640x640 with 3 channels.
    calib_dataset = load_calibration_dataset(calib_folder, target_size)
    print("Calibration dataset created.")

    # Run optimization (quantization). This process uses the calibration dataset to
    # convert floating-point parameters into their quantized (integer) counterparts.
    runner.load_model_script(alls_script)
    runner.optimize(calib_dataset)
    print("Model quantization complete.")

    # -----------------------------------------------------
    # Step 4. Save the quantized model as an HAR file
    # -----------------------------------------------------
    har_file = f"{net_name}_quantized_model.har"
    runner.save_har(har_file)
    print(f"HAR file saved to: {har_file}")
    return har_file

and then this one for compiling the hef

# src/compile_hef.py
from hailo_sdk_client import ClientRunner
import os

def compile_hef(har_file,  build_dir, hw_arch="hailo8l",):

    os.makedirs(build_dir, exist_ok=True)

    runner = ClientRunner(har=har_file, hw_arch=hw_arch)
    hef_data = runner.compile()
    hef_file = os.path.join(build_dir, os.path.basename(har_file).split(".")[0] + ".hef")
    with open(hef_file, "wb") as f:
        f.write(hef_data)
        print(f"HEF file saved to: {hef_file}")

@mgreiner79
I see it now, I should have paid more attention. I think I just got so excited that I was finally going to be able to convert my model that I jumped in too early.
Thanks