feat(mining): Pearl mining integration

Consolidates NVIDIA vLLM, Apple Silicon, CPU Pearl mining support, CLI/docs, and live H100 validation.
This commit is contained in:
Jon Saad-Falcon
2026-05-05 19:11:15 -07:00
committed by GitHub
parent e79bc1e196
commit f41cf420be
74 changed files with 14533 additions and 18 deletions
@@ -0,0 +1,711 @@
# Spec B — Apple Silicon enablement for Pearl mining
| | |
|---|---|
| **Date** | 2026-05-05 |
| **Status** | Design — Phase 0 investigation complete; v1 scope reduced (see §1.5) |
| **Owner** | OpenJarvis team (parallel-agent friendly) |
| **Companion spec** | [Spec A — vLLM-Pearl mining integration (v1)](2026-05-05-vllm-pearl-mining-integration-design.md) |
| **Repos referenced** | `OpenJarvis`, `pearl-research-labs/pearl`, possibly upstream `ml-explore/mlx`, `ggerganov/llama.cpp` |
> **For agents picking this up cold:** read §1 ("Cold-start brief") first, then **§1.5 ("Phase 0 findings")** which substantially simplifies v1 scope. The original Phase 14 GPU-kernel plan in §5–§8 is preserved as the v2/v3 path; v1 ships using upstream Pearl's PyTorch reference and pure-Rust miner.
## 1. Cold-start brief
**One-paragraph problem statement.** OpenJarvis is adding a `mining` subsystem (Spec A) that lets users mine the Pearl PoUW blockchain through their local LLM inference. Pearl's reference miner is CUDA-only and bound to NVIDIA Hopper (`sm_90a`, H100/H200) — most OpenJarvis users on Apple Silicon are locked out at the protocol level. **This spec is the plan to unblock them.** The OJ-side integration work is small (a new `MiningProvider` implementation that drops into the existing `MinerRegistry` from Spec A); the substantial work is a Metal port of Pearl's `NoisyGEMM` kernel and a matching plugin into an Apple-native inference backend (MLX or llama.cpp Metal). The Pearl validation path is plonky2-STARK-based and **already hardware-neutral** — see §3 for the evidence — so a correct Metal implementation produces blocks Pearl validators accept without any consensus changes.
**Three things to know before doing any work.**
1. The Pearl validator (`pearl/zk-pow/src/api/verify.rs::verify_block`) operates on a STARK proof and references no hardware. **The protocol does not care what GPU produced the work** as long as the math is correct and the proof verifies. CUDA-only is a performance choice, not a consensus choice.
2. Pearl's CUDA kernel (`pearl/miner/pearl-gemm/csrc/gemm/`) uses Hopper-only primitives (TMA, WGMMA, thread-block clusters, CUTLASS 3.x). A Metal port is **not a translation** — it's a from-scratch reimplementation against a different programming model. Plan effort accordingly.
3. The OJ integration boundary is the `MiningProvider` ABC defined in Spec A §4.4. **Do not modify Spec A.** Add a new provider file (`mining/mlx_pearl.py` or `mining/llamacpp_pearl_metal.py`), implement the ABC, register via `MinerRegistry`, ship a new optional extra. Everything else in Spec A — sidecar shape, config schema, telemetry adapter contract, v2 fee/pool seams — applies unchanged.
## 1.5. Phase 0 findings — significant scope simplification (2026-05-05)
Phase 0 investigation produced four findings that reshape this spec. **The original §5–§8 plan (Metal NoisyGEMM kernel + custom MLX/llama.cpp plugin) is preserved as v2/v3, but is no longer required for v1.**
### 1.5.1 The validator is hardware-neutral (confirmed)
`zk-pow/src/api/verify.rs::verify_block` and `verify_plain_proof` are pure Rust + plonky2; no CUDA, no GPU paths, no hardware introspection. §3.1's claim is **verified by direct code reading** (see file paths in §11). The protocol accepts blocks from any implementation that produces correct math.
### 1.5.2 A complete hardware-neutral miner already exists upstream
`pearl/zk-pow/src/ffi/mine.rs::mine()` is a pure-Rust mining function:
- Generates random `i8` matrices `A` (m×k) and `B` (k×n) with values in `[-64, 64]`
- Computes blake3-derived noise via `circuit/pearl_noise.rs::compute_noise_for_indices`
- Performs the noised dot products in tile patterns (`PeriodicPattern.rows_pattern × cols_pattern`)
- Hashes the jackpot tile and checks the difficulty target
- Returns a `PlainProof` that `verify_plain_proof` accepts
It is exposed to Python via `py-pearl-mining` (`pearl_mining.mine`). Dependencies: pure Rust (`zk-pow`, `pearl-blake3`, `blake3`, `rayon`, `pyo3`, `tikv-jemallocator`). **No CUDA, no platform-specific code in the Cargo dependency tree.**
### 1.5.3 A PyTorch reference of the *production* NoisyGEMM also exists upstream
`pearl/miner/miner-base/src/miner_base/noisy_gemm.py::NoisyGemm` is a complete PyTorch reference of the same NoisyGEMM that vllm-miner accelerates with the H100 CUDA kernel:
- `noise_A`, `noise_B`, `gemm`, `noisy_gemm` methods replicating the kernel's math in `torch.matmul` calls
- The denoising path produces **bit-exact results** versus a vanilla `torch.matmul(A.int32, B.int32)` — verified by the `assert torch.equal(result, expected)` test at `miner/miner-base/tests/test_noisy_gemm.py:92`. The "fp16 tolerance" budget I assumed in the original §5.2 is unnecessary for the int7×int7→int32 protocol path
- Dependencies: `torch==2.11.0`, `blake3`, `numpy`, `pearl-gateway`, `py-pearl-mining` — all install on macOS arm64
### 1.5.4 Empirical confirmation (2026-05-05, on this hardware)
`py-pearl-mining` was built from the upstream source on the spec author's M2 Max. The build produced `py_pearl_mining-0.1.0-cp312-abi3-macosx_11_0_arm64.whl` in 56 seconds. End-to-end mining cycle:
```
running mine(m=256, n=128, k=1024, rank=32) on Apple Silicon CPU…
mine() returned a proof in 0.078s
verify_plain_proof: ok=True, msg='Mining solution verified successfully' (0.3 ms)
END-TO-END MINING ON APPLE SILICON SUCCEEDED
```
The test difficulty here is `nbits=0x1D2FFFFF` (the test fixture from `py-pearl-mining/tests/test_python_api.py`), much lower than mainnet difficulty — so 78 ms is **per-share at the test difficulty**, not real expected wall-clock per share at the network's current difficulty. But the *correctness* of the path is proven.
### 1.5.5 Reframed v1 — what to build, what to defer
| Layer | Original plan (§5–§8) | New v1 plan |
|---|---|---|
| Reference oracle (Phase 0-B) | Build from scratch in PyTorch, validate against H100 CUDA | **Already exists upstream**`miner-base.noisy_gemm` + `pearl_mining.mine`. OJ ships a thin wrapper, no reimplementation. |
| Inference-backend plugin (Phase 2) | Custom MLX or llama.cpp Metal plugin doing NoisyGEMM | **Deferred to v2.** v1 is **decoupled mining** — mining runs as a separate process via the upstream Rust miner; user's existing inference (Ollama, MLX, llama.cpp) is unaffected. |
| Metal NoisyGEMM kernel (Phase 1) | Months of GPU-kernel engineering | **Deferred to v3** as a perf optimization once v1 ships and demand is proven. Original §6.1 content preserved as the v3 plan. |
| OJ provider integration (Phase 3) | New `MiningProvider` impl | **v1 ships this** — see §13. `MiningProvider` ABC from Spec A unchanged. |
| Bringup + verification (Phase 4) | Hardware matrix + testnet | **v1 ships this** — see §14. Same hardware matrix, simpler scope. |
| Pearl coordination (Phase 0-A) | Confirm upstream-vs-fork posture | Still needed (see §12) — but the bar is lower since v1 doesn't require any code from us in Pearl's tree. |
### 1.5.6 Honest performance expectations for v1
This is **not** competitive mining. The point of vllm-miner is that it amortizes mining work over LLM inference matmuls (the matmul you're already doing for inference *is* the mining work). v1 here decouples them: your CPU does mining, your GPU does inference. The hashrate will be low. **But it works today, and it ships with a credible upgrade path.** Document this transparently in `mine doctor` and the user guide.
The v2 (PyTorch-MPS NoisyGEMM coupled with MLX/llama.cpp inference) and v3 (native Metal kernel) work paths in §5–§8 remain the route to competitive Apple Silicon mining. They're explicitly not blocking v1.
## 2. Why this is its own spec
Spec A's scope is the v1 integration that ships today on the only working configuration (vLLM + sm90). Apple Silicon enablement is a separate, parallelizable workstream because:
- **Different ownership boundary.** Spec A is Python integration of an existing Pearl Docker image. Spec B is GPU-kernel engineering with potential upstream contribution to Pearl. These need different reviewers, different CI surfaces (no H100 needed, but Apple Silicon required), and different release cadence.
- **Different timeline.** Spec A is weeks. Spec B is plausibly months for the kernel work alone.
- **Different blast radius.** Spec A ships zero risk to non-mining users; even mining users who edit the wrong config get a clear error. Spec B carries protocol-correctness risk — a bug in NoisyGEMM produces invalid blocks that get rejected by validators.
- **Parallel-agent ergonomics.** The user has explicitly asked for this spec to be picked up by a separate agent in parallel. Self-containment is a design goal.
## 3. Evidence: Apple Silicon support is possible
### 3.1 The validator is hardware-neutral
`pearl/zk-pow/src/api/verify.rs::verify_block`:
```rust
pub fn verify_block(public_params: &PublicProofParams, proof: &ZKProof, cache: &mut CircuitCache) -> Result<()> {
let (params, pis) = prepare_verification(public_params, proof, None)?;
PearlRecursion::compile_circuits(params, cache, false)?;
verify_with_cache(params, cache, &pis, proof)
}
```
Verification is `PearlRecursion::verify(params, cache, pis, &proof.plonky2_proof)` — a recursive plonky2 STARK check. No GPU code path, no CUDA dependency. Validator nodes run pure Rust.
The mining work consists of three things, all of which are mathematical specifications — not implementation specifications:
1. A NoisyGEMM result whose noise pattern is derived from blake3 of a per-block key
2. A blake3 commitment hash over the noised matmul that meets a difficulty target
3. A plonky2 STARK proof that ties the result to the commitment
Any implementation that produces matching outputs is acceptable to the network. **This is the design intent of PoUW** — the work has to be replayable and verifiable, but not hardware-bound.
### 3.2 The Pearl team explicitly anticipates non-CUDA plugins
From `pearl/miner/README.md`:
> "Currently only mining via vLLM is supported, in the future we hope to supply plugins for other LLM inference libraries, like SGLang, TensorRT-LLM, Ollama, ..."
Apple is not in their list, but the framing — "supply plugins for other LLM inference libraries" — implies the boundary is at the inference backend, not at the consensus protocol. Confirms the architectural read.
### 3.3 Reference implementation exists in py-pearl-mining
`pearl/py-pearl-mining/` is a PyO3 crate exposing Pearl mining primitives in Python. **Read it before designing the Metal port** — it likely contains the protocol-relevant constants in a hardware-neutral form, suitable as a reference oracle for Phase 0 testing (§5).
## 4. Scope
### 4.1 In scope
- **Phase 0** (§5): protocol-acceptance verification + Pearl-team coordination + Python reference oracle
- **Phase 1** (§6.1): Metal NoisyGEMM kernel — the substantive engineering
- **Phase 2** (§6.2): inference-backend plugin — MLX or llama.cpp Metal
- **Phase 3** (§7): OJ provider integration — new `MiningProvider` impl, new optional extra, registry hookup
- **Phase 4** (§8): verification matrix across Apple Silicon variants and bringup on Pearl testnet
- Documentation deliverables and the upstream-contribution path
### 4.2 Out of scope
- Pool support and the 20% OJ fee (Spec A §8.5; that lives in a future v2 pool spec)
- Custody, signing, or routing Pearl funds (Spec A anti-goal; same here)
- AMD ROCm enablement (separate spec, parallel structure to this one)
- Intel Arc / Mac Intel / older CUDA enablement (separate specs)
- Modifying anything in Spec A. **Spec B is purely additive.**
- Pearl protocol changes (none required; see §3.1)
### 4.3 Explicit non-goal: economic competitiveness
This spec does not promise that Apple Silicon mining will be **profitable**. The performance gap to a tuned H100 kernel is likely large (§6.1.5 discusses why). What this spec *does* promise: a correct, working Apple Silicon path that's enabled the day the kernel ships, with a transparent doctor surface that tells Mac users honestly what their hashrate looks like. Whether it's worth the electricity is a user decision.
## 5. Phase 0 — investigation, coordination, reference oracle
The phase that costs the least and prevents the most rework. Three workstreams in parallel.
### 5.1 Workstream P0-A: Pearl-side coordination
**Goal:** Confirm protocol acceptance in writing from Pearl maintainers; align on whether OJ contributes upstream or ships independently.
**Steps:**
1. Open a GitHub Discussion on `pearl-research-labs/pearl`: "Apple Silicon / Metal NoisyGEMM enablement — coordination". Reference Spec B URL.
2. Get explicit confirmation from a Pearl maintainer that:
- Validator path is hardware-neutral as believed (§3.1).
- There is no Pearl-internal Metal port already in flight that would conflict.
- LICENSE compatibility allows OJ-authored kernel code to be either contributed upstream (preferred) or distributed alongside OJ.
3. Discuss the upstream-vs-fork question. Strong default: **contribute upstream into a new `pearl/miner/pearl-gemm-metal/` crate**, paralleling `pearl-gemm/`, so Pearl owns the kernel long-term and we benefit from their CI and review. Fork only if upstream contribution is blocked.
**Exit criteria:**
- [ ] Written confirmation of protocol acceptance
- [ ] Agreement on contribution model (upstream / coordinated fork / independent)
- [ ] No duplicate-effort risk
### 5.2 Workstream P0-B: build a reference oracle
**Goal:** A pure-Python (or pure-Rust) implementation of NoisyGEMM that produces bit-exact-or-fp16-tolerance-bounded outputs versus Pearl's CUDA reference. **Used as the test oracle for Phase 1** — without it, you can't verify the Metal kernel's correctness against a portable baseline.
**Steps:**
1. Read `pearl/miner/pearl-gemm/csrc/gemm/` end-to-end. Catalog the protocol-relevant constants in `pearl_gemm_constants.hpp`:
- `kAxEBLScaleFactor = 1 << 14`
- `kEARxBpEBScaleFactor = 1 << 12`
- `kIntToFp16ScaleFactor = 1 << 12`
- `kEBRScaleFactorDenoise`, `kEALScaleFactorDenoise`
2. Read `pearl/py-pearl-mining/` to see what's already exposed in Python. If a reference impl already lives there, **use it**; do not duplicate.
3. If gaps exist, build them in PyTorch (CPU). Mirror the structure of the CUDA kernels:
- `noise_generation.cu``noise_generation.py` — derive `EAL`, `EAR`, `EBL`, `EBR` from blake3-of-key + seed
- `pearl_gemm` (matmul + noise) → `pearl_gemm.py` — compute `Y_noisy = (A + EAL·EAR) × (B + EBL·EBR)` with the documented scaling
- `inner_hash_kernel.cu``inner_hash.py` — blake3 commitment over the noised matmul
- `denoise_converter.cu``denoise.py` — recover `Y_clean = A·B` from `Y_noisy` and the noise components
- `pow_utils.hpp``pow_check.py` — difficulty target check
4. Cross-check: run a corpus of 100+ inputs through the Pearl CUDA reference (on an H100 dev box; see §5.4) and through the Python reference. Assert outputs match within the documented tolerance — most likely **bit-exact for int paths and fp16-tolerance for the denoised result**.
**Exit criteria:**
- [ ] `tools/pearl-reference-oracle/` (in OJ repo, or separate repo) builds and tests pass
- [ ] Parity confirmed against Pearl CUDA on ≥100 input sets
- [ ] Constants table documented in this spec (replace the bullet list above with the verified values)
### 5.3 Workstream P0-C: Apple-side viability
**Goal:** Decide between MLX and llama.cpp Metal as the integration host before designing the kernel.
**Decision criteria:**
| Factor | MLX (`ml-explore/mlx`, `mlx-lm`) | llama.cpp Metal (`ggerganov/llama.cpp`) |
|---|---|---|
| Op-replacement hooks | Less mature; would likely require monkey-patching `mlx.nn.Linear` or upstream PR adding plugin hooks | More mature; `ggml` op tree is open and Metal backend has clear extension points (`ggml-metal.metal`) |
| Apple-native quantization story | Excellent (4-bit, 8-bit native ops) | Good but not as native |
| Inference-quality fidelity for OJ users today | High — MLX-LM is the de facto Mac LLM stack | High — also widely used |
| Upstream-contribution complexity | Higher (smaller team, less plugin culture) | Lower (large open community, clear contributor flow) |
| Ecosystem alignment with OJ engine map | OJ's `engine/` doesn't currently have an MLX engine; would need both | OJ already has llama.cpp via `engine/openai_compat_engines.py` |
**Recommendation:** **llama.cpp Metal first**, MLX as a fast-follow. Reasoning: ggml's op tree gives a cleaner extension path for a custom NoisyGEMM op; OJ already has llama.cpp engine wiring; and the upstream-contribution path is more navigable. MLX is a better long-term fit for Apple-native users but is currently a harder integration target.
**Steps:**
1. Spike: implement a no-op "custom op" passthrough in llama.cpp Metal. ~1-2 days work to confirm the integration mechanism is real and the build pipeline cooperates.
2. Spike: same in MLX. Compare effort.
3. Pick one. Document the decision in this spec.
**Exit criteria:**
- [ ] Decision made and documented in §6.2
- [ ] Trivial plugin hook proven on the chosen backend
### 5.4 Hardware required for Phase 0
- One H100/H200 box (cloud rental fine — Lambda, RunPod, Crusoe). Used for: running Pearl's CUDA reference to capture parity test vectors, running the Pearl Docker miner end-to-end as a known-good baseline.
- Apple Silicon dev machines: M2 Max minimum, M3/M4 Pro+ preferred. M-series Ultra ideal for any perf experiments.
- Estimated cloud cost for Phase 0: < $200.
## 6. Phases 1 and 2 — kernel and plugin
### 6.1 Phase 1 — Metal NoisyGEMM kernel
**Goal:** A Metal compute-shader implementation of NoisyGEMM that produces outputs matching the Phase 0 reference oracle, performant enough to make Mac mining a real (if low-yield) feature.
#### 6.1.1 Implementation surface
Two viable targets, in order of preference:
**A. Direct Metal Shading Language (MSL) compute kernels.** Maximum control, maximum performance ceiling, maximum effort. The CUDA reference is highly tuned (TMA, WGMMA, multi-stage pipelines); a direct MSL port can lean on Apple's matmul intrinsics where they exist (`simdgroup_matrix` ops on M3+).
**B. Metal Performance Shaders Graph (MPSGraph).** Higher-level than raw MSL; uses Apple's tuned matmul kernels under the hood; limited control over the in-kernel commitment hash. Likely path: do the matmul via MPSGraph, do noise generation + commitment hashing as separate kernels, accept the perf hit from less fusion.
**Recommendation:** Start with B for correctness and shipping speed; profile; move hot paths to A only if economically justified. Apple's matmul intrinsics are fast enough that the perf gap to a fused implementation may be acceptable.
#### 6.1.2 Algorithm structure
Following the Pearl CUDA reference, end-to-end work performed for one mining attempt:
1. **Quantize inputs.** `A: fp16 → int8 + scale_A`, `B: fp16 → int8 + scale_B`. Match Pearl's `quantize_kernel.cu` semantics (per-row or per-channel scales — verify via Phase 0 oracle).
2. **Generate noise tensors.** From `key_A`, `key_B` (per-block blake3-derived seeds), produce `EAL` (m, R), `EAR` (k, R), `EBL` (k, R), `EBR` (n, R) of int8. Scale factors per `pearl_gemm_constants.hpp`.
3. **Noisy matmul.** Compute `Y_noisy = (A + EAL · EAR_T) × (B + EBL · EBR_T)`. Output is int32 then converted to fp16.
4. **Inner-hash commitment.** blake3 over `Y_noisy` (or a row-tile of it) to produce the PoW target candidate. This is the hottest path — the noise + commitment loop runs at every share.
5. **PoW check.** Compare commitment digest against the difficulty target (`make_pow_target_tensor` semantics from Pearl's Python interface).
6. **On hit: denoise.** Compute `Y_clean = Y_noisy - (noise contributions)` to feed back into vLLM/MLX as the actual matmul output. Inference cannot be wrong.
7. **Post-hit: STARK proof generation.** When a share meets the network difficulty target, the miner generates a plonky2 STARK proof tying the noisy matmul + commitment to the block. This proving step is **separate from the Metal kernel** — it runs in pure Rust via Pearl's existing `zk-pow/` and `py-pearl-mining` code paths and should work cross-platform unchanged. Cost: seconds-to-minutes of CPU per block. Confirm cross-platform builds during Phase 0-C and §7.5.
#### 6.1.3 Crate / package layout
Strong preference: **upstream contribution to Pearl** as `pearl/miner/pearl-gemm-metal/` paralleling the existing `pearl-gemm/`:
```
pearl/miner/pearl-gemm-metal/
Cargo.toml (or pyproject.toml + setup.py — match Pearl conventions)
metal/ (.metal MSL source files)
src/
lib.rs (or src/pearl_gemm_metal/__init__.py)
tests/
```
If upstream contribution is blocked (Phase 0 outcome), fork with attribution into `OpenJarvis/vendor/pearl-gemm-metal/` and document the divergence policy in this spec.
#### 6.1.4 Testing
- **Parity tests.** Each kernel (noise gen, matmul, inner hash, denoise, PoW check) tested independently against the Phase 0 reference oracle. Bit-exact for int paths; fp16-tolerance bounded for fp paths (specific tolerance: TBD via Phase 0 measurement).
- **End-to-end correctness.** Full mining attempt produces a candidate proof that the reference Rust prover (`zk-pow/`) accepts.
- **Hardware fuzz.** Run on M1 Pro, M2 Max, M3 Max, M4 Max, and M-Ultra variants. Catch any silently-wrong hardware behavior (Metal feature variance across generations is real).
#### 6.1.5 Performance expectations
Honest baseline: **expect 0.050.2× the share rate of an H100** on a high-end M-Ultra, and proportionally less on smaller chips. Reasons:
- H100 has dedicated FP8/FP16 tensor cores with WGMMA throughput Apple Silicon does not match
- Pearl's CUDA kernel is heavily fused (matmul + noise + commitment in one kernel via TMA pipelining); a Metal version will likely be less fused
- 70B model bandwidth requirements stress unified memory
This is fine. Mac mining is a feature for Apple Silicon owners who want to participate, not a competitive yield product. Document it transparently in `mine doctor` and the user guide.
#### 6.1.6 Exit criteria for Phase 1
- [ ] Parity tests pass on M2 Max and M4 Max
- [ ] End-to-end mining attempt produces a valid proof accepted by `zk-pow::verify_block`
- [ ] Performance characterized and published (M-series matrix)
- [ ] Code merged upstream OR forked-with-policy per Phase 0 outcome
### 6.2 Phase 2 — Inference-backend plugin
**Goal:** A llama.cpp Metal (or MLX, per Phase 0-C) plugin that swaps the standard quantized linear op for Phase 1's NoisyGEMM during inference, so a Mac running this plugin produces both correct LLM outputs and valid mining shares.
#### 6.2.1 Path: llama.cpp Metal (assuming Phase 0-C selected this)
- Add a custom `ggml` op `GGML_OP_PEARL_NOISY_GEMM` with a Metal backend implementation that calls Phase 1's kernels.
- Plugin entry point: a small library that, when loaded, replaces the default linear op in the model graph during loading.
- Build artifact: `libpearl_metal_plugin.dylib` (or static lib).
#### 6.2.2 Path: MLX (alternate)
- Define `mlx.NoisyLinear` as a subclass of `mlx.nn.Linear` that calls Phase 1's kernels via a custom Metal op binding.
- Provide a model-loading shim: `from openjarvis.mining import patch_mlx_for_pearl; patch_mlx_for_pearl()` that monkey-patches `mlx.nn.Linear` instances at load time. Less elegant; works.
#### 6.2.3 Inference-quality regression tests
The plugin is correctness-critical: a noised model that doesn't fully denoise produces degraded responses. Test:
- Load a small reference model (e.g., a 1-3B parameter Pearl-blessed model if one exists for testing, otherwise the smallest model the protocol accepts).
- Run a fixed prompt set through both noised+denoised and standard paths.
- Assert outputs are bit-exact or within fp16 tolerance.
- Run OJ's existing eval framework (`src/openjarvis/evals/`) on a small benchmark (e.g., an MMLU subset registered as a Pearl-mining-mode dataset). Assert no degradation > the tolerance budget. Falling back to `lm-eval-harness` is acceptable if OJ's eval surface for Mac is incomplete at the time.
#### 6.2.4 Exit criteria
- [ ] Plugin loads in chosen backend
- [ ] End-to-end inference produces correct outputs (regression tests pass)
- [ ] Mining shares are submitted to a Pearl testnet during inference
- [ ] At least one block found on testnet from a Mac
## 7. Phase 3 — OpenJarvis provider integration
Where the OJ-side work is small. Inherits the entire `MiningProvider` ABC, registry, sidecar, config schema, telemetry adapter, and v2 seams from Spec A unchanged.
### 7.1 New files
```
src/openjarvis/mining/
llamacpp_pearl_metal.py # OR mlx_pearl.py — depending on Phase 2 path
# @MinerRegistry.register("llamacpp-pearl-metal")
# implements MiningProvider ABC from Spec A §4.4
```
### 7.2 New optional extra
```toml
# pyproject.toml
mining-pearl-metal = [
"pearl-metal-plugin>=0.1", # the Phase 2 plugin, however published
# MLX path adds: "mlx>=0.X", "mlx-lm>=0.X"
# llama.cpp path adds: "llama-cpp-python>=0.X" with Metal extras
]
```
### 7.3 Capability detection
```python
# src/openjarvis/mining/llamacpp_pearl_metal.py
class LlamaCppPearlMetalProvider(MiningProvider):
provider_id = "llamacpp-pearl-metal"
@classmethod
def detect(cls, hw: HardwareInfo, engine_id: str, model: str) -> MiningCapabilities:
if hw.platform != "darwin":
return MiningCapabilities(False, reason="Apple Silicon required (platform != darwin)")
if hw.gpu is None or hw.gpu.vendor != "apple":
return MiningCapabilities(False, reason="Apple Silicon GPU required")
if engine_id not in {"llamacpp", "llama-cpp"}:
return MiningCapabilities(False, reason=f"engine '{engine_id}' has no Pearl Metal plugin; use llamacpp")
if not _pearl_metal_plugin_available():
return MiningCapabilities(False, reason="install with `uv sync --extra mining-pearl-metal`")
if not _model_has_pearl_variant(model):
return MiningCapabilities(False, reason=f"model '{model}' has no Pearl-blessed variant")
return MiningCapabilities(True, estimated_hashrate=_estimate_hashrate(hw))
```
Each branch is exactly the kind of "why can't I mine" message Spec A's `mine doctor` surfaces verbatim.
### 7.4 Lifecycle
Unlike Spec A's vLLM provider which orchestrates a Docker container, the Apple provider runs **two coordinated subprocesses directly on the host**:
1. The inference server (llama.cpp server with the Pearl Metal plugin loaded, or MLX-LM server depending on Phase 0-C path)
2. `pearl-gateway` as a sibling process — same one that runs inside the Docker container in Spec A, but here it runs natively on the Mac
Lifecycle:
- `start()`: spawn (1) with the Pearl Metal plugin pre-loaded (`DYLD_INSERT_LIBRARIES`-style or `--plugin` flag depending on chosen backend's invocation contract), then spawn (2) pointing at it. Write the same sidecar shape Spec A defines, with `gateway_url` pointing at the native pearl-gateway. Track both PIDs internally.
- `stop()`: SIGTERM (2) first, then (1), with bounded waits and SIGKILL fallback. Remove sidecar.
- `is_running()`, `stats()`: identical contract to vLLM provider; `stats()` reads from the native pearl-gateway's `:8339/metrics`.
**No Docker.** Apple Silicon Docker doesn't pass through Metal; running Pearl in a Mac Docker container would defeat the purpose. Document this explicitly in §7 of this spec; do not attempt a Docker path.
### 7.5 Pearl gateway on Mac
The Pearl `pearl-gateway` process is currently only documented as part of the Docker container. For Mac, we need it to run natively. Two options:
1. Build `pearl-gateway` from source via `uv sync --package pearl-gateway` — same workspace package the Docker image uses. Should work cross-platform since it's pure Python plus py-pearl-mining bindings. Verify.
2. If (1) fails on Apple Silicon, work with Pearl maintainers (Phase 0-A) to port it — a small amount of work compared to the kernel.
**Phase 3 verifies (1).** This is a Phase 0-A coordination point.
### 7.6 Exit criteria
- [ ] `LlamaCppPearlMetalProvider` registered, detection matrix correct on M1/M2/M3/M4
- [ ] `jarvis mine init` runs to completion on Apple Silicon
- [ ] `jarvis mine start` launches subprocess + Pearl gateway on Mac
- [ ] `jarvis mine status` returns valid `MiningStats` from a real Mac mining session
- [ ] `jarvis mine doctor` produces honest, actionable output for Mac users
## 8. Phase 4 — Verification & bringup
### 8.1 Hardware matrix
| Chip | Test priority | Expected outcome |
|---|---|---|
| M1 / M1 Pro / M1 Max | low — generation 1 GPU may have feature gaps | works but slow |
| M2 / M2 Pro / M2 Max | medium | works |
| M2 Ultra | medium | best M2-class hashrate |
| M3 / M3 Pro / M3 Max | high — first gen with `simdgroup_matrix` | works, meaningful share rate |
| M4 / M4 Pro / M4 Max | high — current flagship | best non-M-Ultra hashrate |
For each chip in the matrix, run:
1. `jarvis mine init` end-to-end
2. `jarvis mine start` and run for ≥4 h continuous
3. Capture and publish: shares submitted, shares accepted, block-find time distribution, GPU temp, system load impact on normal use
4. Run a parallel `lm-eval-harness` on the mining endpoint to assert inference quality is unaffected
### 8.2 Pearl testnet bringup
Before any mainnet recommendation:
- Mine on Pearl testnet for ≥7 continuous days from at least two Apple Silicon variants
- Find at least one block on testnet from each variant
- Verify all blocks accepted by `zk-pow::verify_block` on a reference validator node
- Report results to Pearl maintainers; gate any mainnet announcement on their sign-off
### 8.3 Documentation deliverables
- `docs/user-guide/mining-apple-silicon.md` — user-facing: prerequisites, install flow, doctor reading guide, performance expectations table, links to share-rate calculators
- `docs/development/mining-providers.md` — generalized "how to add a new provider" guide using this spec as the canonical worked example
- An update to `docs/user-guide/mining.md` (Spec A) adding Apple Silicon to the supported-platforms list
### 8.4 Exit criteria
- [ ] Hardware matrix covered
- [ ] Testnet bringup complete
- [ ] Documentation merged
- [ ] Pearl maintainer sign-off obtained
- [ ] OJ release notes call out Apple Silicon mining as supported
## 9. Risks
| ID | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| R1 | Pearl validator rejects non-CUDA-mined blocks despite hardware-neutral validator code | low (validator code reviewed) | catastrophic (whole spec invalid) | Phase 0-A explicit confirmation; Phase 0-B oracle reduces likelihood of math drift |
| R2 | Pearl ships their own Metal port, conflicts with OJ's | medium (depends on Pearl roadmap) | high (rework or fork) | Phase 0-A coordination; default to upstream contribution |
| R3 | Metal NoisyGEMM is so slow that mining is uneconomical even for hobbyists | medium-high | medium (feature ships but unused) | §4.3 names this as a non-goal; transparency in `mine doctor`; consider M-Ultra-only-by-default in v1 of this spec |
| R4 | NoisyGEMM correctness bug → invalid blocks → wasted user electricity | low if §6.1.4 testing rigorous | high (trust hit) | Strong parity testing against oracle; testnet bringup before mainnet |
| R5 | Inference-quality regression — denoised path doesn't fully recover model fidelity | medium | high | §6.2.3 regression tests; eval-harness gate before ship |
| R6 | Pearl protocol changes between Phase 0 and Phase 4 (multi-month) | medium | medium | Pin Phase 0 ref same as Spec A; renegotiate at each Pearl rev |
| R7 | Apple changes Metal API in macOS update | low-medium | medium | Use stable MSL features; pin Xcode toolchain |
| R8 | Upstream Pearl PR rejected | low (Pearl wants this) | medium (forced fork) | Phase 0-A negotiates upstream-vs-fork up front |
| R9 | `pearl-gateway` doesn't build on Apple Silicon (§7.5) | medium (it's Python — should work, but py-pearl-mining has Rust deps) | low (small fix) | Phase 0 verifies builds; Phase 0-A coordination if not |
## 10. Open questions
Phase 0 answered most of these from the upstream code (annotations below). The remaining open items are ones that require either Pearl maintainer input or empirical measurement on real network conditions.
1. **(Open — coordination)** Is there a Pearl-blessed "small" model for testing? The reference miner uses a 70B model — too big for fast iteration. A 7B or 13B variant for development would dramatically speed up v2/v3 plugin work. *Less critical for v1, since v1 is decoupled from inference and runs `mine()` directly without a model.*
2. **(Answered — N/A for v1)** ~~Documented fp16 tolerance budget for denoised matmul output~~`miner-base/tests/test_noisy_gemm.py:92` does `torch.equal(result, expected)`: the int7×int7→int32 path is **bit-exact**, no fp16 tolerance budget is needed. (May reappear in v3 Metal kernel work if int↔fp16 conversions are introduced for perf.)
3. **(Answered — yes)** Does `py-pearl-mining` already expose enough of NoisyGEMM in Python that Phase 0-B becomes a thin wrapper? **Yes.** `pearl_mining.mine` runs the entire mining algorithm in pure Rust. Additionally, `miner-base.NoisyGemm` provides a PyTorch reference of the production NoisyGEMM. Phase 0-B's "build a reference oracle" deliverable is now a *thin OJ-side wrapper* that calls upstream — see §13 and `tools/pearl-reference-oracle/` (created in this session).
4. **(Answered — likely yes; empirically verified for `py-pearl-mining`)** Is `pearl-gateway` cross-platform? Its `pyproject.toml` requires Python ≥ 3.10 and depends on `aiohttp`, `bitcoin-utils`, `blake3`, `numpy`, `prometheus-client`, `pybase64`, `pydantic`, `pyyaml`, `torch==2.11.0`, `py-pearl-mining` — all install on macOS arm64. Empirical install of the workspace was not run in this session; **action item for v1 implementation**: `uv sync` the workspace on macOS-15 and capture the build output.
5. **(Open — coordination)** Does Pearl gate any difficulty / consensus parameters on hardware introspection? Code review found none. Confirm in Phase 0-A discussion.
6. **(Open — measurement)** Minimum acceptable hashrate floor for `jarvis mine init` on Apple Silicon. The `MiningCapabilities.estimated_hashrate` field in Spec A §4.4 exists for this. v1 will populate from a calibration run during `mine init`. The *floor* is a policy decision, not a technical one — defer to user-research / community feedback once v1 ships.
7. **(Open — coordination)** Upstream contribution / CLA / LICENSE. Pearl is ISC; OJ is Apache-2.0; both are permissive and combine cleanly. **CLA TBD via Phase 0-A discussion**, but for v1 this is moot — OJ contributes no code into Pearl's tree, only consumes their published Python packages.
8. **(Answered — yes)** Apple Silicon CI on GitHub Actions: `macos-14` / `macos-15` runners are arm64 and can install `py-pearl-mining` via the wheel build verified in §1.5.4. OJ's CI can run mining unit tests. Mining the *real* network in CI is still out of scope.
9. **(Answered — `"llamacpp"`)** OJ's llama.cpp engine_id is `"llamacpp"` (single token, no hyphen). Confirmed at `src/openjarvis/engine/openai_compat_engines.py:9` and `src/openjarvis/engine/_discovery.py:18`. Update §7.3 capability detection to use this key. *(For v1 in §13, this only matters if we add an "informational" mining-aware hint to the existing llamacpp engine — v1 does not require any plugin into the engine.)*
10. **(Open — measurement, but de-risked)** plonky2 STARK proving latency on Apple Silicon CPU. Spec A §1 already notes proving is seconds-to-minutes of CPU per block (cross-platform, runs unchanged). For v1 the hashrate is so low that block-find latency is dominated by the search, not the proof. Empirical measurement still needed for v2/v3.
11. **(New — v1 specific)** Does `bitcoin-utils>=0.7.0` (a `pearl-gateway` dependency) have C extensions that need Apple-specific build flags? Likely pure-Python; verify during the v1 install workstream.
12. **(New — v1 specific)** Will `torch==2.11.0` (the version `miner-base` and `pearl-gateway` pin) install cleanly on macOS arm64? PyTorch generally has arm64 macOS wheels. Verify during v1 install.
## 11. Cross-references
- **[Spec A](2026-05-05-vllm-pearl-mining-integration-design.md)** — the v1 integration this extends. Read §4.4 (the `MiningProvider` ABC), §5.3 (sidecar shape), §8.18.2 (telemetry adapter contract), §8.5 (v2 fee/pool seams). All apply unchanged.
- **Pearl coordination thread (P0-A draft):** [`2026-05-05-pearl-coordination-discussion-draft.md`](2026-05-05-pearl-coordination-discussion-draft.md) — content the user posts on `pearl-research-labs/pearl` to confirm protocol acceptance and align on contribution model.
- **OJ-side Phase 0 deliverables (created this session):**
- `tools/pearl-reference-oracle/` — thin Python wrapper around upstream Pearl bindings + smoke test, runnable on Apple Silicon
- **Pearl repo paths read in Phase 0 (in priority order):**
1. `pearl/zk-pow/src/api/verify.rs` — the validator. Pure Rust, no GPU. **Hardware-neutrality verified.**
2. `pearl/zk-pow/src/api/proof.rs``PublicProofParams`, `ZKProof`, `PrivateProofParams`, `IncompleteBlockHeader`, `MiningConfiguration`, `MMAType`. Defines what the protocol commits to.
3. `pearl/zk-pow/src/ffi/mine.rs`**the entire hardware-neutral mining function**. Pure Rust. Already exposed to Python.
4. `pearl/zk-pow/src/circuit/pearl_noise.rs` — noise generation: `compute_noise_for_indices`, `generate_uniform_random_matrix`, `generate_permutation_matrix`. Hardware-neutral.
5. `pearl/py-pearl-mining/src/lib.rs` — PyO3 module. Re-exports `mine`, `verify_plain_proof`, `generate_proof`, `verify_proof`, `warmup_prove`. **Builds on macOS arm64, verified §1.5.4.**
6. `pearl/py-pearl-mining/Cargo.toml` — pure Rust deps: `pearl-blake3`, `zk-pow`, `blake3`, `rayon`, `pyo3`, `lazy_static`, `tikv-jemallocator`. No CUDA in tree.
7. `pearl/py-pearl-mining/tests/test_python_api.py` — the canonical end-to-end test. Use as the OJ smoke-test template.
8. `pearl/miner/miner-base/src/miner_base/noisy_gemm.py` — PyTorch reference of the production NoisyGEMM. The "reference oracle" §5.2 wanted to build is here.
9. `pearl/miner/miner-base/src/miner_base/noise_generation.py` — PyTorch noise generation matching `pearl_noise.rs`.
10. `pearl/miner/miner-base/src/miner_base/inner_hash.py` — PyTorch inner-hash with XOR reduction.
11. `pearl/miner/miner-base/tests/test_noisy_gemm.py` — bit-exact denoising verified at line 92.
12. `pearl/miner/miner-base/pyproject.toml` — deps (`torch==2.11.0`, `blake3`, `numpy`, `pearl-gateway`, `py-pearl-mining`); **no platform markers** → installs on Apple Silicon.
13. `pearl/miner/pearl-gateway/pyproject.toml` — deps (pure Python + py-pearl-mining + torch); **no platform markers**.
14. `pearl/miner/vllm-miner/src/vllm_miner/register.py` — vLLM plugin registration via `vllm.general_plugins` entry point. The pattern Phase 2 (v2 plan) would mirror.
15. `pearl/miner/pearl-gemm/csrc/gemm/pearl_gemm_constants.hpp` — protocol scale factors. Verified values:
- `kAxEBLScaleFactor = 1<<14 = 16384`
- `kEARxBpEBScaleFactor = 1<<12 = 4096`
- `kIntToFp16ScaleFactor = 1<<12 = 4096`
- `kEBRScaleFactorDenoise = -4` (= -kAxEBLScaleFactor / kIntToFp16ScaleFactor)
- `kEALScaleFactorDenoise = -1` (= -kEARxBpEBScaleFactor / kIntToFp16ScaleFactor)
16. `pearl/miner/pearl-gemm/setup.py:88``COMPUTE_CAPABILITY = "arch=compute_90a,code=sm_90a"`. Confirms CUDA kernel is Hopper-only.
17. `pearl/Taskfile.yml``build:miner` task is gated to `platforms: [linux, windows]`. **The miner Python install path Pearl ships today is Linux/Windows-only**; OJ's v1 path uses the components that *do* install on macOS, sidestepping this gate.
- **Pearl paper:** [Proof-of-Useful-Work via matrix multiplication (arXiv:2504.09971)](https://arxiv.org/abs/2504.09971) — read for the math formalization. Less critical now that the PyTorch reference exists upstream.
- **Apple references (still relevant for v2/v3):**
- [Metal Shading Language Specification](https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf)
- [MPS / MPSGraph documentation](https://developer.apple.com/documentation/metalperformanceshadersgraph)
- [MLX](https://github.com/ml-explore/mlx) — alternative v2 plugin host
- [PyTorch MPS backend docs](https://pytorch.org/docs/stable/notes/mps.html) — relevant for v2 (MPS-accelerated decoupled mining)
## 12. Implementation plan
Two implementation plans now live alongside this spec:
- **v1 plan (decoupled CPU mining via upstream Pearl):** Written via `superpowers:writing-plans` after this Phase 0 update. Tracking issue: [`2026-05-05-apple-silicon-pearl-mining-plan-v1.md`](2026-05-05-apple-silicon-pearl-mining-plan-v1.md).
- **v2 plan (PyTorch-MPS or MLX/llama.cpp coupled mining):** TBD. Written when v1 ships and we have empirical hashrate data justifying the next investment.
- **v3 plan (native Metal NoisyGEMM kernel):** TBD. Written only if v2 measurements show the additional kernel work is economically justified.
The original §5–§8 content describing Phases 04 of the *kernel-first* approach is preserved as the v3 plan's reference. Do not delete it — when the time comes to write the v3 plan, that content is the starting point.
## 13. Apple Silicon v1 — minimal path
This section defines the v1 design that ships in weeks rather than months. v1 is **decoupled mining**: the user's existing inference workflow (Ollama, MLX-LM, llama.cpp, vLLM-on-CPU, anything) is untouched; mining runs as a separate process via the upstream Pearl miner.
### 13.1 Architecture
```
OpenJarvis user (Apple Silicon)
┌────────────────────────────────┐
│ jarvis mine start │
│ ↓ │
│ CpuPearlProvider (this spec) │
│ ↓ subprocess.Popen │
│ ┌──────────────────────────┐ │
│ │ pearl-gateway (Python) │ │
│ │ ↑ JSON-RPC :8337 │ │
│ │ pearl-mine-loop (Python) │ │ ← uses py-pearl-mining
│ │ wraps pearl_mining.mine│ │ (pure Rust)
│ └──────────────────────────┘ │
│ │
│ Inference (untouched) │
│ ┌──────────────────────────┐ │
│ │ Ollama / MLX / llamacpp │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
pearld (BYO, same as Spec A)
```
The mining loop wraps `pearl_mining.mine()` in a process that:
1. Polls `pearl-gateway` for current `IncompleteBlockHeader` + `MiningConfiguration`
2. Calls `pearl_mining.mine(m, n, k, header, config)` to find a `PlainProof`
3. Submits the proof back to `pearl-gateway`, which generates the ZK proof and forwards to `pearld`
4. Loops
This is the *same* control flow vllm-miner runs — just without coupling the matmul to vLLM's inference. Pearl's existing `pearl-gateway` already does the orchestration we need; we just need a small mining loop that uses the CPU `mine()` instead of the CUDA path.
### 13.2 Module layout in OJ
```
src/openjarvis/mining/
cpu_pearl.py # @MinerRegistry.register("cpu-pearl") — v1 provider
_pearl_subprocess.py # PearlSubprocessLauncher — gateway + miner subprocesses
# Reused for future Apple-MPS / Metal providers
src/openjarvis/cli/
# mine_cmd.py is unchanged; cpu-pearl participates via the provider ABC
tests/mining/test_cpu_pearl.py
tools/pearl-reference-oracle/
README.md # documentation: oracle exists upstream
smoke_test.py # end-to-end mine + verify smoke test (created in this session)
```
### 13.3 Optional extra
```toml
mining-pearl-cpu = [
"py-pearl-mining>=0.1", # the wheel built in §1.5.4
"miner-base>=0.1", # PyTorch reference (used for parity testing)
"pearl-gateway>=0.1", # gateway service
]
```
When Pearl publishes these as PyPI wheels, the install is `uv sync --extra mining-pearl-cpu`. Until then, the spec for the implementation plan covers the local-build fallback (clone Pearl at the pinned ref, `maturin build` `py-pearl-mining`, `uv pip install` the workspace packages from local paths).
### 13.4 Capability detection
```python
# src/openjarvis/mining/cpu_pearl.py
class CpuPearlProvider(MiningProvider):
provider_id = "cpu-pearl"
@classmethod
def detect(cls, hw: HardwareInfo, engine_id: str, model: str) -> MiningCapabilities:
# cpu-pearl is engine-independent — it doesn't plug into inference
if not _pearl_mining_available():
return MiningCapabilities(False, reason="install with `uv sync --extra mining-pearl-cpu`")
if not _pearl_gateway_available():
return MiningCapabilities(False, reason="pearl-gateway package not installed")
if hw.platform not in {"darwin", "linux"}:
return MiningCapabilities(False, reason=f"platform '{hw.platform}' not yet supported")
# Optional: hardware-specific hashrate estimates
return MiningCapabilities(True, estimated_hashrate=_estimate_cpu_hashrate(hw))
```
The `engine_id` parameter is ignored because v1 is decoupled — mining works with **any** OJ engine, including no engine at all. (A future Apple-coupled provider would inspect `engine_id` to require `"llamacpp"` or `"mlx"`.)
### 13.5 Lifecycle (from Spec A's `MiningProvider` ABC)
- `start(config)`: spawn (1) `pearl-gateway` and (2) `pearl-mine-loop` subprocesses. Wait for gateway readiness on `:8339/metrics`. Write the standard sidecar JSON (Spec A §5.3) with `provider="cpu-pearl"`, gateway URL, and PIDs of both subprocesses.
- `stop()`: SIGTERM mining loop, then gateway. Bounded waits, SIGKILL fallback.
- `is_running()`: check sidecar + both PIDs.
- `stats()`: read from `pearl-gateway`'s `:8339/metrics` exactly as Spec A §8.1 specifies. **Same metrics adapter contract.** No code changes in OJ's gateway-metrics adapter.
### 13.6 Configuration
Inherits Spec A's `[mining]` config schema unchanged. v1 uses:
```toml
[mining]
provider = "cpu-pearl" # NEW: was "vllm-pearl" in Spec A
wallet_address = "prl1q..."
submit_target = "solo"
fee_bps = 0
fee_payout_address = ""
[mining.extra]
gateway_port = 8337
metrics_port = 8339
pearld_rpc_url = "http://localhost:44107"
pearld_rpc_user = "rpcuser"
pearld_rpc_password_env = "PEARLD_RPC_PASSWORD"
# v1-specific: matmul shape for the search loop
m = 256
n = 128
k = 1024
rank = 32
```
The `m / n / k / rank` shape can be tuned per Phase 0-A measurement (or per chip). Larger shapes search more space per call but use more memory.
### 13.7 Doctor surface (Apple Silicon)
```
$ jarvis mine doctor
Hardware
GPU vendor apple ✓
Apple chip M2 Max ✓
Unified memory 96 GB ✓
Pearl install
py-pearl-mining 0.1.0 (cp312-abi3-macos-arm64) ✓
miner-base 0.1.0 ✓
pearl-gateway 0.1.0 ✓
Pearl node
RPC http://localhost:44107 ✓
Auth ok ✓
Block height 442107 (synced) ✓
Wallet
Address format prl1q... ✓
Provider capability
cpu-pearl SUPPORTED (est. 0.X share/h on M2 Max)
Notes
- This is decoupled mining: your normal LLM inference is unaffected
- Hashrate is far below H100 mining; see docs/user-guide/mining-apple-silicon.md
- Metal-accelerated mining: planned for v2; not available yet
Session
Sidecar absent (not running)
```
Each row maps to a check function in `mining/_discovery.py`. The "est. share/h" line is populated from a one-time calibration during `mine init` — runs `pearl_mining.mine` in a 30-second loop and extrapolates.
### 13.8 v1 anti-goals
- **No coupling to inference.** The user's MLX-LM / Ollama / llama.cpp inference is untouched. v1 does not introduce a custom matmul. The "use AI = mine" narrative is **explicitly deferred to v2**.
- **No Metal kernel.** All math is in upstream Rust + PyTorch + Python. Zero MSL written.
- **No Pearl tree changes.** We consume their published packages; we contribute zero code into Pearl's repo for v1. (Phase 0-A discussion still happens — but it's lower-stakes since we're a downstream consumer in v1, not a contributor.)
- **No upstream PRs blocking v1 ship.** v1 ships against the Pearl ref pinned in `mining/_constants.py` (Spec A §6) regardless of whether any of our coordination questions are answered.
### 13.9 v1 exit criteria
- [ ] `mining-pearl-cpu` extra installs cleanly on macOS arm64 (M1, M2, M3, M4 — at minimum the chip the spec author owns)
- [ ] `jarvis mine init` completes successfully on macOS arm64
- [ ] `jarvis mine start` launches gateway + miner subprocesses; sidecar valid; `mine status` reports live data
- [ ] `mine doctor` produces honest, actionable output for Mac users
- [ ] At least one block found on Pearl testnet from at least one Apple Silicon variant
- [ ] User-facing doc `docs/user-guide/mining-apple-silicon.md` ships, including the honest hashrate caveat
### 13.10 Out of v1, into v2/v3
- **v2 (months):** Re-route `noisy_gemm` math to PyTorch-MPS for Apple Silicon GPU acceleration; integrate as a plugin into MLX-LM or `llama-cpp-python` so inference matmuls produce mining work (preserving the "use AI = mine" narrative). The original §5–§8 plan applies, swapped to use PyTorch MPS instead of raw MSL.
- **v3 (months — optional, only if v2 perf is insufficient):** Native Metal Shading Language NoisyGEMM kernel as an upstream Pearl contribution. The original §5–§8 plan applies as written.
## 14. Phase 0 deliverables status (this session, 2026-05-05)
Tracking what was actually produced, against the §5 Phase 0 plan and the §1.5 reframing.
| Workstream | Original plan | Status | Deliverable |
|---|---|---|---|
| P0-A | Open Pearl GitHub Discussion, get protocol-acceptance confirmation | Draft written; user posts | `docs/design/2026-05-05-pearl-coordination-discussion-draft.md` |
| P0-B | Build reference oracle from scratch in PyTorch, validate against H100 CUDA | **Reference oracle exists upstream.** Built thin OJ-side wrapper + verified empirically that `pearl_mining.mine` runs on Apple Silicon (78 ms / proof at test difficulty) | `tools/pearl-reference-oracle/` |
| P0-C | Decide MLX vs llama.cpp Metal | **Deferred to v2.** v1 doesn't need either. | — |
| Spec update | Capture findings | Done | This document, §1.5, §10–§14 |
| v1 implementation plan | Plan written via `superpowers:writing-plans` after Phase 0 | Pending | `2026-05-05-apple-silicon-pearl-mining-plan-v1.md` (next deliverable) |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,91 @@
# Pearl coordination thread — draft
**For:** Posting on `pearl-research-labs/pearl` GitHub Discussions (Category: General / Q&A).
**By:** OpenJarvis team (Stanford Hazy Research); contact: [user fills in].
**Status:** Draft — review and edit before posting.
---
## Suggested title
> Apple Silicon support for Pearl mining — coordination & confirmation
## Suggested body
Hi Pearl team — we're [OpenJarvis](https://github.com/open-jarvis/OpenJarvis), a local-first personal AI agent framework from Stanford Hazy Research. We're working on a `mining` subsystem that lets OJ users mine Pearl through the agent framework. The first integration is the `vllm-miner`-on-H100/H200 path, which is straightforward. The second is Apple Silicon, where the situation is more interesting and we'd like to confirm a few things before we ship.
We have a v1 architecture that ships **today** using only your published Python packages (`py-pearl-mining`, `miner-base`, `pearl-gateway`) without any new code in your tree, plus an aspirational v2/v3 path that does involve potentially upstream contributions. Three asks below, plus a heads-up.
### What we built and verified locally (no protocol changes; all upstream code paths)
We read the Pearl source carefully — particularly:
- `zk-pow/src/api/verify.rs` — the validator
- `zk-pow/src/ffi/mine.rs` — the pure-Rust `mine()` function
- `zk-pow/src/circuit/pearl_noise.rs` — noise generation
- `py-pearl-mining/` — the PyO3 bindings exposing the above to Python
- `miner/miner-base/src/miner_base/noisy_gemm.py` — the PyTorch NoisyGEMM reference
…and then we built `py-pearl-mining` from source on an Apple Silicon M2 Max (macOS 26.4, Python 3.12, Rust 1.94). It produced `py_pearl_mining-0.1.0-cp312-abi3-macosx_11_0_arm64.whl` in ~56 seconds. We installed it and ran the `mine()` + `verify_plain_proof()` cycle from `tests/test_python_api.py`:
```
running mine(m=256, n=128, k=1024, rank=32) on Apple Silicon CPU…
mine() returned a proof in 0.078s
verify_plain_proof: ok=True, msg='Mining solution verified successfully'
```
So our v1 plan is: ship a CPU-mining mode for OJ users on Apple Silicon (and potentially other non-CUDA platforms) that wraps `pearl_mining.mine()` and your `pearl-gateway` as a subprocess. **We're not modifying anything in Pearl's tree for v1.** Just consuming what you've already published.
### Three asks
**1. Protocol acceptance confirmation.**
Reading the validator path, we believe `verify_block` and `verify_plain_proof` accept any `PlainProof` produced by a correct implementation, regardless of which hardware produced it. The plonky2 STARK and the difficulty check don't reference hardware.
**Could you confirm in writing that blocks mined via the pure-Rust `mine()` path (from a non-CUDA host like Apple Silicon) will be accepted by Pearl validators on testnet and mainnet?** We don't expect surprises here, but it's load-bearing for our spec and we want to record your sign-off before we ship.
**2. Heads-up: your `Taskfile.yml` restricts `build:miner` to `[linux, windows]`.**
That makes total sense for the GPU miner (CUDA + vLLM is Linux-only). But the `py-pearl-mining` and `miner-base` packages don't actually need that restriction — they install fine on macOS. We're working around the gate by installing the individual packages directly. Two questions:
- Is the `[linux, windows]` restriction load-bearing in some way we don't see (e.g., do you intend `py-pearl-mining` to remain a CUDA-bound dependency long-term)?
- Would you be open to a small PR that splits `build:miner-cpu` (cross-platform) from `build:miner-gpu` (Linux + CUDA)? It would help downstream consumers like us — and any hobbyist who wants to experiment with `pearl_mining.mine()` on whatever hardware they own.
**3. PyPI publication of `py-pearl-mining` / `miner-base` / `pearl-gateway`.**
Do you have a roadmap for publishing these as PyPI wheels (`pip install py-pearl-mining` etc.)? Today we'd vendor a pinned commit and `maturin build` locally, which works but is brittle. If a 2026 PyPI publication is plausible, we'd defer the local-build code path; if it's not on the roadmap, we'll plan for the long-term local-build path.
### Aspirational (v2 / v3) — context only, no asks yet
Once v1 ships, we'd like to explore Apple-native acceleration:
- **v2:** Use PyTorch MPS to GPU-accelerate `miner-base.NoisyGemm` on Apple Silicon. Could potentially become a plugin into `mlx-lm` or `llama-cpp-python` so a Mac user's *inference* matmuls do mining work — same "useful work" framing as your vllm-miner. We don't need anything from Pearl for this; we'd build it on top of your existing PyTorch reference.
- **v3 (only if v2 isn't enough):** A native Metal Shading Language port of NoisyGEMM, paralleling `pearl-gemm/`. That would be a real upstream contribution candidate (`pearl/miner/pearl-gemm-metal/`), and we'd want to coordinate with you before starting kernel work to avoid duplicate effort.
If you're already building Apple Silicon support internally (or have someone planning it), please tell us — we'd rather coordinate than duplicate.
### Logistics
- License compatibility: Pearl is ISC; OpenJarvis is Apache-2.0. We don't see any conflict for either consumption (v1) or contribution (v3), but please flag if you do.
- CLA: do you require one for upstream contributions? Not blocking v1 — just want to know for v3.
- Preferred coordination channel: this Discussion thread, a Discord, an email? We're happy to use whatever works for you.
Thanks for building this — Proof-of-Useful-Work via matmul is genuinely interesting and we're excited to bring more (slower!) hardware to the network.
— [user name], on behalf of OpenJarvis
---
## Notes for the user before posting
- Replace `[user fills in]` with your contact info, `[user name]` with your name.
- The architecture/perf claims are all backed by code + an actual local build; you can stand behind them.
- "Heads-up" framing on the `Taskfile.yml` is intentional — we're not asking them to *change* it, just flagging the friction point in case they want to.
- Don't post until OJ Spec A is at least branch-pushed (which it is, PR #310) — it gives Pearl a way to see the broader integration we're building.
- When their reply lands, update Spec B §10 (open questions 1, 5, 7) and §11 (cross-references → coordination thread URL).
## Possible Pearl responses to anticipate
- **Best case:** "Confirmed, looks great, we don't have an Apple Silicon plan, please do it." — proceed with §13.
- **Middle case:** "Confirmed, but we have a Metal port in flight." — coordinate, share Spec B §6.1, decide upstream-vs-fork. v1 (CPU) is unaffected.
- **Worst case:** "We'd prefer downstream non-CUDA mining stay disabled for now." — unlikely given their `pearl-gateway` README explicitly anticipates "plugins for other LLM inference libraries", but if it happens, this becomes a much harder problem and we'd need to revisit.
@@ -0,0 +1,560 @@
# Spec A — vLLM-Pearl mining integration (v1)
| | |
|---|---|
| **Date** | 2026-05-05 |
| **Status** | Design — pending implementation plan |
| **Owner** | OpenJarvis team |
| **Companion spec** | [Spec B — Apple Silicon enablement](2026-05-05-apple-silicon-pearl-mining-design.md) (separate effort, runs in parallel) |
| **Repos referenced** | `OpenJarvis` (this repo), `pearl-research-labs/pearl` |
## 1. Summary
Add a new sibling subsystem `openjarvis.mining` that lets users run [Pearl](https://github.com/pearl-research-labs/pearl) Proof-of-Useful-Work mining as a property of their local LLM inference. v1 ships solo mining for users who already have an H100/H200 and run vLLM — the only configuration Pearl's reference miner currently supports. The architecture leaves three deliberate seams for v2 (pool support + a 20% OJ fee) and is engine-agnostic by construction so Apple Silicon, AMD, Ollama, llama.cpp, and MLX paths plug in via the registry without a rewrite when Pearl ships the matching plugins.
The narrative thesis: Pearl's `vllm-miner` is a vLLM plugin that swaps quantized linear ops with `NoisyGEMM`, a CUDA kernel that produces both the correct matmul output *and* a PoW commitment. Mining IS inference. For an OJ user already serving prompts on a powerful local GPU, this is a way to capture economic value from compute they were going to do anyway — directly aligned with OJ's Intelligence-Per-Watt thesis rather than against it.
## 2. Scope
### In scope (v1)
- New `openjarvis.mining` subsystem with `MiningProvider` ABC, `MinerRegistry`, `MiningCapabilities` / `MiningConfig` / `MiningStats` dataclasses
- `vllm-pearl` provider implementation: orchestrate Pearl's published `vllm-miner` Docker container
- `[mining]` TOML section in OJ config; `MiningConfig` field in `JarvisConfig`
- New CLI namespace: `jarvis mine init|start|stop|status|doctor|attach|logs`
- Runtime sidecar at `~/.openjarvis/runtime/mining.json` for engine ↔ mining handoff
- Hybrid Docker image acquisition: pull-if-published, otherwise build from a pinned Pearl ref
- On-demand telemetry via Pearl gateway `:8339/metrics`; `mining_session_id` nullable column on telemetry inference rows
- v2 seams: `submit_target` tagged-union parsing, zero-valued `fee_bps` / `fees_owed` plumbing, reserved `mining/pools/` location
- Test strategy that doesn't require an H100 in CI
- Documentation: `docs/user-guide/mining.md`, `docs/development/mining.md`, `CLAUDE.md` paragraph, `REVIEW.md` bullet
### Out of scope (v1) — deferred or owned elsewhere
- Pool support and the 20% OJ fee mechanism (own spec, v2)
- Custody, signing, or routing Pearl funds (anti-goal — must remain zero in v1)
- Apple Silicon, AMD ROCm, sm89 (RTX 4090) NVIDIA, CPU, MLX, Ollama, llama.cpp, SGLang mining paths (Spec B for Apple; remaining hardware/engine paths blocked on Pearl)
- Wallet generation, Oyster integration, key custody (paste-only address)
- pearld lifecycle management (BYO node)
- Background telemetry collection in OJ's gateway daemon (v1.x; the hook point is reserved)
- Inference-quality drift detection (v1.x at earliest)
- `mine doctor --fix` automatic remediation (v1.x stub)
- Multi-GPU / multi-worker / multi-session per host (v2+)
## 3. Load-bearing decisions from brainstorming
These were the forks where the design could have gone several ways. Recorded so future-readers can audit reasoning rather than re-derive it.
| Decision | What we picked | Why |
|---|---|---|
| Target audience for v1 | H100/H200 owners running vLLM (Pearl's only working config today) | Anything broader is blocked on Pearl shipping non-CUDA / non-vLLM plugins. Power-user MVP ships in weeks; pool/fee/Apple are separate specs. |
| Mining model | Co-located: every inference through the Pearl-flavored vLLM is mining work | Matches Pearl's `vllm-miner` plugin design and OJ's Intelligence-Per-Watt thesis. Side-car deferred until Pearl ships plugins for engines users care about for non-mining inference. |
| Coupling to Pearl miner process | Wrap-and-launch via Docker | Pearl's Docker image (or Dockerfile) is the most stable contract they expose. (1) "BYO miner" is too thin to be a feature; (3) running Pearl's `uv` workspace natively couples us to their build system. |
| Module placement | Sibling top-level subsystem `mining/` (peer to `engine/`, `agents/`) | Matches OJ's existing module pattern. `MinerRegistry` is a peer registry. Future non-vLLM providers slot in identically. |
| Engine attachment | Runtime sidecar JSON at `~/.openjarvis/runtime/mining.json` | Existing vLLM engine class stays untouched. Sidecar is the single source of truth tying mining lifecycle to engine resolution. Inspectable via `cat`. |
| Config shape | Flat top-level `[mining]` TOML section | Only one provider in v1; nested per-engine config can grow later if multi-provider becomes real. |
| Wallet handling | Paste-only Pearl Taproot address | Keys are sensitive; Pearl's wallet RPC is unstable surface. v1.x can add Oyster integration once the contract stabilizes. |
| pearld | BYO; user points OJ at their own node | OJ doesn't orchestrate L1 nodes. Doctor surfaces unreachable cleanly. |
| Telemetry collection | On-demand reads in v1; persistent collector class shipped unwired (`MiningTelemetryCollector`) | Most users won't enable mining; daemon shouldn't grow surface for them. v1.x lights up the hook with zero API churn. |
| v1 fee/pool seams | Three seams: `submit_target` parsed (one variant works), `fee_bps`/`fees_owed` plumbed at zero, `mining/pools/` reserved | Cheap to leave; painful to retrofit. Does not pre-decide the v2 API. |
| Custody | **Anti-goal**: zero. v1 must not accept, sign, or route Pearl funds. | Avoids prematurely binding a legal/regulatory posture. v2 revisits as part of pool design. |
| Apple Silicon support | Not in v1. Designed-for via the `MiningProvider` ABC + `MiningCapabilities.detect()`. Spec B documents the enablement work. | The Pearl `pearl-gemm` kernel is heavily Hopper-bound (`sm_90a`, WGMMA, TMA, cluster mode, CUTLASS 3.x). A Metal port is real GPU-kernel engineering, not a config flag. |
## 4. Architecture & module layout
### 4.1 New module tree
```
src/openjarvis/mining/
__init__.py # soft-imports providers (try/except ImportError)
_stubs.py # MiningProvider ABC + dataclasses (MiningCapabilities, MiningConfig, MiningStats, SoloTarget, PoolTarget)
_discovery.py # detect_providers(hardware, engine, model) -> list[MiningCapabilities]
_docker.py # PearlDockerLauncher — shared Docker orchestration (image acquisition + container lifecycle)
_collector.py # MiningTelemetryCollector class — defined but UNWIRED in v1; lit up in v1.x
_constants.py # PEARL_REPO, PEARL_PINNED_REF, PEARL_IMAGE_TAG, OJ default tag
vllm_pearl.py # @MinerRegistry.register("vllm-pearl") — only impl in v1
pools/ # RESERVED for v2. Empty in v1 except for an __init__.py with a docstring saying so.
src/openjarvis/cli/
mine_cmd.py # jarvis mine init|start|stop|status|doctor|attach|logs
tests/mining/
__init__.py
conftest.py # mining-specific fixtures (synthetic HardwareInfo, sample Prometheus output)
fixtures/
gateway_metrics_sample.txt # captured Prometheus output from a real Pearl run
config_*.toml # golden TOML files
test_stubs.py
test_discovery.py
test_docker.py
test_collector.py
test_vllm_pearl.py
test_cli.py
```
### 4.2 Registry additions
`MinerRegistry` added to `src/openjarvis/core/registry.py` as a peer to `EngineRegistry`, `AgentRegistry`, etc. `tests/conftest.py`'s autouse `_clean_registries` fixture is updated to include `MinerRegistry.clear()`.
`mining/vllm_pearl.py` exposes idempotent `ensure_registered()`:
```python
def ensure_registered() -> None:
if not MinerRegistry.contains("vllm-pearl"):
MinerRegistry.register_value("vllm-pearl", VllmPearlProvider)
```
`mining/__init__.py` soft-imports `vllm_pearl` inside `try / except ImportError` and calls `ensure_registered()`. Standard OJ pattern.
### 4.3 Optional-deps extras
```toml
mining-pearl = ["docker>=7.0"] # v1 requires only the Docker SDK
# mining-pearl-mlx = [...] # future, owned by Spec B
# mining-pearl-rocm = [...] # future
```
### 4.4 The central ABC
```python
# src/openjarvis/mining/_stubs.py
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from openjarvis.core.config import HardwareInfo
@dataclass(slots=True)
class MiningCapabilities:
supported: bool
reason: str | None = None # human-readable: "needs sm90", "no Pearl plugin for engine ollama"
estimated_hashrate: float | None = None
@dataclass(slots=True)
class SoloTarget:
pearld_rpc_url: str
@dataclass(slots=True)
class PoolTarget:
url: str
worker_id: str | None = None
SubmitTarget = SoloTarget | PoolTarget
@dataclass(slots=True)
class MiningConfig:
provider: str # MinerRegistry key
wallet_address: str
submit_target: SubmitTarget # parsed from TOML "solo" / "pool:<url>"; v1 accepts only SoloTarget at runtime
fee_bps: int = 0 # v1: 0; v2: 2000 (=20%)
fee_payout_address: str | None = None # v1: ignored; v2: OJ's address
extra: dict = field(default_factory=dict)
@dataclass(slots=True)
class MiningStats:
provider_id: str
shares_submitted: int = 0
shares_accepted: int = 0
blocks_found: int = 0
hashrate: float = 0.0
uptime_seconds: float = 0.0
last_share_at: float | None = None
last_error: str | None = None
payout_target: str = "solo" # v2 reporting; "solo" in v1
fees_owed: int = 0 # v2 accounting hook; 0 in v1
class MiningProvider(ABC):
provider_id: str
@classmethod
@abstractmethod
def detect(cls, hw: HardwareInfo, engine_id: str, model: str) -> MiningCapabilities: ...
@abstractmethod
async def start(self, config: MiningConfig) -> None: ...
@abstractmethod
async def stop(self) -> None: ...
@abstractmethod
def is_running(self) -> bool: ...
@abstractmethod
def stats(self) -> MiningStats: ...
```
## 5. Config schema & engine attachment
### 5.1 TOML schema
```toml
[mining]
provider = "vllm-pearl" # MinerRegistry key
wallet_address = "prl1q..." # user's Pearl Taproot address (paste-only)
submit_target = "solo" # v1: "solo" only; "pool:<url>" raises NotImplementedError at start()
fee_bps = 0 # v1: 0; v2: 2000
fee_payout_address = "" # v1: ignored; v2: OJ's address
[mining.extra]
docker_image_tag = "openjarvis/pearl-miner:<pinned-ref>"
model = "pearl-ai/Llama-3.3-70B-Instruct-pearl"
gateway_port = 8337
gateway_metrics_port = 8339
vllm_port = 8000
gpu_memory_utilization = 0.9
max_model_len = 8192
pearld_rpc_url = "http://localhost:44107"
pearld_rpc_user = "rpcuser"
pearld_rpc_password_env = "PEARLD_RPC_PASSWORD" # name of env var, not the secret
hf_token_env = "HF_TOKEN" # name of env var
```
Secrets: env-var *names*, never literal values. Matches OJ's existing convention for cloud API keys.
### 5.2 JarvisConfig field
`core/config.py` adds:
```python
@dataclass(slots=True)
class JarvisConfig:
...
mining: MiningConfig | None = None
```
The TOML loader reads `[mining]`, parses `submit_target` into `SoloTarget | PoolTarget`, validates against the dataclass, surfaces unknown `extra` keys as warnings. Absent section → `mining = None` → zero behavior change.
### 5.3 Runtime sidecar
`~/.openjarvis/runtime/mining.json` (created on `mine start`, removed on `mine stop`):
```json
{
"provider": "vllm-pearl",
"vllm_endpoint": "http://127.0.0.1:8000/v1",
"model": "pearl-ai/Llama-3.3-70B-Instruct-pearl",
"gateway_url": "http://127.0.0.1:8337",
"gateway_metrics_url": "http://127.0.0.1:8339",
"container_id": "abc123...",
"wallet_address": "prl1q...",
"started_at": 1714867200
}
```
Sidecar deliberately omits all secrets and process IDs. `container_id` is the authoritative handle (Docker is the source of truth for liveness); `wallet_address` is captured for drift-detection (config-vs-runtime).
### 5.4 Engine handoff flow
1. `jarvis mine start``MinerRegistry.get("vllm-pearl").start(config)`.
2. `VllmPearlProvider.start()` calls `_docker.PearlDockerLauncher.start(config)` and writes the sidecar.
3. `engine/_discovery.py` checks for `mining.json` on every engine lookup. When present, it auto-registers a `vllm` engine instance pointing at `vllm_endpoint`, named `vllm-pearl-mining`, marked default for mining-aware operations.
4. `jarvis ask` and the SDK route to that endpoint transparently. The user's normal inference is the mining work.
The vLLM engine class itself (`engine/openai_compat_engines.py`) is **not modified**. The change to `engine/_discovery.py` is small and additive: it inspects for `mining.json` and registers a derived `vllm` instance pointing at the mining endpoint when the sidecar is present. Absent sidecar → unchanged discovery behavior.
### 5.5 Manual mode
Power users running their own Pearl container skip `jarvis mine start` and write the sidecar themselves via `jarvis mine attach --vllm-endpoint=... --gateway-url=...`. Decouples lifecycle from wiring.
## 6. CLI surface, lifecycle & daemon integration
### 6.1 Subcommands
| Command | Purpose |
|---|---|
| `jarvis mine init` | Interactive: hardware/Docker checks, prompt for wallet + pearld credentials, write `[mining]`, pull/build image. Does NOT start mining. Pre-checks `>=200 GB` free disk. |
| `jarvis mine start` | Launch container via the registered provider, write sidecar, print endpoint info. Idempotent if running. |
| `jarvis mine stop` | Stop container, remove sidecar. Idempotent if not running. |
| `jarvis mine status` | Read sidecar + query gateway `:8339/metrics`. Print `MiningStats`. |
| `jarvis mine doctor` | Capability matrix; every check ✓/✗ with reason. Works in any state. |
| `jarvis mine attach` | Manual mode: write sidecar without launching. |
| `jarvis mine logs [-f]` | Tail container logs through Docker SDK. |
### 6.2 Doctor output (canonical example)
```
$ jarvis mine doctor
Hardware
GPU vendor nvidia ✓
Compute capability sm_90a ✓
VRAM 80 GB ✓ (need ≥ 70 GB for Pearl 70B)
Docker
Daemon running 24.0.7 ✓
GPU runtime nvidia-container-toolkit ✓
Disk
Free in HF cache 312 GB ✓ (need ≥ 200 GB)
Image
openjarvis/pearl-miner:<ref> present (built 2026-04-30) ✓
Pearl node
RPC http://localhost:44107 ✓
Auth ok ✓
Block height 442107 (synced) ✓
Wallet
Address format prl1q... ✓
Provider capability
vllm-pearl SUPPORTED
Session
Sidecar absent (not running)
Container —
```
Each row maps to one check function in `mining/_discovery.py`. Failures print actionable reasons (e.g. `✗ reason: needs sm90, you have sm89 (RTX 4090)`).
### 6.3 Lifecycle states
```
NOT_CONFIGURED → CONFIGURED → STARTING → RUNNING ⇄ STOPPING → STOPPED
FAILED
```
State derivation rules (no separate state file — derived from config + sidecar + container introspection):
- `NOT_CONFIGURED` — no `[mining]` in config
- `CONFIGURED` — config present, no sidecar
- `STARTING` — sidecar with `started_at` < ~30 s ago, container exists but gateway not yet healthy
- `RUNNING` — sidecar present, container running, gateway responding
- `FAILED` — sidecar present, but container exited or gateway failing > threshold
- `STOPPING``mine stop` invoked, Docker stop in progress
- `STOPPED``mine stop` complete, sidecar removed
### 6.4 Daemon integration: deliberately minimal in v1
- Docker handles container restart via `--restart=unless-stopped`. OJ does not babysit.
- Existing `com.openjarvis.gateway` daemon is unchanged.
- v1.x hook: `MiningTelemetryCollector` (already shipped in v1, unwired) can be added to the gateway as a 30-second-tick async task.
- launchd/systemd installation surface (`jarvis daemon install`) untouched.
### 6.5 Concurrency
POSIX `flock` on `~/.openjarvis/runtime/mining.lock` prevents racing `mine start` invocations.
### 6.6 `jarvis ask` UX hint
When `[mining]` is configured but no sidecar exists, `cli/hints.py` emits one line: `"mining configured but not running — start it with \`jarvis mine start\`"`. One-line UX nudge, no new infrastructure.
## 7. Pearl Docker integration
### 7.1 Realities from inspecting Pearl's repo
- **Build context = entire Pearl monorepo.** Dockerfile copies root `pyproject.toml`/`uv.lock`, `miner/`, `pearl-blake3/`, `py-pearl-mining/`, `zk-pow/`, `plonky2/`. Building requires the full repo.
- **Pearl publishes no registry image as of writing.** README documents only `docker buildx build -t vllm_miner . -f miner/vllm-miner/Dockerfile`.
- **Single container, three ports.** `entrypoint.sh` launches `pearl-gateway` in the background, waits on `:8339/metrics`, then `exec`s `vllm serve`. Ports: `8000` (vLLM), `8337` (miner RPC), `8339` (gateway metrics).
- **Pinned stack inside the image.** CUDA 12.9.1, vLLM 0.20.0+cu129, Python 3.12, `compute_90a/sm_90a`. Set by Pearl, not by us.
- **First-launch cost.** vLLM pulls the 70 B model from HF on first serve (~140 GB). Build itself is 3060 min on first init.
### 7.2 Hybrid image acquisition
| Mode | Behavior | When |
|---|---|---|
| **Pre-built pull** | OJ `docker pull`s the configured tag if it resolves in a registry | Default once Pearl publishes; users with private registry; CI |
| **Build-from-pin** | OJ git-clones Pearl at a pinned ref into `~/.openjarvis/cache/pearl/`, then `docker buildx build` | v1 default (Pearl publishes nothing today) |
| **BYO image** | User sets `mining.extra.docker_image_tag` to an image they built/pulled themselves | Power users, air-gapped envs |
Selection logic in `_docker.PearlDockerLauncher.ensure_image()`:
1. If `docker_image_tag` resolves locally → use it.
2. Else `docker pull <tag>` → on success, use it.
3. Else if `tag == OJ_DEFAULT_TAG`, fall back to clone-and-build from `PEARL_PINNED_REF`.
4. Else fail with a clear error pointing at `mine doctor`.
### 7.3 Pearl version pinning
`mining/_constants.py`:
```python
PEARL_REPO = "https://github.com/pearl-research-labs/pearl.git"
PEARL_PINNED_REF = "<sha-or-tag>" # bumped per OJ release after rev-testing
PEARL_IMAGE_TAG = f"openjarvis/pearl-miner:{PEARL_PINNED_REF}"
```
OJ release notes call out the Pearl ref shipped. Bumping the ref is its own PR with a documented Pearl-rev workflow.
### 7.4 Container launch shape
Via `docker>=7.0` SDK in `_docker.PearlDockerLauncher.start()`:
```python
container = client.containers.run(
image=PEARL_IMAGE_TAG,
command=[
config.extra["model"],
"--host", "0.0.0.0",
"--port", str(config.extra["vllm_port"]),
"--gpu-memory-utilization", str(config.extra["gpu_memory_utilization"]),
"--enforce-eager",
"--max-model-len", str(config.extra.get("max_model_len", 8192)),
],
name="openjarvis-pearl-miner",
detach=True,
auto_remove=False,
restart_policy={"Name": "unless-stopped"},
device_requests=[ DeviceRequest(count=-1, capabilities=[["gpu"]]) ],
shm_size="8g",
network_mode="host",
volumes={
str(Path.home() / ".cache/huggingface"): {
"bind": "/root/.cache/huggingface",
"mode": "rw",
},
},
environment={
"PEARLD_RPC_URL": config.extra["pearld_rpc_url"],
"PEARLD_RPC_USER": config.extra["pearld_rpc_user"],
"PEARLD_RPC_PASSWORD": os.environ[config.extra["pearld_rpc_password_env"]],
"PEARLD_MINING_ADDRESS": config.wallet_address,
"HF_TOKEN": os.environ.get(config.extra.get("hf_token_env", "HF_TOKEN"), ""),
"MINER_RPC_TRANSPORT": "tcp",
},
)
```
### 7.5 Trade-offs called out
- **`network_mode="host"`** because pearld's RPC at `http://localhost:44107` lives on the host. A user-defined Docker network adds setup steps with no real isolation benefit on a single-tenant miner box. Pragmatism > purity. Note: host networking has Linux semantics; macOS/Windows Docker handle it differently. Acceptable for v1 since H100/H200 + nvidia-container-toolkit constrains the deployment to Linux anyway.
- **`auto_remove=False`** so a crashed container stays around for `jarvis mine logs` post-mortem.
- **HF cache mounted from host.** 140 GB weight download is one-time, survives container restarts, visible to other tools.
- **Secrets via env-var names**, never persisted in the container image, the sidecar, or Docker labels.
### 7.6 Wallet handling boundary
OJ never sees Pearl mnemonic seeds, never imports Oyster keys, never signs Pearl transactions. Only Pearl-secret OJ touches is the pearld RPC password (passed through container env, sourced by name from host env). Mining address is public — fine in plaintext config.
### 7.7 Image lifecycle UX
- `jarvis mine init` triggers `ensure_image()`, streams build/pull output through the CLI with a clear time estimate (`"Building Pearl miner image — first run takes ~45 min on a fast machine"`).
- `jarvis mine doctor` reports `image: present (tag, age, sha)` or `image: missing (run mine init)`.
- `jarvis mine prune` (v1.x) cleans old `openjarvis/pearl-miner:*` tags. Manual `docker image rm` works in v1.
## 8. Telemetry hooks & v2 fee/pool seams
### 8.1 Telemetry — read surface
Pearl's container exposes `:8339/metrics` (Prometheus exposition format). v1 reads only this endpoint. Deeper RPC introspection via `:8337` deferred to v2.
### 8.2 Adapter and metric mapping
`mining/vllm_pearl.py::_parse_gateway_metrics()` translates Prometheus lines to `MiningStats`. Metric names are TBD on implementation — verified against captured fixture `tests/mining/fixtures/gateway_metrics_sample.txt`. Expected mapping (fallback: zero-fill any missing field, log a one-shot warning):
| `MiningStats` field | Likely Pearl metric (verify on implementation) |
|---|---|
| `shares_submitted` | `pearl_gateway_shares_submitted_total` |
| `shares_accepted` | `pearl_gateway_shares_accepted_total` |
| `blocks_found` | `pearl_gateway_blocks_found_total` |
| `hashrate` | derived rate of `shares_submitted_total` |
| `uptime_seconds` | `process_start_time_seconds` |
| `last_share_at` | `pearl_gateway_last_share_timestamp` |
| `last_error` | derived from `pearl_gateway_errors_total` deltas |
### 8.3 Collection cadence
- **v1: on-demand only.** `jarvis mine status` makes one HTTP GET per call (~10 ms). No background polling.
- **v1.x: `MiningTelemetryCollector` lit up in the gateway daemon.** The class is shipped in v1 but unwired. v1.x adds a periodic asyncio task; same `MiningStats` schema, same gateway endpoint. Zero API churn.
### 8.4 Intelligence-Per-Watt extension
The `telemetry/store.py` schema gains a nullable `mining_session_id` column on inference rows:
- Tagged when an inference goes through the Pearl-mining endpoint; null otherwise.
- Untagged rows behave exactly as today — zero impact on the non-mining path.
- `jarvis telemetry stats --mining` (v1.x) joins to the latest `MiningStats` snapshot and reports `tokens / share`, `joules / share`, `est. PRL / kWh`.
v1 ships the column and the no-op join path. v1.x lights up the reporting. This is the metric the IPW thesis genuinely cares about.
### 8.5 v2 fee/pool seams (three concrete, no more)
**1. `submit_target` parsed into a tagged union; only one variant works.** `SoloTarget` accepted at runtime in v1; `PoolTarget` raises `NotImplementedError("pool support is v2 — track openjarvis#XYZ")`. Reachable only by users who edit their config to opt in.
**2. `fee_bps` / `fee_payout_address` plumbed; zero-valued in v1.** `MiningStats.fees_owed = 0` and `MiningStats.payout_target = "solo"` always in v1. Schema is real; values are zero. No migration in v2.
**3. `mining/pools/` module location reserved.** Empty in v1 except for an `__init__.py` whose docstring says the location is reserved for v2 `PoolClient` work. The v1 spec **does not** define a `PoolClient` ABC — predicting the v2 API precisely creates migration debt. The v2 spec writes against an empty slot.
### 8.6 What v1 deliberately does not lock in
- Pool protocol (PPLNS / PPS / SOLO+ / custom)
- Custody model (escrow / trustless split-coinbase / settlement contract)
- OJ pool URL, share format, share difficulty
- KYC / TOS / payout thresholds
### 8.7 Custody anti-goal
v1 must not introduce any code path where OJ accepts custody of, signs, or routes Pearl funds. Closest v1 comes is reading `wallet_address` (public) and passing it through to the container. v2 revisits.
### 8.8 Single-session assumption (called out, not seamed)
v1 assumes one mining session per host (one sidecar). Multi-GPU / multi-worker fanout is v2+. Sidecar would become a list or directory.
## 9. Failure handling & test strategy
### 9.1 Principles
1. **Fail loud, don't auto-heal.** Docker handles container restarts; `mine doctor` surfaces what's wrong. OJ does not retry mining work, restart pearld, or paper over crashes.
2. **`mine doctor` is the canonical failure surface.** Every failure mode below maps to one or more rows in doctor output.
3. **Sidecar is authoritative; config is intent.** Drift surfaces as a warning, not a crash.
### 9.2 Failure mode matrix
| Failure | v1 behavior | Surface |
|---|---|---|
| Image missing | `mine start` errors with "run `mine init` to build/pull" | `mine doctor: image: missing` |
| GPU not reachable in container | Docker error with `nvidia-container-toolkit` hint | `mine doctor: docker.gpu_runtime: ✗` |
| Disk too low | `mine init` pre-checks `shutil.disk_usage`; errors if < 200 GB free | `mine doctor: disk_free: ✗` |
| vLLM model load fails (HF auth, OOM, model not found) | Container exits; `mine status` reports `FAILED` with `last_error` from `docker logs` tail | `mine status` + `mine logs` |
| pearl-gateway can't reach pearld | `:8339/metrics` exposes the error; adapter populates `MiningStats.last_error` | `mine status: last_error` |
| Container crashes mid-run | Docker `--restart=unless-stopped` restarts; `mine status` shows brief `STARTING` → `RUNNING` | self-healing, logged |
| Stale sidecar (container died, sidecar not cleaned) | `mine start` validates `container_id`; if Docker says it's gone, removes sidecar and proceeds | one-line warning |
| Concurrent `mine start` | POSIX `flock` on `~/.openjarvis/runtime/mining.lock`; second invocation errors clearly | clear message |
| Already-running `mine start` | Idempotent: detect via sidecar + container introspection, print status, exit 0 | informational |
| Wallet/config drift | Sidecar carries wallet from start time; `mine status` cross-checks and warns on mismatch | warning, not auto-restart |
| User edits `submit_target = "pool:..."` in v1 | `start()` raises `NotImplementedError` with tracking issue link | clear error |
| Pearl protocol upgrade (block format / metric names change) | Adapter zero-fills with one-shot warning. `mine doctor` does a best-effort check: it reads the `image: openjarvis/pearl-miner:<ref>` Docker label and compares against `PEARL_PINNED_REF` baked into the OJ release; mismatch surfaces a warning. **OJ does not poll Pearl's GitHub at runtime.** | warning + spec'd Pearl-rev workflow |
| Inference quality regression from NoisyGEMM | **Out of v1 scope to detect.** Documented risk; v1.x may add automated drift detection. | docs only |
### 9.3 Test strategy
Hard constraint: **OJ's CI has no H100, no GPU, no Pearl image, no pearld.** Almost everything must be testable without those.
| Layer | Pattern | Marker | Runs in CI? |
|---|---|---|---|
| `MiningCapabilities.detect()` matrix | Pure unit, parametrized over synthetic `HardwareInfo` | unmarked | yes |
| `MiningConfig` parsing (TOML → dataclass, including `submit_target` tagged-union) | Unit, golden TOML fixtures | unmarked | yes |
| Docker launch shape | `unittest.mock.patch("docker.from_env")`; assert `containers.run(...)` kwargs | unmarked | yes |
| Gateway metrics adapter | `tests/mining/fixtures/gateway_metrics_sample.txt`, parse + assert `MiningStats` | unmarked | yes |
| Sidecar lifecycle (write/read/stale-cleanup, `flock` acquisition) | `tmp_path`, real filesystem, real `flock` | unmarked | yes |
| CLI smoke | Click `CliRunner`, mocked `MiningProvider` | unmarked | yes |
| Container start/stop with real Docker daemon | Real Docker, swap Pearl image for tiny stub `alpine`-based image opening the right ports | new `docker` marker | optional in CI |
| End-to-end mining (real container, real pearld, real shares) | Real H100 + pearld testnet + pinned Pearl image | `live and nvidia and slow` | **no** — manual pre-release smoke |
**New pytest marker.** `docker` registered alongside `live`, `cloud`, `nvidia`, etc. in `pyproject.toml`. CI matrix optionally runs `-m "docker and not live"` on a Docker-enabled runner.
**Conftest hygiene.** `tests/conftest.py`'s autouse fixture clears `MinerRegistry`. `mining/__init__.py`'s `ensure_registered()` survives the autouse clear via `MinerRegistry.contains(...)`.
**Captured Prometheus fixture.** Real metrics output from a Pearl gateway run, committed to the repo. Pins the metric-name assumptions and is the canary if Pearl renames metrics.
### 9.4 What v1 deliberately does not test
- Mining throughput/economics on a real H100 (Pearl's CI tests their kernels)
- Inference quality drift from NoisyGEMM (out of v1 scope)
- Pool share submission paths (v2 spec)
- Apple Silicon paths (Spec B)
## 10. Documentation deliverables (part of this spec)
- `docs/user-guide/mining.md` — user-facing: prerequisites, init flow, doctor output reading guide, `mine status` interpretation, deliberately-unsupported list (Mac, AMD, sm89, non-vLLM engines)
- `docs/development/mining.md` — for contributors: `MiningProvider` ABC, registry pattern, how to add a new provider (Spec B is the canonical worked example)
- One paragraph in `CLAUDE.md` under "Architecture" pointing future-Claude at `mining/` as a sibling subsystem with its own optional-deps discipline
- `REVIEW.md` gets a new bullet under registry compliance specifically calling out `MinerRegistry`
## 11. Open items to resolve at implementation time
1. **Pearl gateway metric names.** Verify the actual exposition labels by capturing `:8339/metrics` from a running Pearl gateway. Update the adapter mapping and commit the fixture.
2. **`PEARL_PINNED_REF`.** Pick a specific commit/tag at the start of implementation. Document the rev-bump workflow.
3. **The `pearl-ai/Llama-3.3-70B-Instruct-pearl` HF model.** Confirm it exists and is gated/ungated; document HF auth requirements.
4. **Pearl Taproot address regex.** Confirm the prefix and length for `mine doctor`'s address-format check.
5. **Pearl `:8337` miner RPC TCP port behavior.** Confirm `MINER_RPC_TRANSPORT=tcp` works as documented and binds to `0.0.0.0` not just `127.0.0.1` inside the host network namespace.
6. **OJ default Docker image tag.** Decide whether to publish to GHCR/Docker Hub once we have a build, or leave users on build-from-pin. Likely v1.x.
7. **Wallet generation hand-off (v1.x).** Decide whether `mine init` shells out to Pearl's `oyster` for users who want guidance, or stays paste-only.
8. **Telemetry schema migration approach.** Adding the nullable `mining_session_id` column to `telemetry/store.py` is a SQLite schema change. Decide between (a) `ALTER TABLE` on first start with a guarded `PRAGMA user_version` bump, (b) per-query `try/except` on the column, or (c) creating a sidecar table joined on inference id. Confirm what convention OJ already uses for `telemetry/` schema evolution before picking; default lean is (a).
## 12. Cross-references
- **[Spec B — Apple Silicon enablement](2026-05-05-apple-silicon-pearl-mining-design.md)** — separate effort tracking the Pearl-side and OJ-side work to make Apple Silicon a registered `MiningProvider`. Spec A is engine-agnostic by design; Spec B drops in via `MinerRegistry` without modifying anything in this spec.
- **Pearl repo:** [`pearl-research-labs/pearl`](https://github.com/pearl-research-labs/pearl) — referenced sub-paths: `miner/vllm-miner/`, `miner/pearl-gemm/`, `miner/pearl-gateway/`, `miner/vllm-miner/Dockerfile`, `miner/vllm-miner/entrypoint.sh`.
- **Pearl paper:** [Proof-of-Useful-Work via matrix multiplication (arXiv:2504.09971)](https://arxiv.org/abs/2504.09971).
- **OJ contributing guide:** `docs/development/contributing.md` — registry pattern, `_stubs.py` / `_discovery.py` conventions, `ensure_registered()` discipline, optional-deps soft-import pattern. All followed in this spec.
## 13. Implementation plan
The implementation plan for Spec A is a separate document, written via the `superpowers:writing-plans` skill after this design is approved by the user. It will decompose section 49 above into ordered, independently-reviewable steps and call out which steps can be parallelized.
File diff suppressed because it is too large Load Diff
-1
View File
@@ -413,4 +413,3 @@ policy:
if the component requires new packages
See the [registry pattern](#registry-pattern) section above for complete examples.
@@ -0,0 +1,289 @@
# NVIDIA Pearl Mining Validation Runbook
This runbook is the release gate for the v1 `vllm-pearl` provider. Unit tests
prove OpenJarvis wiring; this validates that a real H100/H200 host can mine
through Pearl and serve inference through OpenJarvis.
## Required Host
Run this on a Linux machine with:
- NVIDIA H100 or H200 GPU, compute capability 9.0, at least 70 GB VRAM
- Current NVIDIA driver with CUDA container support
- Docker 24+ and `nvidia-container-toolkit`
- At least 200 GB free disk
- Reachable `pearld` JSON-RPC endpoint
- Pearl payout address beginning with `prl1q` or `prl1p`
- Hugging Face access to `pearl-ai/Llama-3.3-70B-Instruct-pearl`
The validated H100 configuration uses `gpu_memory_utilization = 0.96` with
`max_model_len = 8192`. Lower memory utilization can fail during vLLM startup
because the Pearl 70B mining model leaves too little KV cache at 8k context.
Do not run this on macOS, Apple Silicon, AMD, RTX 4090, or CPU-only hosts.
Those are separate providers.
## Wallet Address Setup
Create the wallet from the Pearl repo root:
```bash
./bin/oyster -u rpcuser -P rpcpass --create
```
If you choose the optional public-data encryption prompt, Oyster will require
that public passphrase on startup via `--walletpass`. Keep private and public
passphrases out of shell history where possible.
Start Oyster:
```bash
./bin/oyster \
-u rpcuser \
-P rpcpass \
--walletpass '<public-wallet-passphrase-if-configured>' \
&
```
Then generate a mining address through the wallet RPC:
```bash
./bin/prlctl \
--wallet \
--skipverify \
-u rpcuser \
-P rpcpass \
-s localhost:44207 \
getnewaddress
```
Notes:
- `--wallet` is required. Without it, `prlctl` talks to `pearld` instead of
Oyster and may look for `Pearld/pearld.conf`.
- Use `-s localhost:44207`, not `-s https://localhost:44207`. `prlctl` expects
host and port, not a URL.
- `--skipverify` is acceptable for this local validation flow unless you have
configured the Oyster RPC certificate path.
- If a mnemonic has been pasted into logs, chat, or a PR, discard that wallet
and create a fresh one before mining.
## Environment
```bash
git checkout feat/mining-spec-a-only
uv sync --extra dev --extra mining-pearl-vllm
export PEARLD_RPC_PASSWORD='<pearld-rpc-password>'
export HF_TOKEN='<huggingface-token>'
```
Confirm host prerequisites:
```bash
nvidia-smi
docker info
docker run --rm --gpus all nvidia/cuda:12.9.1-base-ubuntu24.04 nvidia-smi
df -h ~/.cache
```
Expected:
- `nvidia-smi` shows H100 or H200.
- Docker can run a CUDA container with GPU access.
- `~/.cache` or the Hugging Face cache volume has at least 200 GB free.
On shared hosts, select only idle GPUs during `mine init`:
```bash
uv run jarvis mine init --cuda-visible-devices 0
```
This writes `[mining.extra].cuda_visible_devices`. `mine start` passes that
device list to Docker and sets `CUDA_VISIBLE_DEVICES` /
`NVIDIA_VISIBLE_DEVICES` inside the container. Omit the option only on a
dedicated host where the miner may use all GPUs.
## Configure Mining
Run:
```bash
uv run jarvis mine doctor
```
Before config exists, `doctor` should show hardware and Docker as OK, and
Pearl node / wallet as unconfigured.
Then initialize:
```bash
uv run jarvis mine init
```
Use:
- Wallet: the user's Pearl `prl1q...` or `prl1p...` address
- `pearld` URL: usually `http://localhost:44107`
- RPC user: configured `pearld` user, often `rpcuser`
- Password env: `PEARLD_RPC_PASSWORD`
- Model: `pearl-ai/Llama-3.3-70B-Instruct-pearl`
- Image: default unless validating a custom Pearl image
- CUDA devices: an idle GPU ID such as `0` on shared hosts
Expected:
- `[mining]` and `[mining.extra]` are written to config.
- Image resolves locally, pulls, or builds from the pinned Pearl ref.
- First build may take 30-60 minutes.
Run `doctor` again:
```bash
uv run jarvis mine doctor
```
Expected:
- Hardware OK
- Docker OK
- Disk OK
- Pearl node RPC OK and synced
- Wallet format OK
- `vllm-pearl SUPPORTED`
- Sidecar absent
## Start Mining
```bash
uv run jarvis mine start
```
Expected:
- Docker container `openjarvis-pearl-miner` starts.
- `~/.openjarvis/runtime/mining.json` is written.
- Sidecar contains `vllm_endpoint`, `gateway_url`, `gateway_metrics_url`, and
`container_id`.
Inspect:
```bash
docker ps --filter name=openjarvis-pearl-miner
cat ~/.openjarvis/runtime/mining.json
uv run jarvis mine logs --tail 200
uv run jarvis mine status
```
Expected:
- Container is running.
- vLLM is listening on the configured port, default `8000`.
- Pearl gateway metrics are available on the configured metrics port, default
`8339`.
- `mine status` exits 0 and prints `provider: vllm-pearl`.
## Verify OpenJarvis Inference Uses Mining Endpoint
Run:
```bash
uv run jarvis mine doctor
uv run jarvis ask "Say hello in one sentence."
```
Expected:
- `doctor` shows sidecar present.
- Engine discovery registers `vllm-pearl-mining`.
- The prompt completes through the Pearl/vLLM endpoint.
- Container logs show vLLM activity during the prompt.
If inference succeeds but mining stats stay zero, continue to the Pearl
network checks below; vLLM serving alone is not enough to prove mining.
## Verify Pearl Network Submission
Check gateway metrics directly:
```bash
curl -fsS http://127.0.0.1:8339/metrics | tee /tmp/pearl-gateway-metrics.txt
uv run jarvis mine status
```
Expected:
- Metrics endpoint returns Prometheus text.
- If Pearl exposes share counters, `mine status` maps them correctly.
- If metric names differ, attach `/tmp/pearl-gateway-metrics.txt` to the PR and
update `src/openjarvis/mining/_metrics.py`.
Check `pearld` connectivity using the same RPC configuration used by mining:
```bash
curl --user "rpcuser:${PEARLD_RPC_PASSWORD}" \
--data-binary '{"jsonrpc":"1.0","id":"oj","method":"getblockchaininfo","params":[]}' \
-H 'content-type: text/plain;' \
http://127.0.0.1:44107
```
Expected:
- `blocks` and `headers` are present.
- Node is synced or close enough for mining validation.
Proof of actual earning requires a successful accepted share/block and wallet
credit. Depending on Pearl network difficulty, this may take longer than the
smoke test window. Record:
- Runtime duration
- `mine status` before and after
- Gateway metrics snapshot
- Relevant container log tail
- Wallet balance / transaction evidence if a reward lands
## Stop And Cleanup
```bash
uv run jarvis mine stop
docker ps --filter name=openjarvis-pearl-miner
test ! -e ~/.openjarvis/runtime/mining.json
```
Expected:
- Container stops.
- Sidecar is removed.
- `jarvis ask` no longer routes through `vllm-pearl-mining` unless another
mining sidecar is attached.
## Pass Criteria
The NVIDIA provider is considered proven when all are true:
- `mine doctor` reports supported on H100/H200.
- `mine init` resolves/builds the Pearl image.
- `mine start` launches the container and writes the sidecar.
- OpenJarvis inference succeeds through `vllm-pearl-mining`.
- Pearl gateway metrics are reachable and `mine status` parses them.
- `pearld` accepts the miner's network path.
- At least one accepted share/block is observed, or a documented Pearl
maintainer confirmation says the observed gateway state is sufficient proof
of live mining.
## Failure Artifacts
For any failure, collect:
```bash
uv run jarvis mine doctor
uv run jarvis mine status || true
uv run jarvis mine logs --tail 300 || true
docker inspect openjarvis-pearl-miner || true
curl -fsS http://127.0.0.1:8339/metrics || true
nvidia-smi
docker info
```
Attach outputs to the implementation PR or follow-up issue. Do not paste
`PEARLD_RPC_PASSWORD`, wallet seed material, or Hugging Face tokens.
+84
View File
@@ -0,0 +1,84 @@
# Adding a Mining Provider
The `openjarvis.mining` subsystem follows the same registry pattern as engines,
agents, tools, memory, and channels. New mining paths should be provider
modules, not special cases in the CLI or engine layer.
## Provider Contract
Every provider implements `openjarvis.mining.MiningProvider`:
- `detect(hw, engine_id, model)` is pure capability detection. It must not
start subprocesses, hit the network, or mutate state.
- `start(config)` owns provider lifecycle setup and writes the mining sidecar
when it changes inference routing.
- `stop()` tears down provider-owned processes or containers.
- `is_running()` answers from provider-owned state.
- `stats()` returns `MiningStats` using the provider's most stable telemetry
surface.
Register providers through `MinerRegistry` and expose idempotent
`ensure_registered()`:
```python
from openjarvis.core.registry import MinerRegistry
def ensure_registered() -> None:
if not MinerRegistry.contains("my-provider"):
MinerRegistry.register_value("my-provider", MyProvider)
```
`tests/conftest.py` clears registries between tests, so test fixtures and CLI
entry points should call `ensure_registered()` before relying on a provider.
## Optional Dependencies
Provider dependencies belong in scoped extras:
- `mining-pearl-vllm` for the NVIDIA/vLLM Docker provider
- Future Apple work should use a separate extra such as `mining-pearl-metal`
or `mining-pearl-cpu`
Avoid a generic `mining-pearl` extra until there is a shared dependency set
that every provider actually needs.
## Sidecar Contract
The runtime sidecar lives at `~/.openjarvis/runtime/mining.json`. Engine
handoff is data-driven:
- If the sidecar has `vllm_endpoint`, engine discovery registers
`vllm-pearl-mining`.
- If a future provider mines alongside the user's normal engine, it should omit
`vllm_endpoint`; engine discovery will ignore it.
Do not branch on `provider == "vllm-pearl"` in generic code. Branch on sidecar
shape or provider capability.
## Apple Silicon Handoff
The Apple Silicon effort should add its own provider module and reuse:
- `MiningProvider`
- `MinerRegistry`
- `MiningConfig`
- `MiningStats`
- `Sidecar`
- `jarvis mine doctor` capability iteration
That work should not need to rewrite the NVIDIA provider, CLI group, telemetry
collector, or engine sidecar handoff.
## NVIDIA Release Gate
The NVIDIA provider is not considered economically proven until the H100/H200
runbook passes on real hardware. See
[`mining-nvidia-validation.md`](./mining-nvidia-validation.md) for the required
commands, artifacts, and pass criteria.
## Model Enablement
New Pearl-compatible language models are tracked separately from provider
support. See [`pearl-model-enablement.md`](./pearl-model-enablement.md) for the
conversion and validation checklist.
@@ -0,0 +1,93 @@
# Pearl Model Enablement
This page tracks the work required to make a new Hugging Face model mineable
through Pearl's vLLM miner and OpenJarvis.
OpenJarvis can point `vllm-pearl` at a model id, but a raw Hugging Face model is
not enough. The Pearl vLLM plugin expects a Pearl-compatible quantized model
whose metadata marks mining layers for 7-bit NoisyGEMM and non-mining layers
for the vanilla Pearl GEMM path.
## Target Models
| Raw model | Planned Pearl model | Status | Tracking |
|---|---|---|---|
| `Qwen/Qwen3.5-9B` | `pearl-ai/Qwen3.5-9B-pearl` | Planned | [#316](https://github.com/open-jarvis/OpenJarvis/issues/316) |
| `Qwen/Qwen3.6-27B` | `pearl-ai/Qwen3.6-27B-pearl` | Planned | [#317](https://github.com/open-jarvis/OpenJarvis/issues/317) |
| `google/gemma-4-E4B-it` | `pearl-ai/Gemma-4-E4B-it-pearl` | Planned | [#318](https://github.com/open-jarvis/OpenJarvis/issues/318) |
| `google/gemma-4-31B-it` | `pearl-ai/Gemma-4-31B-it-pearl` | Planned | [#319](https://github.com/open-jarvis/OpenJarvis/issues/319) |
The current validated model remains:
```text
pearl-ai/Llama-3.3-70B-Instruct-pearl
```
## Enablement Checklist
1. Reproduce the current Llama Pearl model recipe.
- Record the compressed-tensors config.
- Record which linear layers are 7-bit mining layers.
- Record which layers are 8-bit non-mining layers.
- Record calibration data and SmoothQuant settings, if used.
2. Convert the target model.
- Start with `Qwen/Qwen3.5-9B`; it is the smallest target.
- Generate Pearl-compatible quantized weights and metadata.
- Publish under the planned `pearl-ai/*-pearl` id or a staging namespace.
3. Validate the Pearl vLLM plugin path.
- Model loads in Pearl's `vllm-miner` container.
- vLLM registers Pearl's quantization plugin.
- Mining layers use int7 NoisyGEMM.
- Non-mining layers use int8 vanilla Pearl GEMM.
- Text generation works with mining enabled and disabled.
4. Validate chain integration.
- `pearld` is reachable.
- `pearl-gateway` receives work.
- NoisyGEMM submits candidate proofs.
- Gateway reports metrics.
- `jarvis mine status` parses those metrics.
5. Promote the model in OpenJarvis.
- Change its registry status from `planned` to `validated`.
- Set measured VRAM and context defaults.
- Add the model to user docs.
- Attach validation logs to the PR.
## OpenJarvis Registry
Model support metadata lives in:
```text
src/openjarvis/mining/_models.py
```
`jarvis mine models` renders that registry. Planned models are visible to users
but blocked by capability detection until the Pearl model artifact and H100/H200
validation exist.
## Acceptance Criteria
A model is `validated` only when all of these pass on real hardware:
- `jarvis mine init --model <pearl-model-id>`
- `jarvis mine start`
- `curl http://127.0.0.1:8000/v1/models`
- `jarvis ask "Say hello in one sentence."`
- `jarvis mine status`
- `jarvis mine validate-model --model <pearl-model-id> --allow-planned --prompt
"Say hello in one sentence." --output <artifact>.json`
- Pearl gateway metrics show the mining path is active.
- No block/share submission errors appear in gateway or miner logs.
Do not mark a model validated based only on vLLM load success. It must exercise
Pearl's NoisyGEMM and submission path.
## Tracking
Use the `Pearl Model Validation` GitHub issue template for each candidate model.
The issue should hold the quantization recipe, hardware details, command output,
metrics excerpts, and the PR that changes the model status to `validated`.
Attach the JSON artifact from `jarvis mine validate-model --output` to the issue.
+29
View File
@@ -199,6 +199,35 @@ jarvis model pull qwen3:8b
---
## `jarvis pearl`
Access Pearl's native node, wallet, and RPC tools from the OpenJarvis CLI.
```bash
jarvis pearl doctor
jarvis pearl node -- <pearld args>
jarvis pearl wallet -- <oyster args>
jarvis pearl ctl -- <prlctl args>
jarvis pearl address
```
All Pearl wrapper commands use the `jarvis pearl <command>` shape. The
pass-through commands map to Pearl's native binaries:
| OpenJarvis command | Pearl binary | Use |
|--------------------|--------------|-----|
| `jarvis pearl doctor` | n/a | Check whether `pearld`, `oyster`, and `prlctl` are discoverable |
| `jarvis pearl node` | `pearld` | Run the Pearl full node |
| `jarvis pearl wallet` | `oyster` | Run the Oyster wallet daemon |
| `jarvis pearl ctl` | `prlctl` | Query Pearl node or wallet RPC |
| `jarvis pearl address` | `prlctl --wallet getnewaddress` | Generate a wallet address from Oyster |
Use `PEARL_HOME=/path/to/pearl` or `--pearl-home /path/to/pearl` if Pearl's
`bin/` directory is not on `PATH`. See the [Pearl CLI guide](pearl.md) for
examples.
---
## `jarvis memory`
Manage the document memory store for retrieval-augmented generation.
+162
View File
@@ -0,0 +1,162 @@
# Mining Pearl on Apple Silicon (and other CPU hosts)
OpenJarvis can mine the [Pearl](https://github.com/pearl-research-labs/pearl) chain
on Apple Silicon Macs (M1/M2/M3/M4) using the `cpu-pearl` provider. **This is
v1**: decoupled CPU mining. Your existing local LLM workflow (Ollama, MLX-LM,
llama.cpp, vLLM) is untouched; mining runs in the background as a separate
process.
## Honest expectations
**Hashrate on Apple Silicon CPU is far below what an H100 produces with
Pearl's `vllm-miner`.** A rough rule of thumb (subject to network difficulty):
- M2 Max / M4 Max: ≪ 1 share per second at typical mainnet difficulty
- H100 with `vllm-miner`: meaningfully higher, plus the mining work is
amortized over real LLM inference
If you want to mine for yield, this isn't the path. If you want to participate
in the network from the hardware you own, with no special hardware purchase,
this is the path.
An experimental `apple-mps-pearl` provider is available for developers. It
uses PyTorch MPS for the NoisyGEMM matmuls, while transcript hashing and proof
construction still run on CPU. This proves the Apple-GPU path can produce
validator-accepted `PlainProof`s, but it is not yet the high-performance Metal
kernel path.
## Prerequisites
- macOS arm64 (M1, M2, M3, M4) — or Linux x86_64 / aarch64
- Python 3.12 (`brew install python@3.12` or use `uv venv --python 3.12`)
- Rust toolchain (`brew install rust` or `curl https://sh.rustup.rs -sSf | sh`)
- Your own running [`pearld`](https://github.com/pearl-research-labs/pearl#node)
node, RPC reachable on `http://localhost:44107`
- A Pearl Taproot wallet address from `oyster` (Pearl's wallet CLI)
- ~1 GB free disk for the Pearl source clone and build artifacts
## Install
```bash
# from your OpenJarvis repo
uv sync --extra mining-pearl-cpu
```
If Pearl wheels are not yet on PyPI (still true as of 2026-05-05), `uv sync`
succeeds but doesn't install the actual Pearl Python packages. Build/install
them from a local Pearl checkout:
```bash
cd /path/to/pearl/py-pearl-mining
maturin build --release
uv pip install target/wheels/py_pearl_mining-*.whl
uv pip install ../miner/miner-utils ../miner/pearl-gateway ../miner/miner-base
```
## Configure
Create a Pearl wallet and start a synced `pearld` separately using Pearl's
README. Then write OpenJarvis' mining config:
```bash
export PEARLD_RPC_PASSWORD="rpcpass"
jarvis mine init \
--provider cpu-pearl \
--wallet-address "<your-prl1...address>" \
--pearld-rpc-url http://127.0.0.1:44107 \
--pearld-rpc-user rpcuser \
--pearld-rpc-password-env PEARLD_RPC_PASSWORD
```
On Apple Silicon, `--provider auto` chooses `apple-mps-pearl`; use
`--provider cpu-pearl` for the conservative CPU path. The MPS path is
experimental and currently useful for validation/profiling, not revenue.
This writes:
```toml
[mining]
provider = "cpu-pearl"
wallet_address = "prl1..."
submit_target = "solo"
fee_bps = 0
[mining.extra]
pearld_rpc_url = "http://127.0.0.1:44107"
pearld_rpc_user = "rpcuser"
pearld_rpc_password_env = "PEARLD_RPC_PASSWORD"
gateway_host = "127.0.0.1"
gateway_port = 8337
metrics_port = 9109
```
## Run
```bash
jarvis mine doctor # capability matrix
jarvis mine start # launch gateway + miner-loop subprocesses
jarvis mine status # check sidecar + gateway metrics
jarvis mine logs -n 120 # print recent logs
jarvis mine stop # stop mining subprocesses
```
## Reading `mine doctor`
Each row is one check. `✓` means the check passed; `✗` shows the actionable fix.
```
$ jarvis mine doctor
Hardware
GPU vendor apple ✓
Apple chip M2 Max ✓
Pearl install
py-pearl-mining 0.1.0 (cp312-abi3-macos-arm64) ✓
miner-base 0.1.0 ✓
pearl-gateway 0.1.0 ✓
Pearl node
RPC http://localhost:44107 ✓
Block height 442107 (synced) ✓
Wallet
Address format prl1q... ✓
Provider capability
cpu-pearl SUPPORTED (calibrated 0.X share/h on M2 Max)
Notes
- This is decoupled mining: your normal LLM inference is unaffected
- Hashrate is far below H100 mining; see this doc above
- MPS mining: available as experimental apple-mps-pearl
Session
Sidecar absent (not running)
```
## Limitations
- **Windows is not supported in v1.** Pearl's pure-Rust miner builds on
Windows in principle but the cross-platform install path is untested. Use
WSL2 if you must.
- **No coupling to inference yet.** v1 is a separate process; your CPU does
mining, your GPU does inference. They don't share work. v2 changes this.
- **Experimental PyTorch-MPS only.** `apple-mps-pearl` moves the NoisyGEMM
matmuls to MPS but still has CPU readbacks for transcript hashing and proof
construction. Use it for validation and profiling, not revenue expectations.
- **No multi-host pool.** Solo mining only. The pool work is a separate spec.
## Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `mine doctor` says `Pearl Python packages not installed` | Wheels not built yet | Run `jarvis mine init` |
| `pearl-gateway` log shows `connection refused` to `http://localhost:44107` | `pearld` not running | Start `pearld` per Pearl's README |
| `mine status` shows `last_error: gateway metrics unreachable` | `pearl-gateway` crashed | Check `~/.openjarvis/logs/mining/pearl-gateway.log` |
| Build fails with `error: linker 'cc' not found` | Xcode CLT not installed | `xcode-select --install` |
| `maturin build` complains about `tikv-jemallocator` | macOS SDK too old | Update macOS / Xcode |
For anything not on this list, capture `~/.openjarvis/logs/mining/` and open
an issue at https://github.com/open-jarvis/OpenJarvis/issues.
## What changes in v2 / v3
- **v2:** Optimize the current `apple-mps-pearl` path, then optionally plug it
into MLX-LM or `llama-cpp-python` so inference matmuls become mining work.
- **v3 (only if v2 perf is insufficient):** Native Metal kernel as a Pearl
upstream contribution. No user-visible change other than higher hashrate.
+133
View File
@@ -0,0 +1,133 @@
# Pearl Mining
OpenJarvis can mine the Pearl Proof-of-Useful-Work chain through local LLM
inference. The primary v1 path supports NVIDIA H100/H200 hosts running vLLM
with Pearl's Docker miner. The consolidated Pearl integration also includes
experimental Apple Silicon and CPU providers through the same `MiningProvider`
registry.
## Prerequisites
| Requirement | v1 expectation |
|---|---|
| GPU | NVIDIA H100 or H200, sm_90a class, at least 70 GB VRAM |
| OS | Linux with `nvidia-container-toolkit` configured |
| Docker | Docker 24+ with GPU runtime access |
| Disk | At least 200 GB free for the 70B model and build cache |
| Pearl node | Reachable `pearld` JSON-RPC endpoint, default `http://localhost:44107` |
| Wallet | Pearl address beginning with `prl1q` or `prl1p` |
The default vLLM config uses `gpu_memory_utilization = 0.96` and
`max_model_len = 8192` for the Pearl 70B mining model on H100/H200 80 GB GPUs.
To generate a wallet address with Pearl's Oyster wallet, run Pearl's wallet
daemon and query it with `prlctl --wallet --skipverify -s localhost:44207
getnewaddress`. Do not reuse a wallet whose mnemonic has been pasted into logs,
chat, or issue trackers.
## Quick Start
```bash
uv sync --extra mining-pearl-vllm
export PEARLD_RPC_PASSWORD=<your-pearld-password>
export HF_TOKEN=<your-huggingface-token>
uv run jarvis mine init
uv run jarvis mine start
uv run jarvis mine status
```
`mine init` writes a `[mining]` config section and resolves the Pearl Docker
image. If Pearl has not published a suitable image for the pinned ref,
OpenJarvis falls back to building from the pinned Pearl source checkout. First
builds can take 30-60 minutes.
On a shared NVIDIA host, restrict the miner to idle GPUs:
```bash
uv run jarvis mine init --cuda-visible-devices 0
```
This writes `[mining.extra].cuda_visible_devices`, which `mine start` passes to
Docker instead of exposing every GPU on the machine.
## Commands
- `jarvis mine models` lists Pearl model support status.
- `jarvis mine doctor` prints hardware, Docker, Pearl node, wallet, provider,
and session checks.
- `jarvis mine init` writes the local mining config and resolves the image.
- `jarvis mine start` launches the Pearl miner container and writes the runtime
sidecar.
- `jarvis mine stop` stops the provider and removes the sidecar.
- `jarvis mine status` reads live gateway metrics.
- `jarvis mine attach` writes a sidecar for a miner you launched manually.
- `jarvis mine logs` prints the Docker container log tail.
- `jarvis mine validate-model` probes the active vLLM miner and gateway before
promoting a planned Pearl model to validated.
## Model Support
Run:
```bash
jarvis mine models
```
OpenJarvis only enables models that have Pearl-compatible quantized artifacts
and real hardware validation. Raw Hugging Face models such as
`Qwen/Qwen3.5-9B` or `google/gemma-4-E4B-it` are not mineable by themselves;
they need corresponding `pearl-ai/*-pearl` variants.
The default validated model is:
```text
pearl-ai/Llama-3.3-70B-Instruct-pearl
```
The Qwen and Gemma targets are tracked in the model registry as planned until
Pearl quantization and H100/H200 validation are complete.
When validating a newly converted Pearl model on a mining host, run:
```bash
jarvis mine validate-model \
--model pearl-ai/Qwen3.5-9B-pearl \
--allow-planned \
--prompt "Say hello in one sentence." \
--output qwen3.5-9b-pearl-validation.json
```
Remove `--allow-planned` only after the model is promoted to validated in the
OpenJarvis registry. Attach the JSON artifact to the validation issue.
## v1 Scope
v1 is solo mining only. OpenJarvis does not take fees, custody funds, generate
wallet keys, run pools, or operate `pearld`. Users provide their own Pearl node
and payout address.
Unsupported in this PR:
- Pool mining and the future 20% OpenJarvis fee model
- AMD GPU mining and non-Pearl backends
- RTX 4090 or other non-Hopper NVIDIA GPUs
- Wallet generation or transaction signing inside OpenJarvis
## Troubleshooting
Run:
```bash
uv run jarvis mine doctor
```
Read the rows top-down. Fix the first failing dependency before retrying
`mine start`. A Mac or AMD machine should fail honestly at provider capability;
those paths are expected to land as separate providers.
## Production Readiness
The NVIDIA path requires one real H100/H200 validation run before it should be
marketed as a proven earning path. The developer runbook is
[`../development/mining-nvidia-validation.md`](../development/mining-nvidia-validation.md).
+58
View File
@@ -0,0 +1,58 @@
# Pearl CLI Integration
OpenJarvis includes a thin `jarvis pearl` wrapper for Pearl's native command
line tools. It does not replace Pearl's node or wallet; it makes the common
commands discoverable from the same CLI users use for mining.
## Binary Discovery
`jarvis pearl` looks for `pearld`, `oyster`, and `prlctl` on `PATH`, then under
`$PEARL_HOME/bin`.
```bash
export PEARL_HOME=/path/to/pearl
jarvis pearl doctor
```
## Native Pass-Through
Use pass-through commands when you need the full Pearl surface:
```bash
jarvis pearl node -- --help
jarvis pearl wallet -- --help
jarvis pearl ctl -- --help
```
These map directly to:
| OpenJarvis command | Pearl binary |
|---|---|
| `jarvis pearl node` | `pearld` |
| `jarvis pearl wallet` | `oyster` |
| `jarvis pearl ctl` | `prlctl` |
The command format is always `jarvis pearl <command>`. Pearl-native arguments
go after that command. Use `--` before Pearl arguments when the arguments begin
with dashes and you want to make the pass-through boundary explicit.
## Wallet Address Helper
If Oyster is already running, generate a mining address through wallet RPC:
```bash
jarvis pearl address \
-u rpcuser \
-P rpcpass \
-s localhost:44207
```
The helper uses `prlctl --wallet` and defaults to `--notls`, which matches the
local validation flow. Use `--tls --skipverify` if your Oyster RPC endpoint is
serving TLS with a local certificate.
## Boundary
`jarvis mine` is the OpenJarvis mining lifecycle. `jarvis pearl` is an escape
hatch to Pearl's native node, wallet, and RPC tools. For advanced node or
wallet administration, Pearl's own help output is the source of truth.