Skip to content

Add Halidoscope for interactive trace and profiling visualization. - #9356

Open
parkerziegler wants to merge 92 commits into
mainfrom
parkerziegler/halidoscope
Open

parkerziegler wants to merge 92 commits into
mainfrom
parkerziegler/halidoscope

Conversation

@parkerziegler

@parkerziegler parkerziegler commented Aug 17, 2026 •

Copy link
Copy Markdown

This PR introduces Halidoscope, a new tool for interactively visualizing Halide traces and performance information captured by the Halide profiler.

Trace View Profile View
A screenshot of the Trace view in the Halidoscope UI. A screenshot of the Profile view in the Halidoscope UI.

Motivation

Understanding the performance characteristics of a Halide program (and, specifically, its schedule) can be tricky for experts and downright daunting for newer Halide developers. Today, the primary developer tooling we have to aid folks in understanding their Halide programs is HalideTraceViz. While HalideTraceViz provides good visual intuition for how a schedule executes, it doesn't provide much insight into how effectively a given schedule balances trade-offs in cache locality, redundant recomputation, and parallelism. Halidoscope attempts to fill this gap by:

  • Interactively visualizing key metrics (called "render modes") like per-Func Store / Load Frequency, Redundant Stores, Reuse Distance, and Thread Coverage
  • Integrating directly with the Halide profiler
  • Allowing users to see myriad useful bits of information, such as pipeline dataflow, NaNs / Infs, and real-time liveness information (e.g., Func buffer liveness, active producer-consumer relationships)

High-Level API

Halidoscope ships as both a GUI and CLI that can be invoked through a new Pipeline::halidoscope member function or directly from the command line.

Calling Halidoscope via Pipeline::halidoscope

Halide developers can launch Halidoscope via a call to Pipeline::halidoscope (either in C++ or via the Python bindings) like so:

// Normal algorithm definition and scheduling code.

// Create the pipeline.
Pipeline pipeline(output);

// Explicitly passing desired output buffer dimensions for allocation,
// which happen to match the input.
std::vector<int32_t> sizes = {input.width(), input.height(), input.channels()};

// Call .halidoscope!
 pipeline.halidoscope(sizes);

Under the hood, the Pipeline::halidoscope member function will:

  1. Serialize the pipeline (to obtain a fresh copy)
  2. Deserialize it and execute it once with tracing enabled
  3. Deserialize it a second time and execute it with profiling enabled

Users can specify a HalidoscopeOptions struct to control how Halidoscope executes with the following fields.

Field Type Description
halidoscope_path std::optional<std::string> The path to the Halidoscope binary on disk. By default, the call will look for Halidoscope on the user's $PATH and error if not found.
halidoscope_output_dir std::optional<std::string> (Optional.) A path to a non-volatile directory for storing Halidoscope-generated trace binaries and profiler output. By default, Halidoscope will write recorded .hltrace and profile JSON files to a temporary directory that is destroyed on process exit.
halidoscope_profile_runs std::optional<int> The number of profiling runs for the Halide profiler to execute on the pipeline. Defaults to 1. Users can opt out of profiling altogether by specifying 0.

Calling Halidoscope from the command line

Users can also call launch Halidoscope directly from the command line, pointing it at a pre-recorded Halide trace binary and (optionally) a profile JSON file.

halidoscope --trace <path/to/recorded.hltrace> [--profile <path/to/recorded-profile.json>]

The Halidoscope CLI also comes with several commands (documented in the README.md and printable via halidoscope --help) that can provide useful, high-level information on a trace (for both humans and agents).

  • halidoscope list — List the Funcs in a trace, along with their dimensionality.
  • halidoscope stats — Print statistics (minimum/maximum coordinates, minimum/maximum value, maximum store/load counts, and thread count) for one or all Funcs in a trace.
  • halidoscope dot — Generate a Graphviz DOT representation of the pipeline's dataflow graph.
  • halidoscope snapshot — Snapshot a Func's values at a given packet index for a given render mode, writing the underlying data to a JSON file.

Stack and High-Level Architecture

Halidoscope is a completely standalone Tauri application in tools/halidoscope — it has no dependency on the Halide runtime.

Warning

The only slight exception to this is our use of bindgen to derive Rust bindings for halide_trace_packet_t, which ensures that our Rust packet parser always reads the correct offsets for Halide's packet format. bindgen is run at build-time against a user's local version of the Halide source. However, note that there is no actual runtime FFI between Halidoscope's Rust parser and the Halide runtime or C ABI.

Backend

The Halidoscope backend is implemented in Rust and contains the following main modules:

  • trace.rs — Responsible for parsing a .hltrace file and accumulating trace-level statistics and metadata.
  • render.rs — Computation supporting Halidoscope's render modes, where trace data is converted to RGBA Vec<u8> buffers (for display) and binary payloads for histogram data and NaN / Inf overlays
  • commands.rs — Sets up the Tauri IPC API for communication with the frontend.
  • cli.rs — Handles calls to the Halidoscope CLI.

Communication is done entirely over IPC using Tauri commands.

Frontend

