Skip to content

Phase 3: convert to_binary to async enif_send - #43

Merged
ausimian merged 1 commit into
mainfrom
phase-3-async-to-binary
Apr 19, 2026
Merged

Phase 3: convert to_binary to async enif_send#43
ausimian merged 1 commit into
mainfrom
phase-3-async-to-binary

Conversation

@ausimian

Copy link
Copy Markdown
Owner

Third PR of the async-worker migration. Phases 1 (#41) and 2 (#42) merged; this PR converts the last worker-bound NIF — `to_binary` — to the async pattern.

What changes

  • `c_src/emily_nif.cpp` — `to_binary` becomes `to_binary_nif`, using `emily::async_reply` with `fine::make_resource_binary` built inside the worker-allocated msg_env (Option 3a, validated by Spike B).
  • `lib/emily/native.ex` — public `to_binary/2` wraps `to_binary_nif/2` through `Async.call/1`.
  • Drops `ERL_NIF_DIRTY_JOB_CPU_BOUND` — `to_binary` was the last dirty-flagged NIF. After this merges, no NIF blocks any BEAM scheduler (regular or dirty) on MLX work.

Pinning

The worker calls `fine::make_resource_binary(msg_env, std::move(pin), data, nbytes)`. ERTS bumps the Tensor resource refcount internally; the binary term carries that ref through `enif_send` into the receiver's heap fragment, where it survives until the binary is GC'd. Spike B exercised this with 250 distinct 4 MB tensors (1 GB of transient MLX memory) — all reclaimed cleanly after GC + `clear_cache`.

User-visible effect

None. `Emily.Native.to_binary(w, tensor)` still blocks until the tensor is materialised and returns a resource binary aliasing MLX memory.

Test plan

  • `mix precommit` — 425 tests, 0 failures
  • Full heavy suite (including `qwen3_full`, `vit_full`, `whisper_full`, `distilbert_full`, `training_full`, `qwen3_quant_full`, `fast_kernels_full`, `conformance`) — 453 tests, 0 failures

Converts the last worker-bound NIF to the async model. `to_binary`
was the hardest case because it returns a resource binary that
aliases MLX-owned memory via `fine::make_resource_binary` — Spike B
validated that building the resource binary on the worker-allocated
msg_env correctly pins the Tensor resource across `enif_send`, so
no memcpy fallback is needed.

The worker thread now runs `mx::contiguous + mx::eval` and builds
the resource binary directly into msg_env. The binary carries one
resource ref into the caller's heap fragment, keeping the MLX
buffer alive for the lifetime of the binary on the receiver.

Drops the `ERL_NIF_DIRTY_JOB_CPU_BOUND` flag — `to_binary` was the
last dirty-flagged NIF in the project. With Phase 3 merged, no
NIF blocks any BEAM scheduler (regular or dirty) on MLX work.

Full suite: 425 tests, 0 failures.
Full heavy suite (including qwen3_full, vit_full, whisper_full,
distilbert_full, training_full, qwen3_quant_full, fast_kernels_full,
conformance): 453 tests, 0 failures.
@ausimian
ausimian merged commit 048cf61 into main Apr 19, 2026
1 check passed
@ausimian
ausimian deleted the phase-3-async-to-binary branch April 19, 2026 00:13
ausimian added a commit that referenced this pull request Apr 19, 2026
After the Phase 1-3 async-worker migration (#41-#43), NIFs no longer
block a scheduler on a sync promise — they enqueue on the worker and
the public wrapper awaits a reply via enif_send. Update the README
Concurrency model section and the CHANGELOG worker-thread-dispatch
bullet so they describe the shipping behaviour.
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.

1 participant