How to perftest a model
For each model running inside Firefox, we want to determine its performance in terms of speed and memory usage and track it over time.
To do so, we use the Perfherder infrastructure to gather the performance metrics.
Adding a new performance test is done in two steps: 1. making it work locally 2. add it in perfherder
Run locally
To test the performance of a model, you can add in the tests/browser a new file
with the following structure and adapt it to your needs:
"use strict";
// unfortunately we have to write a full static structure here
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1930955
const perfMetadata = {
owner: "GenAI Team",
name: "ML Test Model",
description: "Template test for latency for ml models",
options: {
default: {
perfherder: true,
perfherder_metrics: [
{ name: "pipeline-ready-latency", unit: "ms", shouldAlert: true },
{ name: "initialization-latency", unit: "ms", shouldAlert: true },
{ name: "model-run-latency", unit: "ms", shouldAlert: true },
{ name: "pipeline-ready-memory", unit: "MB", shouldAlert: true },
{ name: "initialization-memory", unit: "MB", shouldAlert: true },
{ name: "model-run-memory", unit: "MB", shouldAlert: true },
{ name: "total-memory-usage", unit: "MB", shouldAlert: true },
],
verbose: true,
manifest: "perftest.toml",
manifest_flavor: "browser-chrome",
try_platform: ["linux", "mac", "win"],
},
},
};
requestLongerTimeout(10);
add_task(async function test_ml_generic_pipeline() {
const options = {
taskName: "feature-extraction",
modelId: "Xenova/all-MiniLM-L6-v2",
modelHubUrlTemplate: "{model}/{revision}",
modelRevision: "main",
};
const request = { args: ["The quick brown fox jumps over the lazy dog."] };
await runMLPerfTest({ name: "example", options, request });
});
Then add the file in perftest.toml and rebuild with ./mach build.
The test downloads models it uses from the local disk, so you need to prepare them.
We provide a script to automate this.
$ mach python toolkit/components/ml/tests/tools/create_local_hub.py --list-models
Available git-based models from the YAML:
- xenova-all-minilm-l6-v2 -> path-prefix: onnx-models/Xenova/all-MiniLM-L6-v2/main/
- mozilla-ner -> path-prefix: onnx-models/Mozilla/distilbert-uncased-NER-LoRA/main/
- mozilla-intent -> path-prefix: onnx-models/Mozilla/mobilebert-uncased-finetuned-LoRA-intent-classifier/main/
- mozilla-autofill -> path-prefix: onnx-models/Mozilla/tinybert-uncased-autofill/main/
- mozilla-smart-tab-topic -> path-prefix: onnx-models/Mozilla/smart-tab-topic/main/
- mozilla-smart-tab-emb -> path-prefix: onnx-models/Mozilla/smart-tab-embedding/main/
(Use `--model <key>` to clone one of these repositories.)
You can then use --model to download locally models, by specifying the local
MOZ_ML_LOCAL_DIR directory, via the env var or command line argument :
$ mach python toolkit/components/ml/tests/tools/create_local_hub.py --model mozilla-smart-tab-emb --fetches-dir ~/ml-fetches
Found existing file /Users/tarekziade/Dev/fetches/ort-wasm-simd-threaded.jsep.wasm, verifying checksum...
Existing file's checksum matches. Skipping download.
Updated Git hooks.
Git LFS initialized.
Cloning https://huggingface.co/Mozilla/smart-tab-embedding into '/Users/tarekziade/Dev/fetches/onnx-models/Mozilla/smart-tab-embedding/main...
Cloning in '/Users/tarekziade/Dev/fetches/onnx-models/Mozilla/smart-tab-embedding/main'...
Checked out revision '2278e76f67ada584cfd3149fd2661dad03674e4d' in '/Users/tarekziade/Dev/fetches/onnx-models/Mozilla/smart-tab-embedding/main'.
Once done, you should then be able to run it locally with :
MOZ_ML_LOCAL_DIR=~/ml-fetches ./mach perftest toolkit/components/ml/tests/browser/browser_ml_engine_perf.js --mochitest-extra-args=headless
Notice that MOZ_ML_LOCAL_DIR is an absolute path to the root directory.
Add in the CI
To add the test in the CI you need to add an entry in
taskcluster/kinds/perftest/linux.ymltaskcluster/kinds/perftest/windows11-24h2.ymltaskcluster/kinds/perftest/macosx.yml
With a unique name that starts with ml-perf.
A test that measures a feature requesting best-onnx gets two entries per
platform file, one per backend: <name>-wasm holding the full definition (and a
YAML anchor), and <name>-native merging it with <<: to override the
treeherder symbol and set MOZ_ML_BACKENDS: onnx-native. Copy an existing pair
such as ml-perf-autofill-wasm / ml-perf-autofill-native.
Example for Linux:
ml-perf-wasm: &ml-perf
fetches:
fetch:
- ort.jsep.wasm
- xenova-all-minilm-l6-v2
description: Run ML Models Perf Tests (wasm onnx backend)
treeherder:
symbol: perftest(ml-perf-w)
tier: 2
attributes:
batch: false
cron: false
run-on-projects: [autoland, mozilla-central]
worker:
env:
MOZ_ML_BACKENDS: onnx
run:
clone-with: hg
command: >-
mkdir -p $MOZ_FETCHES_DIR/../artifacts &&
cd $MOZ_FETCHES_DIR &&
python3 python/mozperftest/mozperftest/runner.py
--mochitest-binary ${MOZ_FETCHES_DIR}/firefox/firefox-bin
--flavor mochitest
--output $MOZ_FETCHES_DIR/../artifacts
--hooks toolkit/components/ml/tests/tools/hooks_local_hub.py
toolkit/components/ml/tests/browser/browser_ml_engine_perf.js
ml-perf-native:
<<: *ml-perf
description: Run ML Models Perf Tests (native onnxruntime backend)
treeherder:
symbol: perftest(ml-perf-n)
tier: 2
worker:
env:
MOZ_ML_BACKENDS: onnx-native
A feature that pins one backend instead of requesting best-onnx gets a single
entry, without the suffix or the anchor, pinning MOZ_ML_BACKENDS to the backend
it actually ships. ml-perf-pdfjs-alt-text is one: PDF.js asks for
onnx-native with no fallback, so a wasm job would measure a configuration no
user ever runs.
You also need to add the models your test uses (like the ones you’ve downloaded locally) by adding entries in
taskcluster/kinds/fetch/onnxruntime-web-fetch.yaml and adapting the fetches section.
Once this is done, try it out with:
./mach try perf --single-run --full --artifact
You should then see the results in treeherder.
Backends: measuring native ONNX and WASM
Most features request the best-onnx backend, which resolves to onnx-native
where the platform bundles libonnxruntime and silently falls back to the WASM
onnx backend everywhere else. Measuring only one of the two therefore tells
you nothing about what a meaningful share of the fleet actually runs.
In CI, every ONNX perf task exists twice: <task>-native and <task>-wasm,
each pinning one backend through the MOZ_ML_BACKENDS environment variable in
taskcluster/kinds/perftest/*.yml. One job measures one backend; a backend
that cannot run fails its own job without costing the other backend’s
numbers.
Metric names carry the backend tag as a suffix, so each backend is its own perfherder series and the two can be compared directly:
SMART-TAB-TOPIC-model-run-latency-NATIVE
SMART-TAB-TOPIC-model-run-latency-WASM
Locally, without MOZ_ML_BACKENDS, runMLPerfTest() probes the runtime once
and measures the most preferred backend that can run: native where the runtime
loads, the wasm fallback otherwise. To measure both in one session, or to
reproduce a specific CI job, set the variable yourself:
MOZ_ML_BACKENDS=onnx-native,onnx ./mach perftest <test.js> ...
Do not set backend in your PipelineOptions — runMLPerfTest() overrides it per
iteration. For a test whose backend is genuinely fixed (llama.cpp, OpenAI,
static embeddings), pin it instead:
await runMLPerfTest({ name, options, request, backends: ["llama.cpp"] });
MOZ_ML_BACKENDS overrides even a pinned list: the CI task name is the
contract for what its job measures.
Tests that drive a feature rather than a pipeline
runMLPerfTest() owns engine creation, so it only fits a test that measures one
pipeline. A test that drives a whole feature (MLSuggest, semantic history
search, smart tab clustering) calls runMLPerfTestForEachBackend() directly,
passes backend down into whatever options it builds, and appends tag to its
own metric names:
add_task(async function test_my_feature() {
await runMLPerfTestForEachBackend({ name: "MY-FEATURE", run: measureMyFeature });
});
async function measureMyFeature({ backend, tag }) {
MyFeature.OPTIONS = { ...MY_OPTIONS, backend };
// ...
reportMetrics({ [`MY-FEATURE-model-run-latency-${tag}`]: latencies });
}
Watch out for features that carry their own native→wasm fallback (MLSuggest
does): pin the fallback options to the same backend, otherwise a native failure
quietly reports wasm numbers under the NATIVE tag.
The tag goes at the end of the name, never between the feature and the
metric. mozperftest keeps a subtest only if a declared perfherder_metrics
name appears in it as a substring: AUTOFILL-model-run-latency still matches
AUTOFILL-model-run-latency-NATIVE, but would not match a name with the tag
in the middle.
When a backend cannot run a model
The job fails. mozperftest stops before emitting PERFHERDER_DATA when a
test fails, and since the job only measures that one backend, no other
numbers are lost.
If a backend can never run a given model, do not schedule that variant: drop it from the kind.yml with a comment saying why, so the hole reads as deliberate. An undeclared hole is treated as a bug.
Two known native gaps to expect if you add a test using them: the native
backend cannot load models that ship their weights as external data
(use_external_data_format, i.e. a .onnx_data file — it has no way to hand
those bytes to the runtime), and text-to-speech fails with
Unsupported device: "wasm" because the vocoder is loaded with the WASM
default device.
To pin a whole run to one backend, for example to reproduce a WASM-only platform on a machine that has the native runtime:
MOZ_ML_BACKENDS=onnx ./mach perftest <test.js> \
--hooks toolkit/components/ml/tests/tools/hooks_local_hub.py \
--mochitest-extra-args headless
Getting numbers out of a try push
./mach try preset ml-perf
Each ONNX test runs as two jobs, -native and -wasm, and the tagged
subtest names make the two series comparable in perfherder. A job that ran
green without reporting metrics measured nothing, so check that every
scheduled job produced data before drawing conclusions from a push.