Skip to content

Purple8 Deployment Model ​

The Embedded Engine Contract ​

Purple8 is an embedded multi-model database — the GraphEngine (backed by BrickCoreStorage and the Rust DashMap buffer) lives inside the serving process. It is not a network service that multiple processes connect to.

This is the same model as SQLite, LMDB, or DuckDB.


Multiple Workers: When It Works and When It Doesn't ​

uvicorn --workers N forks N separate OS processes. Each worker starts from the same on-disk state (WAL replay + BrickSlab restore at startup), so they all begin with identical data.

Startup:  all workers replay WAL from disk → identical state ✓
Runtime reads:  any worker can serve any query → fine ✓
Runtime writes: POST /nodes round-robins across workers
                → each worker's in-memory DashMap gets 1/N of the writes
                → reads on a different worker miss those writes ✗

Works fine with N workers ​

  • Read-heavy workloads after a completed ingest — all workers share the same on-disk baseline, all queries are served correctly
  • Stateless API layers (auth, schema, metadata) — no in-memory write state
  • Query, RAG, vector search, graph traversal — all read from the same BrickSlabs on disk

Breaks with N workers ​

  • Concurrent HTTP ingest — if a client posts 32k nodes while 16 workers are running, each worker's in-memory DashMap receives ~2k nodes, and reads on any single worker return only that worker's subset until replay. Do not ingest via HTTP across multiple workers on a single instance — keep WORKERS=1 for write-enabled instances.

Hardware & Accelerator Auto-Discovery (worker_solver.py & CortexAutoConfig) ​

At startup, CortexAutoConfig and purple8_graph.core.worker_solver automatically inspect the host hardware and accelerator topology:

  • Multi-socket CPU & NUMA topology: Detects cpu_sockets and numa_nodes via HardwareTopology and /sys/devices/system/node, computing recommended_shards = max(cpu_sockets, numa_nodes) for NUMA-local shard pinning.
  • Intel AMX & Intel XPU: Detects Intel AMX (amx_tile, amx_int8, amx_bf16) matrix extensions on Xeon Scalable processors and Intel XPU (torch.xpu integrated and discrete Arc / Data Center GPU Max accelerators).
  • NVIDIA Tensor Core GPUs: Detects Hopper (H100/H200, sm_90) and NVIDIA Blackwell / Blackwell Ultra B300 & GB300 (288 GB HBM3e, compute capability sm_103, 5th-gen FP4 Tensor Cores).
  • Apple Silicon Metal / Unified Memory: Detects Apple M-series unified memory (mps) where CPU and GPU share physical RAM with zero PCIe copy overhead.

Run the worker solver directly to inspect your host's detected topology:

bash
python -m purple8_graph.core.worker_solver --mode balanced --data-path /data

The thread budget constraint ​

HardwareTopology allocates a full Rayon thread pool per worker process:

InstancePhysical coresRayon threads/workerWORKERS=4 totalWORKERS=16 total
c6i.4xlarge8~936 threads144 threads
c6i.16xlarge32~30120 threads480 threads

480 threads on 32 cores = 15× over-subscription. At WORKERS=16 on EC2 the OS scheduler was thrashing before any actual work happened.

Rule of thumb: WORKERS ≤ 4 on any instance type. Above 4, Rayon thread over-subscription dominates.


Purple8's parallelism comes from Rust, not from forking:

┌─────────────────────────────────────────────────────┐
│                   Python process                    │
│                                                     │
│  uvicorn (uvloop)  ←  async HTTP, no blocking I/O  │
│  asyncio event loop ← handles N concurrent requests │
│                                                     │
│  ThreadPoolExecutor (READ pool)   ← from Rust HW   │
│  ThreadPoolExecutor (WRITE pool)  ←  topology       │
│  ThreadPoolExecutor (BULK pool)   ← partition       │
│                                                     │
│  GraphEngine → BrickCoreStorage → Rust DashMap      │
│                     ↕                               │
│  Rayon thread pool (all CPU cores)                  │
│  ├── WAL subsystem                                  │
│  ├── Graph traversal                                │
│  ├── Vector search (HNSW)                           │
│  ├── Full-text search                               │
│  ├── Query planner                                  │
│  └── Background compaction                         │
└─────────────────────────────────────────────────────┘

