spaCy CPU (Experimental)

Copy Markdown View Source

For the public :efficient profile with versioned installation and improved form boundaries, see the efficient guide. This page preserves the experimental alias and its historical results.

:spacy_cpu combines Obscura's deterministic recognizers with the pinned en_core_web_lg 3.8.0 NER-only model in a native CPU process. The model adds person and location detection; organizations and other spaCy labels are excluded from the profile's model outputs. All 18 labels still participate in decoding. The assigned NER score is 0.85, not a calibrated probability.

The profile is opt-in and supports Apple Silicon macOS and glibc Linux on ARM64 and x86-64. Linux uses OpenBLAS; macOS uses Accelerate. Alpine/musl, Windows, Intel macOS, and other CPU architectures are not supported. :balanced remains the stronger general accuracy recommendation. The initial native benchmark reproduced spaCy NER-only outputs on all 2,648 documents twice, with 4.7–6.1× lower end-to-end latency than the Python control. Strict F1 on Nemotron was 0.5403 versus 0.5659 for :balanced. Those are feasibility measurements; see eval/spacy_native/results/PROFILE.md for validation through the profile API.

Provision local assets

Build the executable and export the pinned model outside request handling:

mix obscura.spacy.build --offline
.presidio-authoritative-venv/bin/python native/spacy_cpu/export.py \
  --output .cache/spacy_cpu/en_core_web_lg-3.8.0

On macOS the build requires Rust's aarch64-apple-darwin target, Xcode command-line tools, and ARM64 Homebrew PCRE2. On Debian/Ubuntu Linux, install a current Rust toolchain and build-essential libopenblas-dev libpcre2-dev. The build chooses aarch64-unknown-linux-gnu or x86_64-unknown-linux-gnu from the running host. Use the standard LP64 OpenBLAS library, whose CBLAS dimensions are 32-bit integers; an ILP64/OpenBLAS64 library is incompatible. Omit --offline only if Cargo may retrieve its locked code dependencies. The exporter uses the existing pinned evaluation Python environment, with a standalone lock in native/spacy_cpu/export-environment.json. The exported files can then be copied unchanged between supported runtime hosts without Python. Build the executable on the target architecture and a compatible glibc distribution; macOS binaries cannot be copied to Linux. Linux runtime hosts need the OpenBLAS and PCRE2 shared libraries (on Debian: libopenblas0-pthread and libpcre2-8-0).

By default, the build installs the executable in the app's priv/spacy_cpu directory. mix obscura.spacy.build --output PATH chooses an external location. Pass that path as native_binary: PATH, or set OBSCURA_SPACY_BINARY. The model directory is explicit via model_dir: PATH or OBSCURA_SPACY_MODEL_DIR; there is no automatic model download or implicit search under the evaluation directory. Options take precedence over environment values.

Preparation streams and verifies SHA-256 for all five pinned files before starting native workers. Assets occupy about 408 MiB. Vectors are mapped read-only; resident memory depends on pages touched, so the earlier 170 MiB observed worker peak is not a memory limit. Provision immutable model files, then stop the runtime before replacing or removing them. Weights and prebuilt executables are not bundled in the Hex package. The source and export tooling are included.

Prepare and use

{:ok, runtime} =
  Obscura.Profile.prepare(:spacy_cpu,
    model_dir: ".cache/spacy_cpu/en_core_web_lg-3.8.0",
    workers: 1,
    request_timeout: 30_000
  )

{:ok, detections} =
  Obscura.analyze("José García lives in São Paulo.",
    profile: runtime,
    include_text: false
  )

{:ok, redacted} = Obscura.redact("Call Alice Smith.", profile: runtime)
Obscura.Spacy.Serving.status(runtime.resources.spacy)
Obscura.Spacy.Serving.stop(runtime.resources.spacy)

Analysis consumes the reusable runtime. Passing profile: :spacy_cpu without a prepared spacy_serving: returns a missing-resource diagnostic. Analysis never loads model files, builds Rust, invokes Python, or downloads assets. Deterministic recognizers, supported entity filtering, normal conflict resolution, byte offsets, and redaction use the existing Obscura path. Requesting only deterministic entity types skips NER execution on an already prepared runtime.

