Skip to content

Makes hl.Buffer <--> Python Buffer Protocol axis convention consistent throughout#8953

Open
jiawen wants to merge 4 commits into
mainfrom
jiawen-hl_buffer
Open

Makes hl.Buffer <--> Python Buffer Protocol axis convention consistent throughout#8953
jiawen wants to merge 4 commits into
mainfrom
jiawen-hl_buffer

Conversation

@jiawen

@jiawen jiawen commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Halide interacts with Python in three place:

  • Python bindings
  • PyCallablle (compile_to_callable() via Python bindings)
  • Python extension

This PR changes our axis ordering conventions to be consistent throughout:

  • Constructing a Python buffer object from an hl.Buffer reverses axes.
  • Constructing an hl.Buffer from any Python buffer object reverses axes.
    • Note that an hl.Buffer is also a Python buffer object. E.g., you can use my_buffer[idx0, idx1, idx2] to retrieve an element. Constructing an hl.Buffer from another hl.Buffer does not reverse axes because that would be surprising.
    • hl.Buffer(my_array, "my_buffer_name", reverse_axes=False) is explicitly supported.
  • Calling a PyCallable with:
    • A hl.Buffer does not reverse axes (it's considered a Halide Buffer, not a Python buffer object)
    • A Python buffer object does reverse axes.
  • Calling a pipeline function in a Python extension generated by a generator the same conventions as a PyCallable.

Because of the new consistent conventions, we now permit:

  • Non-contiguous views. Previously, Python-side C-contiguous buffers were reversed and F-contiguous buffers were not. Now we support arbitrary views.

  • This includes negatives strides.

  • hl.Buffer also gained an explicit .numpy_view() method that returns a np.array with an explicit reverse_axes argument (defaulting to True).

- numpy_view() with no arguments:
  - Always tries to returns a C-contiguous view of the buffer if possible.
  - If Halide Buffer is stored in the default order, will reverse axes.
  - If Halide Buffer is stored in the reverse order, will preserve axes.
- numpy_view(reverse_axes: Bool):
  - Requires an explicit reverse_axes argument to be passed in.
  - It will do what was requested, and supports non-contiguous buffers.
@jiawen

jiawen commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author

@alexreinking PTAL at the design. I left some ELEPHANTs as points for discussion.

.def_buffer([](Buffer<> &b) -> py::buffer_info {
return to_buffer_info(b, /*reverse_axes*/ true);

// ELEPHANT: this always reverses axes, which might be surprising?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to break the interface here.

The smoothest path would be to permit this, with automatic maybe-reverse-axes-depending on whether the Buffer is contiguous, only when the Buffer is contiguous. This is analogous to the numpy_view() with no args below.

This means breaking automatic conversion from cropped Buffers to the buffer protocol - but I imagine that's not used very much. Clients can use numpy_view(reverse_axes: Bool) instead.

WDTY?

// - It is possible for a Buffer to be both C and F contiguous (e.g., a scalar or a
// 1D vector), or for a Buffer to be neither (e.g., storage_order=[1, 0, 2] for a 3D
// buffer).
// ELEPHANT: maybe I should just call it [densest_first, densest_last]. But that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To discuss how to name this.


// This allows us to use any buffer-like Python entity to create a Buffer<>
// (most notably, an ndarray)
.def(py::init_alias<py::buffer, const std::string &, bool>(), py::arg("buffer"), py::arg("name") = "", py::arg("reverse_axes") = true)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to make this more explicit as well. Have two versions:

  • A version without reverse_axes that requires a contiguous buffer. It will auto-reverse if contiguous.
  • A version with explicit reverse_axes that accepts anything.

@jiawen

jiawen commented Feb 25, 2026

Copy link
Copy Markdown
Contributor Author

Bump

@jiawen

jiawen commented Mar 19, 2026

Copy link
Copy Markdown
Contributor Author

@abadams @alexreinking Bump, part deux

@abadams

abadams commented Apr 13, 2026

Copy link
Copy Markdown
Member

Alex and I finally spent some time talking about this. We think that conversions back and forth between numpy/python buffer protocol and Halide buffers should just always reverse the axes, and the automatic behavior in the python_extension generator output is bad. It means, for example, that calling a Halide-generated function on a numpy array computes the same thing as calling it on a copy-free transpose of the array, which seems surprising. The semantics should depend on the passed object as indexed, not as stored in memory. Having a numpy_view that doesn't reverse the axes given an explicit argument is fine too, but we think everything should default to axis reversing.

@jiawen

jiawen commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

Alex and I finally spent some time talking about this. We think that conversions back and forth between numpy/python buffer protocol and Halide buffers should just always reverse the axes, and the automatic behavior in the python_extension generator output is bad. It means, for example, that calling a Halide-generated function on a numpy array computes the same thing as calling it on a copy-free transpose of the array, which seems surprising. The semantics should depend on the passed object as indexed, not as stored in memory. Having a numpy_view that doesn't reverse the axes given an explicit argument is fine too, but we think everything should default to axis reversing.

This makes sense to me. When I find some cycles, I'll go over the API surfaces with a fine-toothed comb to make sure I catch all the "loops". Off the top of my head, the interactions are:

  • Construct hl.Buffer from a buffer-like object.
  • Indexing into hl.Buffer.
  • Getting a buffer object or explicit numpy view from a hl.Buffer.
  • AOT generator emitted .py.cpp accepting either hl.Buffer or buffer protocol objects.
  • generator.compile_to_callable accepting either hl.Buffer or buffer protocol objects.

I'm almost tempted to make things more restrictive:

  • Make AOT and JIT compiled callable only accept hl.Buffer.
  • Remove indexing from hl.Buffer. Instead, force you to call .numpy_view(reverse_axes=True) to manipulate it.

@alexreinking

Copy link
Copy Markdown
Member
  • Make AOT and JIT compiled callable only accept hl.Buffer.
  • Remove indexing from hl.Buffer. Instead, force you to call .numpy_view(reverse_axes=True) to manipulate it.

I don't think either of these are necessary. Just being consistent about flipping logical axes between each platform's native buffer representation should be enough.

@jiawen jiawen changed the title Python bindings: adds numpy_view() to hl.Buffer. Makes hl.Buffer <--> Python Buffer Protocol axis convention consistent throughout Jun 24, 2026
@jiawen jiawen marked this pull request as ready for review June 24, 2026 18:32
@jiawen

jiawen commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@alexreinking @abadams PTAL. I think this reflects the design you had in mind. I like it - the story is consistent with few surprises.

@alexreinking alexreinking self-assigned this Jun 24, 2026
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@2fad88f). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8953   +/-   ##
=======================================
  Coverage        ?   69.37%           
=======================================
  Files           ?      254           
  Lines           ?    78274           
  Branches        ?    18729           
=======================================
  Hits            ?    54301           
  Misses          ?    18479           
  Partials        ?     5494           

☔ 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.

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