HardwareTopology in the Rust core detects the available logical CPUs and divides them across 6 Rayon subsystem pools. A 32-vCPU machine gets ~5 cores for vector search, ~5 for graph traversal, ~4 for WAL, etc. — all inside a single Python process.

Configuration:

bash
WORKERS=1          # always — the default in Dockerfile.commercial
P8G_HOST=0.0.0.0
P8G_PORT=8000

Horizontal Scaling: Multiple Container Instances ​

To scale beyond one machine, run multiple container instances, each with its own data volume, behind a load balancer with consistent hashing (route by tenant, entity type, or key range):

                 ┌── LB (consistent hash) ──┐
                 │                          │
client ──────────┼── Instance A (vol-a) ────┤
                 │     engine owns shard A   │
                 ├── Instance B (vol-b) ────┤
                 │     engine owns shard B   │
                 └── Instance C (vol-c) ────┘
                       engine owns shard C

This is true horizontal sharding — not in-process forking. Each instance is a fully independent Purple8 node. The supergraph/ module provides cross-instance query federation when needed.

For HA within a shard, use raft_cluster.py (Raft consensus, 3-node quorum) with a replica.py follower per node.


Primary / Replica Topology (Horizontal Read Scaling + HA) ​

Purple8 Graph runs as one primary and N replicas. The primary owns writes; replicas receive the WAL over a dedicated replication link (port 8101), apply it to their own storage, and serve reads independently. The same replicas are the failover candidates.

                 ┌── LB: writes (POST/PUT/PATCH/DELETE) ──► Primary
client ──────────┤
                 └── LB: reads (GET/HEAD) ──► Primary + Replica 1 + Replica 2 ...

   Primary ──WAL stream (8101, token/TLS)──► Replica 1
           ──WAL stream ─────────────────► Replica 2

How it works:

  1. Primary runs with P8G_REPLICATION_ROLE=primary and lists its replicas in P8G_FOLLOWER_ADDRESSES. P8G_REPLICATION_MODE=sync makes a write return only after P8G_REPLICATION_SYNC_REPLICAS replicas have acknowledged it (RPO ≈ 0); async returns immediately.
  2. Replicas run with P8G_REPLICATION_ROLE=follower and P8G_PRIMARY_HOST/PORT. Each has its own data volume — no shared filesystem is required or recommended. The full HTTP API is served; write verbs return 503 with Retry-After (follower_write_guard) so a mis-routed write fails closed.
  3. Reads on replicas are eventually consistent; GET /health reports replication_lag (operations behind the primary). Route reads that need read-your-writes to the primary.
  4. Failover (P8G_FAILOVER_ENABLED=1 on a replica): a watchdog probes the primary's /health every P8G_FAILOVER_INTERVAL_S; after P8G_FAILOVER_UNHEALTHY_THRESHOLD consecutive failures it promotes itself via POST /replication/promote (authenticated with the shared P8G_REPLICATION_AUTH_TOKEN) and repoints the load balancer through the P8G_FAILOVER_TARGET_BACKEND hook (elbv2 on AWS; noop logs only). A cooldown (P8G_FAILOVER_COOLDOWN_S) prevents flapping. With more than one replica, enable the Raft coordination backend so the election guard and STONITH fencing apply.

Configuration:

bash
# Primary
P8G_DATA_PATH=/data/graph
P8G_REPLICATION_ROLE=primary
P8G_REPLICATION_BIND_HOST=0.0.0.0
P8G_REPLICATION_TLS=1
P8G_REPLICATION_TLS_CERT=/run/tls/primary.pem
P8G_REPLICATION_TLS_KEY=/run/tls/primary.key
P8G_REPLICATION_TLS_CA=/run/tls/ca.pem
P8G_REPLICATION_MODE=sync
P8G_REPLICATION_SYNC_REPLICAS=1
P8G_FOLLOWER_ADDRESSES=replica-1:8101
P8G_REPLICATION_AUTH_TOKEN=<shared secret>
WORKERS=1

# Replica
P8G_DATA_PATH=/data/graph            # its own volume
P8G_REPLICATION_ROLE=follower
P8G_REPLICATION_BIND_HOST=0.0.0.0
P8G_REPLICATION_TLS=1
P8G_REPLICATION_TLS_CERT=/run/tls/replica.pem
P8G_REPLICATION_TLS_KEY=/run/tls/replica.key
P8G_REPLICATION_TLS_CA=/run/tls/ca.pem
P8G_PRIMARY_HOST=primary
P8G_PRIMARY_PORT=8101
P8G_REPLICATION_AUTH_TOKEN=<shared secret>
P8G_FAILOVER_ENABLED=1
P8G_FAILOVER_PRIMARY_ADDRESS=primary:8080
P8G_NODE_ADDRESS=replica-1:8080
P8G_FAILOVER_TARGET_BACKEND=noop      # or elbv2 + P8G_FAILOVER_WRITE_TG_ARN / READ_TG_ARN
WORKERS=1

Replication requires a shared authentication token and TLS before network threads start. Cross-host listeners must opt into a non-loopback bind address. Promoted replicas retain their authentication, TLS and synchronization settings. Automated promotion requires verified HTTPS on the advertised API addresses (directly or through the existing TLS ingress) and does not follow redirects. See Clustering for the explicit loopback-only development exception.

P8G_READONLY=1 is a separate, static mode: the instance rejects all writes for its lifetime regardless of replication role. Use it for a standalone read-only copy restored from backup; do not combine it with P8G_REPLICATION_ROLE=follower (the follower guard already covers writes and promotion must be able to lift it).

Constraints:

  • One primary per logical database. Write throughput scales vertically; read throughput scales with replica count.
  • Read-your-writes is not guaranteed on replicas. Route /auth/*, /replication/*, /mcp*, /lcnc* and SSE change feeds to the primary.
  • Promotion is irreversible within the process lifetime; the old primary must be restarted as follower to rejoin.

When to use:

  • Read-heavy workloads that exceed one instance
  • Two-AZ / two-host availability with automated failover
  • Analytics queries that would otherwise contend with writes

Reference deployment: deployment/cloudformation/aws/purple8-aml-instance.yaml (EC2 + EBS primary and replica, method-based ALB routing, ELBv2 failover hook).


uvloop + httptools ​

uvicorn[standard] installs:

  • uvloop — Cython-based libuv event loop; 2–4× faster than asyncio's default SelectorEventLoop for I/O-bound workloads
  • httptools — fast LLHTTP-based HTTP/1.1 parser (same parser Node.js uses)

These are enabled automatically when installed. The entrypoint passes --loop uvloop --http httptools explicitly so the flags are visible in process listings and container logs.


Performance Landmarks (c6i.16xlarge, 32 vCPU) ​

OperationTimeNotes
count_nodes()0.006 msO(1) Rust DashMap atomic counter
get_nodes_page(limit=10)0.052 msStorage push-down, O(limit)
iter_nodes()[:10] (old path)34.2 msFull scan, O(N) — 658× slower
Middleware stack (9 layers)0.06 msHMAC, RBAC, tenant, telemetry
BrickWAL fdatasync (1 batch)~7 µsPer the lock-free write path

The list_nodes HTTP handler serialises via raw orjson.dumps() (skips FastAPI response model re-serialisation) and returns a plain Response with media_type="application/json".


Checklist: Before You Change WORKERS ​

  • [ ] Are you running Purple8 as an embedded engine (default)? → WORKERS=1
  • [ ] Do you need more throughput? → Add a second container instance with its own volume; do not fork the same instance
  • [ ] Are you running a pure stateless FastAPI service that does NOT use GraphEngine? → WORKERS=N is fine; set P8G_ALLOW_MULTI_WORKER=1 to suppress the warning
  • [ ] Running Locust/k6 benchmarks? → Seed and query against the same worker (WORKERS=1) or your latency numbers will be meaningless

Purple8 Hyper Graph v0.72.0 · AI-Native Execution Infrastructure · NVIDIA Inception Member · Red Hat Technology Partner.