Adding a new MSL kernel¶
Step-by-step walkthrough for porting a new CUDA kernel from upstream
upstream/src/aliceVision/depthMap/cuda/ into our Metal port. The pattern
below is what every kernel from S3 (eig33) through S31 (custom patch
pattern, volume Gaussians, adaptive-P2) followed.
Mental model¶
flowchart LR
SRC[upstream CUDA<br/>.cu / .cuh]
MSL[src/shaders/depth_map/<br/>foo.metal]
HOST[src/depth_map_metal/<br/>FooOps.cpp + .hpp]
TEST[tests/test_foo.cpp]
REF[host-side reference impl<br/>typically inside the test]
SRC -- "transliterate" --> MSL
MSL -- "loaded by" --> HOST
HOST -- "dispatches" --> MSL
TEST -- "drives" --> HOST
TEST -- "compares against" --> REF
REF -- "from" --> SRC
Steps (concrete order)¶
1. Choose a CPU reference¶
For every kernel we have ported, the test file in tests/test_<name>.cpp
contains a host-side reference implementation that mirrors the upstream
algorithm in plain C++. The reference is FP64 (so we can quantify FP32
drift) and uses Eigen where the algorithm naturally calls for it.
This is non-negotiable. You cannot validate a kernel against "the CUDA
version produced X on Linux" because you don't have a Linux CUDA box.
You validate against a CPU reference in the same TU.
2. Write the MSL kernel¶
Land it in src/shaders/depth_map/<name>.metal (one kernel per file is the
norm; multi-kernel files like volume_kernels.metal are reserved for tightly
coupled variants).
Conventions:
- Kernel symbol name
av_<name>— visible fromDevice::make_pipeline("av_<name>"). [[buffer(0)]]/[[buffer(1)]]/ etc. ordering must match the hostset_buffer()calls in the matchingOps.cpp.- POD parameter structs go in
[[buffer(N)]]withpacked_float3for any 3-vector that the host C++ also defines. Plainfloat3in MSL is 16-byte-aligned;packed_float3is 12 bytes — matters for layout compatibility (DeviceCameraParamsis 276 B,DevicePatchPatternis 868 B, validated bystatic_assert(sizeof(…))). - FP32 only. Apple GPUs have no FP64. Half (
half) is allowed for storage (Refine cost volume), but arithmetic is FP32.
3. Add a shared .h if other .metal translation units will reuse it¶
Headers like eig33.h, Patch.h, color.h, SimStat.h, matrix.h,
volume_helpers.h, DevicePatchPattern.h, operators.h are included both
from .metal translation units and from the host C++ that builds the
matching CPU reference. Keep them small and dependency-light.
4. Add the host driver class¶
Land it in src/depth_map_metal/src/<Name>.cpp +
include/av/depth_map/<Name>.hpp. The class:
- Holds an
av::gpu::Pipeline pipeline_(one per kernel variant). - Builds it lazily in the ctor via
device.make_pipeline("av_<name>"). - Exposes a single-purpose
run(...)method that constructs aCommandBuffer, sets the pipeline + buffers + bytes, callsdispatch, and commits.
Two commit modes:
| Mode | When |
|---|---|
commit_and_wait() |
Tests and synchronous code paths. |
commit_async(handler) |
Steady-state pipeline where the next dispatch reads the result. |
Multi-dispatch hot paths (like Volume::optimize) batch all dispatches
onto a single command buffer + encoder and call commit_and_wait() once
at the end (S44 perf lesson: ~0.5 ms/dispatch driver overhead on M4
dominates a kernel that takes <1 ms of real GPU work).
5. Wire up cmake/Metal.cmake¶
The Metal source list lives in src/shaders/CMakeLists.txt under
av_shaders_sources. Add your .metal file there; the build picks it up
into default.metallib.
The host driver belongs in src/depth_map_metal/CMakeLists.txt under
av_depth_map_metal_sources. Add the .cpp.
6. Add the test¶
Land it in tests/test_<name>.cpp and register it in tests/CMakeLists.txt
via av_add_test(<name>). The test must:
- Construct an input set with known structure (synthetic plane, random matrices with fixed seed, …).
- Compute the expected result on CPU (your FP64 reference from step 1).
- Run the GPU kernel via your
<Name>::run(...). - Compare element-wise. Quantify the worst |err| and assert it is below the agreement budget appropriate for the algorithm.
Agreement-budget guidance from prior sessions:
| Kind of arithmetic | Budget |
|---|---|
| FP32-only matrix ops | 1e-5 rel |
| Householder + QL eigenvectors (3×3) | 1e-6 cos deviation |
| NCC with sigmoid invert-and-filter | 5e-2 worst |
Chained sigmoid (optimize_depth_sim_map) |
1e-3 rel sim (FP32-ULP on depth) |
| Clamp-fused thickness | 1e-6 rel (sub-FP32-ULP) |
The chained-sigmoid case is the canonical place where -ffast-math requires
a relaxed budget (S23). See PORTING_NOTES.md §5.
7. Verify on the build¶
cmake --build build
cd build && ctest -R test_<name> --output-on-failure -V
ctest # confirm 37/37 still pass
8. Wire up an adapter forwarder (only if upstream calls it)¶
If upstream's host code (Sgm.cpp, Refine.cpp, etc.) calls into the kernel
via a cuda_<name> function, you need a forwarder in
src/depth_map_metal/src/upstream_adapter.cpp. Audit the parameter
pre-processing against the upstream CUDA call site — this is the S40 bug
class (memory/mental_note.md §8i). Specifically: every * 254.f,
1.f + …, 1.0f / float(…) that the CUDA caller does before the kernel
must be mirrored on our side. The S41 audit (memory/adapter_audit_s41.md)
checks each forwarder against dSV.cu line by line — read it as a template.
Wrap the body in AV_ADAPTER_PROFILE_SCOPE("cuda_<name>") for profiling
(see Performance profiling).
Anti-patterns¶
- Don't silently change the parameter range. If upstream's CUDA caller
scales
maxSimilarity * 254, your adapter must too. Don't infer it from the parameter name in the struct. - Don't use
boolin MSL ↔ host struct layouts. Useint32_t. MSLboolis implementation-defined size; the CUDA/host C++/MSL combination is fragile. Pattern fromDevicePatchPattern::isCircle(S31). - Don't add a multi-mip
access::read_writetexture kernel without readingPORTING_NOTES.md §6— explicit-LOD reads/writes are required and we work around it viaDeviceMipmapImage::fill's working-texture indirection. - Don't disable
-ffast-mathto fix a sub-ULP drift problem. Performance-critical code relies on it; relax the test budget instead and document the drift case (per §5 of PORTING_NOTES).
Reference patterns¶
The cleanest small examples to read first:
src/shaders/depth_map/eig33.metal+tests/test_eig33.cpp— single kernel, FP64→FP32 port, Householder + QL. (S3.)src/shaders/depth_map/color_kernels.metal+src/depth_map_metal/src/ColorOps.cpptests/test_color.cpp— value type with multiple entry points.src/shaders/depth_map/volume_optimize.metal+src/depth_map_metal/src/Volume.cpp(Volume::optimize) — the best-in-class example of multi-dispatch on a single command buffer (S44).
Larger end-to-end pipelines to read for orchestration patterns:
tests/test_sgm_pipeline.cpp(S14/S15) — SGM core.tests/test_refine_pipeline.cpp(S18) — Refine.tests/test_depth_pipeline.cpp(S24) — full SGM → Bridge → Refine → Optimize fused.