Ownership and bounded admission

Use a supervised preparer for an application-wide runtime:

children = [
  {Obscura.Profile.Preparer,
   name: MyApp.SpacyCPU,
   profile: :spacy_cpu,
   prepare_options: [model_dir: "/models/en_core_web_lg-3.8.0", workers: 2]}
]

{:ok, runtime} = Obscura.Profile.Preparer.await(MyApp.SpacyCPU)
Obscura.analyze(text, profile: runtime)

The pool monitors its owning preparer and closes its native processes when the preparer stops. Direct Profile.prepare/2 ties the pool to the calling process; prepare from a long-lived owner, or pass runtime_owner: owner_pid. Explicit Serving.stop/1 releases resources and is safe after owner-driven shutdown. Serving.start_link/1 is also available for direct supervision of an experimental pool, using the same preparation options.

workers accepts 1–4 and defaults to 1. Each worker uses one math thread. The pool sets VECLIB_MAXIMUM_THREADS, OPENBLAS_NUM_THREADS, OPENBLAS_DEFAULT_NUM_THREADS, and OMP_NUM_THREADS to 1 for its child processes. This covers both pthread and OpenMP OpenBLAS builds; see the OpenBLAS runtime variables. A caller reserves a slot before sending text. With no free slot, inference returns :spacy_busy; there is no internal text queue and no fallback to weaker detection. The analyzer wraps native errors as recognizer failures. Hosts should bound concurrency or handle saturation before accepting more input.

request_timeout accepts 1–300,000 ms and defaults to 30,000 ms. Reservations, inference, and caller death are monitored. Timeout, native exit, malformed response, and abandoned work close the affected Port before replacement. Restarts are limited to five attempts within five seconds; persistent startup failure reduces available capacity until the host explicitly recreates the runtime. Status reports available workers, busy slots, failures, and native RSS without source text. Restart handshakes can temporarily delay pool responses; this experimental pool does not promise a hard real-time deadline or a production tail-latency SLO.

Inputs are limited to valid UTF-8, 1 MiB, and 10,000 native tokens. Protocol frames are bounded. Prediction labels, scores, lengths, and UTF-8 boundaries are validated. Error reasons and crash formatting exclude source text and raw native responses.

Readiness and checks

mix obscura.profile.check --profile spacy_cpu \
  --model-dir .cache/spacy_cpu/en_core_web_lg-3.8.0 --offline --json
mix obscura.profile.prepare --profile spacy_cpu \
  --model-dir .cache/spacy_cpu/en_core_web_lg-3.8.0 --workers 2 --offline --json

Local preflight verifies platform, executable presence, and asset hashes without inference. --prepare on profile.check additionally checks native startup. The CLI tasks release their temporary pools after reporting; application code must retain its own runtime. Explicit incompatible backend options are rejected; this profile always runs on CPU, independent of global GPU backend configuration. Readiness/status identify :accelerate_cpu on macOS or :openblas_cpu on Linux; the native handshake must match the host backend. Peak RSS is normalized to bytes on both platforms.

mix test test/obscura/spacy test/obscura/profile_test.exs test/obscura/profile
OBSCURA_SPACY_MODEL_DIR="$PWD/.cache/spacy_cpu/en_core_web_lg-3.8.0" \
  mix test test/obscura/spacy/real_model_test.exs
mix run --no-start eval/spacy_native/profile_benchmark.exs

Portable tests use an explicitly fake executable only to exercise pool lifecycle and error handling. Real-model tests and the profile benchmark use the native executable and pinned weights. Real-model tests skip when assets/platform are unavailable. No authoritative accuracy profile is promoted by these checks.

For reproducible Docker Desktop checks of both Linux architectures, see Linux validation. The lane requires the real assets, tests the public profile and worker lifecycle, and compares all 2,648 selected documents to the pinned native baseline twice with networking disabled; see the recorded Linux results. x86-64 runs under emulation on an Apple Silicon host, so its timing does not predict performance on physical x86-64 hardware.

The model package declares MIT licensing; review its training/vector provenance for deployment. Obscura does not bundle or license the model weights. See model-asset-licensing.md and native/spacy_cpu/THIRD_PARTY_NOTICES.md.