Skip to content

[New Operator] Initial Implementation of AverageBlur - #168

Open
daniellegillai wants to merge 23 commits into
ROCm:developfrom
daniellegillai:op_average
Open

[New Operator] Initial Implementation of AverageBlur#168
daniellegillai wants to merge 23 commits into
ROCm:developfrom
daniellegillai:op_average

Conversation

@daniellegillai

@daniellegillai daniellegillai commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Motivation

Initial implementation of the AverageBlur operator.

Technical Details

Reuses the same Filter2D kernels as Gaussian.
Currently using the separated horizontal and vertical kernels, but benchmarking shows that for 3x3 kernels the 2d version is faster, so may implement different paths.
Also follows the same concurrency control methods as Gaussian.

Test Plan

Tests added for C++ and Python implementations.

Test Result

C++ and Python tests all pass.

Submission Checklist

@daniellegillai
daniellegillai marked this pull request as ready for review July 10, 2026 22:57
@daniellegillai

Copy link
Copy Markdown
Contributor Author

Current design: Use the 2D kernel when the max kernel size the operator is constructed with is 3x3. Otherwise use the linearly separated optimization.

@zacharyvincze zacharyvincze left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few issues come up since this is a stateful operator and has a handful of caveats compared to the usual stateless operators. For now, due to time constraints, it would be better to document the limitations in the operator's header itself and file an issue in the repo to track and come back to a solution to this later on.

Comment thread src/op_average_blur.cpp
}
processAnchor(anchorX, anchorY, kernelWidth, kernelHeight);

std::lock_guard<std::mutex> lock(m_bufferMutex);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential race condition when calling the operator using the GPU then calling the same operator on the CPU afterwards. Because GPU ops are async, the operator() call can release the lock while the GPU is still doing work. If the operator is called on the CPU during previous GPU work, it will pick up the lock (and not synchronize to the hip event, since that is gated on it being a GPU call) and potentially modify the contents of m_hostKernelMem while the GPU is copying.

Should be able to fix this by having both versions synchronize to m_completionEvent rather than making that GPU only. The host-side lock is redundant in this case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

An event synchronize ruins the asynchronous nature of this operator when pipelining, which has been discussed before. But we'll need to come up with a better solution when it comes to these stateful operators later on.

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.

Concluded that host side lock is still needed to synchronize the CPU work in a multithreaded scenario

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.

02008bb: Always synchronizes on the hip event, on both cpu and gpu device types. This way if the operator was previously called on GPU, the CPU call will wait for the GPU asynchronous work to complete before touching the CPU memory (that may still be being copied over to the GPU).

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.

6f4f20f: Documents the serialization of the operator in the header

Comment thread src/op_average_blur.cpp
eDataType, std::array<std::function<void(hipStream_t, const Tensor&, const Tensor&, float*, float*, int, int, int, int, eBorderType, eDeviceType)>, 4>>
funcs =
{
{eDataType::DATA_TYPE_U8, {dispatch_filter_2d_dtype_separable<uchar1, float*>, 0, dispatch_filter_2d_dtype_separable<uchar3, float*>, dispatch_filter_2d_dtype_separable<uchar4, float*>}},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The seperable filter2d kernel launch path requests a certain amount of shared memory that's based on the size of said kernel. Since there is no bounding of the kernel size, it's possible that it may request more LDS than is available on the card..

@zacharyvincze

Copy link
Copy Markdown
Contributor

Another note. Since this follows the same concurrency logic as Gaussian (and has issues that went uncaught in my initial review of it), the same rules apply and we'll need to document the limitations/file an issue for that operator as well.

@daniellegillai

daniellegillai commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Issue #182 made about the stateful / serialization constraint

@daniellegillai

Copy link
Copy Markdown
Contributor Author

PR #183 applies the synchronization fix to the Gaussian operator too

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.

2 participants