Custom providers
Connect new backends and devices to existing toolchains.
You don’t always need to create a new toolchain from scratch. If the existing compiler, profiler, or invoker already handles the model format you need, you can register a custom provider that adds support for running on a new backend or device type.
Overview
A provider is a function registered on an existing component that
handles execution for a specific provider type. For example, the built-in ONNXRuntimeCompiler has providers for qai-hub and embedl-onnxruntime.
You can register additional providers for your own backends.
This is useful when you:
- Have a custom compilation or profiling tool that works with a supported model format (TFLite, ONNX Runtime, TensorRT).
- Want to connect existing toolchains to a new cloud service or device type.
- Need to customize how models are transferred or executed on your hardware.
Registering a provider on an existing component
Use the @Component.provider decorator on the existing component class:
from embedl_hub.core.compile import ONNXRuntimeCompilerfrom embedl_hub.core import HubContextfrom pathlib import Path@ONNXRuntimeCompiler.provider("my_custom_backend")def _compile_on_my_backend( ctx: HubContext, onnx_path: Path, *, device: str | None = None, input_shape: tuple[int, ...] | None = None, calibration_data=None, calibration_method=None, per_channel: bool = False, quantize_io: bool = False,): """Compile ONNX models using my custom backend.""" # Your compilation logic here ...The provider function's keyword arguments must match the component's run() method exactly. Check the component's signature before
writing your provider.
Creating a device configuration
If your provider needs custom settings (paths, flags, etc.), create a ProviderConfig subclass:
from dataclasses import dataclass, fieldfrom embedl_hub.core.device import ProviderConfig@dataclass(frozen=True)class MyBackendConfig(ProviderConfig): """Configuration for my custom backend.""" backend_path: str = "/usr/local/bin/my-backend" cli_args: tuple[str, ...] = field(default_factory=tuple)Then create devices with this config:
from embedl_hub.core.device import Devicefrom embedl_hub.core.device import SSHCommandRunner, SSHConfigfrom embedl_hub.core.device import DeviceSpecdef get_my_device( host: str, username: str, *, name: str = "main", config: MyBackendConfig = MyBackendConfig(),) -> Device: runner = SSHCommandRunner(SSHConfig(host=host, username=username)) return Device( name=name, runner=runner, spec=DeviceSpec(device_name="My Custom Device"), provider_type="my_custom_backend", provider_config=config, )Accessing the configuration inside a provider
Inside your provider function, retrieve the typed configuration from the device:
@ONNXRuntimeCompiler.provider("my_custom_backend")def _compile_on_my_backend(ctx, onnx_path, *, device=None, **kwargs): dev = ctx.devices[device or "main"] cfg = dev.get_provider_config(MyBackendConfig, ONNXRuntimeCompiler) backend_path = cfg.backend_path if cfg else "/usr/local/bin/my-backend" # Use the runner to execute commands on the device result = dev.runner.run([backend_path, str(onnx_path)]) ...Per-component configuration overrides
You can provide different configurations for different components on the same device. For example, using a different backend path for compilation vs. profiling:
from embedl_hub.core.compile import ONNXRuntimeCompilerfrom embedl_hub.core.profile import ONNXRuntimeProfileroverrides = dict()overrides[ONNXRuntimeProfiler] = MyBackendConfig( backend_path="/usr/bin/profile-tool",)device = Device( name="my-device", runner=runner, spec=DeviceSpec(device_name="My Device"), provider_type="my_custom_backend", provider_config=MyBackendConfig(backend_path="/usr/bin/compile-tool"), provider_config_overrides=overrides,)Provider type strings
Provider types are plain strings. The built-in ProviderType enum provides
well-known values (LOCAL, QAI_HUB, AWS, EMBEDL_ONNXRUNTIME, TRTEXEC), but any string works:
from embedl_hub.core.component import ProviderType# Using a built-in type@MyCompiler.provider(ProviderType.LOCAL)def _local(ctx, model_path, **kwargs): ...# Using a custom string@MyCompiler.provider("my_custom_backend")def _custom(ctx, model_path, **kwargs): ...Example: Adding SSH support to an existing compiler
Here’s a full example that adds a new SSH-based provider to the existing TFLiteCompiler:
from pathlib import Pathfrom embedl_hub.core.compile import TFLiteCompilerfrom embedl_hub.core import HubContext@TFLiteCompiler.provider("my_tflite_device")def _compile_tflite_ssh( ctx: HubContext, onnx_path: Path, *, device: str | None = None, input_shape: tuple[int, ...] | None = None, calibration_data=None, quantize_io: bool = False,): dev = ctx.devices[device or "main"] # Transfer model to device remote_path = "/tmp/" + onnx_path.name dev.runner.put(onnx_path, remote_path) # Run compilation on the device output_remote = "/tmp/" + onnx_path.stem + ".tflite" dev.runner.run(["my-tflite-compiler", remote_path, "-o", output_remote]) # Fetch result local_output = ctx.artifact_dir / (onnx_path.stem + ".tflite") dev.runner.get(output_remote, local_output) # Log and return if ctx.client is not None: ctx.client.log_artifact(onnx_path, name="input") ctx.client.log_artifact(local_output, name="path") from embedl_hub.core.compile import TFLiteCompiledModel return TFLiteCompiledModel.from_current_run(ctx)Next steps
- See custom toolchains to learn how to create entirely new toolchain components.
- See the providers guide for the full reference of built-in providers.