The frontend is implemented in TypeScript and React, using Jotai for state management and Tailwind for styling and CSS management. We use Vite as our frontend build system. This is about as standard a frontend stack as you'll find in modern web dev.

By default, Tauri uses a system's native WebView for rendering, which allows us to avoid bundling a full browser engine with the Halidoscope binary. Today, the Halidoscope binary weighs in at 10.5 MB uncompressed and 3.7 MB gzipped.

Breaking changes

No changes here are breaking; all changes are purely additive. Still, it is likely worth reviewing the additions to the Pipeline API a bit more carefully to confirm.

Checklist

  • Tests added or updated (not required for docs, CI config, or typo fixes)
  • Documentation updated (if public API changed)
  • Python bindings updated (if public API changed)
  • Benchmarks are included here if the change is intended to affect performance.
  • Commits include AI attribution where applicable (see Code of Conduct)

@codecov

codecov Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.02469% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.14%. Comparing base (c623f16) to head (61619d3).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/Halidoscope.cpp 77.27% 20 Missing and 15 partials ⚠️
src/Generator.h 20.00% 8 Missing ⚠️
src/BoundsInference.cpp 89.09% 2 Missing and 4 partials ⚠️
src/Pipeline.cpp 89.47% 0 Missing and 2 partials ⚠️
src/Target.cpp 60.00% 0 Missing and 2 partials ⚠️
src/VectorizeLoops.cpp 0.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9356      +/-   ##
==========================================
+ Coverage   71.03%   71.14%   +0.10%     
==========================================
  Files         262      263       +1     
  Lines       81073    81373     +300     
  Branches    19768    19825      +57     
==========================================
+ Hits        57593    57892     +299     
+ Misses      17512    17464      -48     
- Partials     5968     6017      +49     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alexreinking

Copy link
Copy Markdown
Member

@parkerziegler — some quick-to-fix clang-tidy diagnostics:

[ 12/389][62.1s] /home/runner/work/Halide/Halide/tools/clang-tidy-filter.sh -p=/tmp/tmp.vaf2F3qPO4 -quiet /home/runner/work/Halide/Halide/src/Pipeline.cpp
/home/runner/work/Halide/Halide/src/Pipeline.cpp:997:9: error: use 'std::scoped_lock' instead of 'std::lock_guard' [modernize-use-scoped-lock,-warnings-as-errors]
  997 |         std::lock_guard<std::mutex> lock(halidoscope_trace_mutex);
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~
      |         std::scoped_lock
