Skip to content

Add non-temporal loads/stores to Halide #9206

Description

@alexreinking

Problem statement

Halide's memcpy performance is quite poor: performance_memcpy allows a 3x throughput gap that still sometimes trips.

I wrote an experimental branch (alexreinking/non-temporal-hack) that asks LLVM to attach non-temporal load/store hints globally when HL_EXPERIMENTAL_NONTEMPORAL_STORES=1 and/or HL_EXPERIMENTAL_NONTEMPORAL_LOADS=1 are set.

On ARM, I see the following:

areinking@Mac non-temporal-hack % HL_EXPERIMENTAL_NONTEMPORAL_LOADS=1 ./build/macOS/test/performance/performance_memcpy
system memcpy: 70.61 GB/s
halide memcpy: 69.44 GB/s
Success!
areinking@Mac non-temporal-hack % HL_EXPERIMENTAL_NONTEMPORAL_LOADS=0 ./build/macOS/test/performance/performance_memcpy
system memcpy: 78.58 GB/s
halide memcpy: 55.63 GB/s
Success!

The benchmarks are pretty noisy, but I can't get a comparable results without the non-temporal loads. On my machine (Apple M3), enabling NT stores does not improve performance, likely because it's writing a whole cache-line anyway, and the cache can skip RFO.

Proposed solution

Add two new scheduling directives:

  1. Func::stream_loads() (also applies to ImageParam) that ensures that loads from this Func are done non-temporally. (e.g. ldnp or movntdqa).
  2. Func::stream_stores() that ensures that stores to the buffer backing this Func are done non-temporally (e.g. stnp or movntiq)

On ARM, there are no coherency issues. On x86, non-temporal stores must be followed by sfence before they can be safely read; this will require additional analysis. I haven't looked into other backends (GPUs, RISC-V, Hexagon, etc.).

The proposed stream_loads directive interacts especially nicely with the .in() directive: in a residual-network-like computation, you re-load an old buffer, streaming it once just for computation. Loads in the network would be temporal; loads during the residual application would be non-temporal ("streaming").

Non-temporal loads are also meaningful on some platforms for matrix-vector operations, like ARM. They are meaningless on others: x86 ignores non-temporal hints on write-back memory (i.e. everything Halide has cared about to date).

Additional context

This issue was brought to my attention by @mcourteaux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions