---
type: reference
title: "MLX↔OpenCL Bridge — Buffer Transfer Patterns"
description: "Buffer transfer chains between PyOpenCL and Apple MLX, zero-copy patterns, the 3-copy problem, and shared memory opportunities."
tags: [mlx, opencl, zero-copy, buffer-transfer, memory, compute-optimization]
timestamp: 2026-07-25
---

# MLX↔OpenCL Bridge — Buffer Transfer Patterns

The bridge between PyOpenCL (GPU compute) and Apple MLX (tensor math / Metal GPU) is the single biggest architectural bottleneck in both [[hashfita]] and [[crustybike]]. Understanding this boundary is essential for any HPC pipeline that mixes OpenCL kernels with MLX tensor operations.

---

## The 3-Copy Problem (hashfita)

hashfita's training pipeline crosses the OpenCL↔MLX boundary on every training step. The current chain:

```
CPU (Python) → OpenCL GPU (COPY_HOST_PTR)
    → CPU (numpy, cl.enqueue_copy)
        → MLX GPU (mx.array(numpy_array))
```

Three full memory copies per data transfer. No shared memory, no pinning, no zero-copy.

**Source:** `src/chronobreaker/core/opencl_context.py`, lines 206-249. The file header (line 8) explicitly states: *"Uses MLX for array operations (no NumPy except for buffer transfer)"* — NumPy is the mandatory bridge because PyOpenCL requires numpy arrays as host-side data.

### Why This Happens

1. PyOpenCL's `cl.Buffer` operates in OpenCL's memory space
2. `cl.enqueue_copy()` transfers to a numpy array in host memory
3. `mx.array(numpy_array)` copies from host memory to MLX's Metal memory space
4. There is no native `cl_khr_metal_shared_memory` extension in PyOpenCL
5. MLX does not expose raw Metal buffers for external framework interop

### Impact

In the 3-pass training step ([[ring-transformer]]):
- Pass 1: Forward inference (MLX) — no transfer
- Pass 2: GPU validation (OpenCL) — requires 3-copy chain for block headers
- Pass 3: Autograd (MLX) — no transfer

The bottleneck is Pass 2: every batch of block headers must traverse the full chain.

---

## Zero-Copy Pattern (crustybike)

crustybike solved the gather problem for Merkle permutations using MLX's `mx.take()`:

```python
# merkle_tensor.py — GPU-side gather
base_mx = mx.array(txid_matrix, dtype=mx.uint8)   # [N, 32] on GPU
idx_mx = mx.array(permutation_indices, dtype=mx.int32)
permuted_mx = mx.take(base_mx, idx_mx, axis=0)    # gather, NO host copy
```

**Source:** `src/crustybike/core/merkle_tensor.py`, line 80. The docstring (line 42) calls this *"Zero-Copy construction and lazy evaluation of MLX."*

The TXID matrix stays in GPU memory. Permutation indices are used to reorder it without Python-side byte manipulation. The generator yields batches of flattened arrays ready for the Metal kernel.

### Key Insight

`mx.take()` is a GPU-side index gather — the data never leaves the GPU. This is true zero-copy for the common pattern of "reorder a matrix by indices." It works because MLX's lazy evaluation defers execution until `mx.eval()` is called.

---

## Explicit JIT Trigger

Both projects use `mx.eval()` to explicitly trigger MLX's lazy computation graph:

```python
# crustybike/analysis/solver.py, line 75
mx.eval()  # Synchronously executes all pending MLX operations on Metal GPU
```

This is the MLX equivalent of PyTorch's `torch.cuda.synchronize()` — it forces the Metal GPU to flush its command buffer. Without it, MLX defers execution indefinitely, which can cause issues when the next operation depends on the result.

**Pattern:** Always call `mx.eval()` before reading results back to Python (`.item()` calls).

---

## OpenCL Buffer Management

### hashfita Pattern

```python
# opencl_context.py
input_buf = cl.Buffer(ctx, READ_ONLY | COPY_HOST_PTR, hostbuf=numpy_data)
output_buf = cl.Buffer(ctx, WRITE_ONLY, output.nbytes)
cl.enqueue_copy(queue, output, output_buf)  # GPU → CPU
```

No explicit buffer release — relies on Python GC.

### crustybike Pattern (Improved)

```python
# opencl_context.py, lines 612-616
try:
    # ... kernel execution ...
finally:
    input_buf.release()
    output_buf.release()
```

Explicit `buffer.release()` in `finally` blocks for deterministic VRAM garbage collection. Critical for long-running mining processes.

---

## Opportunities

| Opportunity | Difficulty | Impact |
|-------------|-----------|--------|
| `cl_khr_metal_shared_memory` extension | High — requires C interop | Eliminates 2 of 3 copies |
| `np.frombuffer()` instead of full copy | Low | Reduces 1 copy to view |
| Shared Metal buffer via MLX C API | High — undocumented | True zero-copy |
| OpenCL→MLX via mmap'd file | Medium | Avoids numpy intermediate |
| Merge validation into MLX graph | High — redesign training loop | Eliminates boundary entirely |

---

## Cross-References

- [[compute-optimization]] — Hub
- [[opencl-kernel-design]] — How the kernels are loaded and dispatched
- [[gpu-async-pipelines]] — How the transfer is hidden behind async
- [[floating-point-determinism]] — Precision at the boundary
- [[ring-transformer]] — Where the 3-copy chain hits hardest (training step)