/home/runner/work/Halide/Halide/src/Pipeline.cpp:1191:26: error: use emplace_back instead of push_back [hicpp-use-emplace,modernize-use-emplace,-warnings-as-errors]
 1191 |         halidoscope_args.push_back("--profile");
      |                          ^~~~~~~~~~
      |                          emplace_back(
/home/runner/work/Halide/Halide/src/Pipeline.cpp:1217:75: error: the parameter 'options' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param,-warnings-as-errors]
 1217 | void Pipeline::halidoscope(std::vector<int32_t> sizes, HalidoscopeOptions options, const Target &target) {
      |                                                                           ^
      |                                                        const             &
/home/runner/work/Halide/Halide/src/Pipeline.cpp:1221:70: error: the parameter 'options' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param,-warnings-as-errors]
 1221 | void Pipeline::halidoscope(RealizationArg output, HalidoscopeOptions options, const Target &target) {
      |                                                                      ^
      |                                                   const             &
Error: use 'std%3A%3Ascoped_lock' instead of 'std%3A%3Alock_guard'
Error: use emplace_back instead of push_back
Error: the parameter 'options' is copied for each invocation but only used as a const reference; consider making it a const reference
Error: the parameter 'options' is copied for each invocation but only used as a const reference; consider making it a const reference

@parkerziegler
parkerziegler force-pushed the parkerziegler/halidoscope branch 3 times, most recently from 6b30e1b to 1061c18 Compare August 20, 2026 19:22
@alexreinking alexreinking self-assigned this Aug 24, 2026
@alexreinking
alexreinking force-pushed the parkerziegler/halidoscope branch from e85bf54 to df45af3 Compare August 24, 2026 19:22
@alexreinking
alexreinking marked this pull request as ready for review August 24, 2026 19:22
alexreinking and others added 21 commits September 3, 2026 12:57
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… Funcs on playback.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…th mode-specific renderers.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…the notion of producer-consumer in the original Halide paper.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… there are no intervening loads.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@alexreinking
alexreinking force-pushed the parkerziegler/halidoscope branch from df45af3 to 8e4ba63 Compare September 3, 2026 16:57
@alexreinking
alexreinking force-pushed the parkerziegler/halidoscope branch from 8e4ba63 to e18cbcd Compare September 29, 2026 19:23
@alexreinking

Copy link
Copy Markdown
Member

Needs a skip for wasm with no threads

abadams and others added 20 commits October 5, 2026 13:26
Tracing emitted the begin produce/consume event in a LetStmt outside the
ProducerConsumer node, but the end event at the end of the node's body.
Later passes wrap the body of these nodes in a condition: skip stages
guards producers of conditionally-used Funcs, and sliding window guards
consumers during warm-up iterations. When the condition was false, the
begin event fired with no matching end. Both events now go inside the
body, so a skipped body emits neither.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a halide_trace_bounds_required trace event, enabled per Func with
Func::trace_bounds_required() or for all Funcs with the
trace_bounds_required target feature (included in trace_all). At each
loop level where bounds inference defines the region of a Func required
by its consumers, an event reports that region using the same
.s0.<arg>.min/max symbols that bounds inference defines. Its parent is
the innermost enclosing traced event.

This lets trace consumers distinguish values that matter from values
computed outside the required region (e.g. due to TailStrategy::RoundUp
in a consumer), which may be computed from uninitialized memory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The profiler report turned colors off while printing the text performance
warnings, but turned them back on before rendering the same warnings into
the JSON output. On a color terminal, warnings containing SI-suffixed
counts (e.g. "2097K") then carried raw escape codes into JSON strings,
which made the JSON invalid. Keep colors off until the JSON warnings have
been rendered too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On Windows the profiler's sampling thread can sleep longer than a short
pipeline run, so the run gets no samples and the time-gated gather
warning never fires. Double the work per run until the warning appears.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lets the host choose the fd that JIT-compiled pipelines write binary trace
events to, applying to the shared runtime now and whenever it is created.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lidoscope

- Trace through the runtime's binary trace writer into a zstd-compressed
  file, with a configurable size limit, and trace bounds_required events.
- Write the profile via halide_profiler_set_json_output and accumulate it
  over the profiling runs with a ProfilerScope.
- Rename HalidoscopeOptions fields (path, output_dir, profile_runs) and add
  trace_file_size_limit.
- Add JITUserContext overloads of Pipeline::halidoscope.
- Add Generator::halidoscope, which takes the same arguments as the
  Callable returned by compile_to_callable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Read zstd-compressed traces.
- Show each realize/produce/consume node as a box covering the region it
  spans, using bounds_required events to exclude overcomputed values from
  value ranges.
- Render inputs from their loads.
- Add black/white point controls, slice selection for Funcs with more than
  two dims, and a hover probe showing the value under the cursor.
- Show cumulative stats and warnings in the profile table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lets the host choose the fd that JIT-compiled pipelines write binary trace
events to, applying to the shared runtime now and whenever it is created.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Key store/load/redundant/reuse/thread metrics on the full coordinate of
  multi-dimensional Funcs instead of (x, y, c), which aliased higher dims
  and made e.g. redundant-store views data-dependent. Metric views get the
  same slice sliders as value views.
- Plot mode as SVG with zoom-scaled height and fixed on-screen stroke.
- On-canvas slice sliders; RGB default only for extent 3 or 4.
- Profile tab revamp: treemap, table, cross-tab Func context menu (now
  including render modes), profiler fields in the trace Funcs panel.
- Deselect Func on background click; don't scroll on warning click.
- Pause playback when leaving the trace tab; default to per-Func
  normalization; render-mode changes take effect during playback.
- Make assembly text selectable in the stmt HTML.
- Stmt tab and Generator::halidoscope plumbing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the zstd dependency with a small dependency-free codec for trace
packets (tools/halide_trace_compression.h), shared by libHalide's trace
writer and the Halidoscope GUI (via a C shim compiled with cc). Each word
of a packet is predicted from the previous packet with the same size and
parent, from earlier words in the packet, or from the last value seen at
the same Func and coordinates, and the residual is range coded. It
matches zstd -19 on bilateral_grid traces and compresses ~10-17x.

The trace is split into independent 64 MB chunks whose headers record
their decoded size, so chunks decode in parallel directly into one
preallocated buffer. The runtime now zeroes packet padding so it
compresses well.

The GUI no longer copies trace data after decompression: packets are
views into the decompressed words, parsed while later chunks are still
decoding. Func names are interned, thread and producer lookups are
inherited from the parent at parse time instead of walking parent chains
afterwards, and per-Func access statistics are computed in parallel.
Loading a 651 MB trace drops from 4.0s to 1.1s (raw) and 4.75s to 1.6s
(compressed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The thread palette was Set3, which has 12 colors, so elements written by
any later thread rendered black, like untouched elements. Use Set3 for up
to 12 threads and evenly spaced hues beyond that, computed in Rust and
sent to the frontend so the legend matches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject chunks whose decoded size isn't a multiple of 4 or can't hold
their packet count, which could misplace later chunks past the end of
the output buffer. Reject decoded packet sizes that aren't a multiple of
4. In the GUI, allocate the decompressed trace fallibly and lazily zeroed,
so a corrupt size claim reports an error instead of aborting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the unused 64x64.png and strip the 512px and 1024px images from
icon.icns, which accounted for most of the icon bytes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@abadams
abadams force-pushed the parkerziegler/halidoscope branch from e18cbcd to bcb08a3 Compare October 7, 2026 20:15
halide-ci Bot and others added 4 commits October 7, 2026 20:20
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The WebAssembly JIT supports neither the profiler (without wasm_threads)
nor tracing to a file, so skip correctness_halidoscope there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants