Research-Stack/4-Infrastructure/shim/rrc_bosonic_tensor_gpu.py
allaun f00ead35c2 feat(infra): add async database buffering program for RRC receipts
Implemented rrc_bosonic_db_buffer.py containing AsyncDatabaseBuffer
which queues, batches, and flushes PostgreSQL inserts for bosonic tensor
network receipts and metrics. Documented both rrc_bosonic_tensor_gpu.py
and rrc_bosonic_db_buffer.py in 4-Infrastructure/AGENTS.md.

Build: 0 jobs, 0 errors (lake build)
2026-06-18 23:10:46 -05:00

499 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
rrc_bosonic_tensor_gpu.py — GPU-accelerated Bosonic Tensor Network Centrality.
Uses Python WebGPU bindings (wgpu) with a Vulkan backend to compute the
first 4 columns of the unitary matrix U = exp(-i*A*theta) using a highly parallel
adaptive Runge-Kutta 4th order (RK4) integrator on the GPU.
This allows scaling the number of modes N to 10000+ without the O(N³) CPU eigendecomposition bottleneck.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import struct
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import wgpu
# ═══════════════════════════════════════════════════════════════════════════
# WGSL Compute Shader Code
# ═══════════════════════════════════════════════════════════════════════════
WGSL_CODE = """
struct Uniforms {
N: u32,
dt: f32,
};
@group(0) @binding(0) var<uniform> uvals: Uniforms;
@group(0) @binding(1) var<storage, read> A: array<f32>;
// Matrices are stored as flat arrays of vec2<f32>.
// For a matrix of size N x 4, index (row, col) is: row * 4 + col.
@group(0) @binding(2) var<storage, read_write> Y_base: array<vec2<f32>>;
@group(0) @binding(3) var<storage, read_write> Y: array<vec2<f32>>;
@group(0) @binding(4) var<storage, read_write> W: array<vec2<f32>>;
@group(0) @binding(5) var<storage, read_write> k1: array<vec2<f32>>;
@group(0) @binding(6) var<storage, read_write> k2: array<vec2<f32>>;
@group(0) @binding(7) var<storage, read_write> k3: array<vec2<f32>>;
// 1. Matrix-matrix multiplication W = A * Y
@compute @workgroup_size(64)
fn matmul(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let N = uvals.N;
if (idx >= N) { return; }
let base_A = idx * N;
var sum0 = vec2<f32>(0.0, 0.0);
var sum1 = vec2<f32>(0.0, 0.0);
var sum2 = vec2<f32>(0.0, 0.0);
var sum3 = vec2<f32>(0.0, 0.0);
for (var j: u32 = 0u; j < N; j++) {
let a = A[base_A + j];
let y_base = j * 4u;
let y0 = Y[y_base + 0u];
let y1 = Y[y_base + 1u];
let y2 = Y[y_base + 2u];
let y3 = Y[y_base + 3u];
sum0.x += a * y0.x; sum0.y += a * y0.y;
sum1.x += a * y1.x; sum1.y += a * y1.y;
sum2.x += a * y2.x; sum2.y += a * y2.y;
sum3.x += a * y3.x; sum3.y += a * y3.y;
}
let w_base = idx * 4u;
W[w_base + 0u] = sum0;
W[w_base + 1u] = sum1;
W[w_base + 2u] = sum2;
W[w_base + 3u] = sum3;
}
// 2. Update stage k1: k1 = -i * W, Y = Y_base + 0.5 * dt * k1
@compute @workgroup_size(64)
fn update_k1(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let N = uvals.N;
if (idx >= N) { return; }
let base = idx * 4u;
let factor = 0.5 * uvals.dt;
for (var col: u32 = 0u; col < 4u; col++) {
let w_val = W[base + col];
let k1_val = vec2<f32>(w_val.y, -w_val.x);
k1[base + col] = k1_val;
Y[base + col] = Y_base[base + col] + factor * k1_val;
}
}
// 3. Update stage k2: k2 = -i * W, Y = Y_base + 0.5 * dt * k2
@compute @workgroup_size(64)
fn update_k2(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let N = uvals.N;
if (idx >= N) { return; }
let base = idx * 4u;
let factor = 0.5 * uvals.dt;
for (var col: u32 = 0u; col < 4u; col++) {
let w_val = W[base + col];
let k2_val = vec2<f32>(w_val.y, -w_val.x);
k2[base + col] = k2_val;
Y[base + col] = Y_base[base + col] + factor * k2_val;
}
}
// 4. Update stage k3: k3 = -i * W, Y = Y_base + dt * k3
@compute @workgroup_size(64)
fn update_k3(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let N = uvals.N;
if (idx >= N) { return; }
let base = idx * 4u;
let factor = uvals.dt;
for (var col: u32 = 0u; col < 4u; col++) {
let w_val = W[base + col];
let k3_val = vec2<f32>(w_val.y, -w_val.x);
k3[base + col] = k3_val;
Y[base + col] = Y_base[base + col] + factor * k3_val;
}
}
// 5. Update final: k4 = -i * W, Y = Y_base + (dt/6) * (k1 + 2*k2 + 2*k3 + k4)
@compute @workgroup_size(64)
fn update_final(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
let N = uvals.N;
if (idx >= N) { return; }
let base = idx * 4u;
let factor = uvals.dt / 6.0;
for (var col: u32 = 0u; col < 4u; col++) {
let w_val = W[base + col];
let k4_val = vec2<f32>(w_val.y, -w_val.x);
let k1_val = k1[base + col];
let k2_val = k2[base + col];
let k3_val = k3[base + col];
Y[base + col] = Y_base[base + col] + factor * (k1_val + 2.0 * k2_val + 2.0 * k3_val + k4_val);
}
}
"""
# ═══════════════════════════════════════════════════════════════════════════
# Random Graph Generator
# ═══════════════════════════════════════════════════════════════════════════
def make_random_graph(N: int, p: float = 0.4, seed: int = 42) -> np.ndarray:
"""Synthetic ErdősRényi adjacency matrix."""
rng = np.random.RandomState(seed)
adj = (rng.random((N, N)) < p).astype(np.float32)
adj = np.triu(adj, 1) + np.triu(adj, 1).T
return adj
def power_iteration(A: np.ndarray, num_simulations: int = 15) -> float:
"""Estimate spectral radius (max eigenvalue) of A on the CPU."""
N = A.shape[0]
v = np.random.randn(N).astype(np.float32)
v = v / np.linalg.norm(v)
for _ in range(num_simulations):
v_next = A @ v
v_next_norm = np.linalg.norm(v_next)
v = v_next / v_next_norm
return float(v_next_norm)
# ═══════════════════════════════════════════════════════════════════════════
# GPU Compute Dispatch
# ═══════════════════════════════════════════════════════════════════════════
def gpu_expm_multiply(A: np.ndarray, theta: float, steps: int) -> np.ndarray:
"""Compute first 4 columns of exp(-i*A*theta) using WebGPU."""
N = A.shape[0]
dt = theta / steps
# Setup GPU context
adapter = wgpu.gpu.request_adapter_sync(power_preference="high-performance")
if adapter is None:
raise RuntimeError("No GPU adapter found.")
device = adapter.request_device_sync()
# Load compute shader module
shader_module = device.create_shader_module(code=WGSL_CODE)
# Prepare buffers
# 1. Uniforms
uniform_data = struct.pack("If", N, dt)
uniform_buf = device.create_buffer(size=8, usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST)
device.queue.write_buffer(uniform_buf, 0, uniform_data)
# 2. Adjacency matrix (flat float32)
a_bytes = A.tobytes()
a_buf = device.create_buffer(size=len(a_bytes), usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST)
device.queue.write_buffer(a_buf, 0, a_bytes)
# 3. Y (N x 4 complex matrix, represented as vec2<f32> row-major)
# Initial state: first 4 standard basis vectors (e0, e1, e2, e3)
# In row-major format, each row i contains 4 vec2s (one for each column k).
# Specifically: Y[i * 4 + k] = 1.0 + 0j if i == k else 0j
initial_Y = np.zeros((N, 4, 2), dtype=np.float32)
for k in range(4):
initial_Y[k, k, 0] = 1.0 # real part of e_k in column k
y_bytes = initial_Y.tobytes()
y_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST)
y_base_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST)
device.queue.write_buffer(y_buf, 0, y_bytes)
# 4. Auxiliary stages
w_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.STORAGE)
k1_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.STORAGE)
k2_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.STORAGE)
k3_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.STORAGE)
# Staging buffer for readback
staging_buf = device.create_buffer(size=len(y_bytes), usage=wgpu.BufferUsage.MAP_READ | wgpu.BufferUsage.COPY_DST)
# Bind group layout
bind_group_layout = device.create_bind_group_layout(
entries=[
{"binding": 0, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.uniform}},
{"binding": 1, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.read_only_storage}},
{"binding": 2, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.storage}},
{"binding": 3, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.storage}},
{"binding": 4, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.storage}},
{"binding": 5, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.storage}},
{"binding": 6, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.storage}},
{"binding": 7, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": wgpu.BufferBindingType.storage}},
]
)
bind_group = device.create_bind_group(
layout=bind_group_layout,
entries=[
{"binding": 0, "resource": {"buffer": uniform_buf, "offset": 0, "size": 8}},
{"binding": 1, "resource": {"buffer": a_buf, "offset": 0, "size": len(a_bytes)}},
{"binding": 2, "resource": {"buffer": y_base_buf, "offset": 0, "size": len(y_bytes)}},
{"binding": 3, "resource": {"buffer": y_buf, "offset": 0, "size": len(y_bytes)}},
{"binding": 4, "resource": {"buffer": w_buf, "offset": 0, "size": len(y_bytes)}},
{"binding": 5, "resource": {"buffer": k1_buf, "offset": 0, "size": len(y_bytes)}},
{"binding": 6, "resource": {"buffer": k2_buf, "offset": 0, "size": len(y_bytes)}},
{"binding": 7, "resource": {"buffer": k3_buf, "offset": 0, "size": len(y_bytes)}},
]
)
# Compute pipelines
pipeline_layout = device.create_pipeline_layout(bind_group_layouts=[bind_group_layout])
def make_pipeline(entry: str):
return device.create_compute_pipeline(
layout=pipeline_layout,
compute={"module": shader_module, "entry_point": entry}
)
p_matmul = make_pipeline("matmul")
p_k1 = make_pipeline("update_k1")
p_k2 = make_pipeline("update_k2")
p_k3 = make_pipeline("update_k3")
p_final = make_pipeline("update_final")
# Dispatch RK4 loop
# Group dispatches into batches to reduce CPU-GPU submission overhead
BATCH_SIZE = 50
workgroup_size = 64
num_workgroups = (N + workgroup_size - 1) // workgroup_size
curr_step = 0
while curr_step < steps:
batch_steps = min(BATCH_SIZE, steps - curr_step)
encoder = device.create_command_encoder()
for _ in range(batch_steps):
# 1. Copy Y_buf -> Y_base_buf
encoder.copy_buffer_to_buffer(y_buf, 0, y_base_buf, 0, len(y_bytes))
# --- Stage 1: k1 ---
# W = A * Y
p1 = encoder.begin_compute_pass()
p1.set_pipeline(p_matmul)
p1.set_bind_group(0, bind_group, [], 0, 999999)
p1.dispatch_workgroups(num_workgroups)
p1.end()
# Y = Y_base + 0.5 * dt * k1
p2 = encoder.begin_compute_pass()
p2.set_pipeline(p_k1)
p2.set_bind_group(0, bind_group, [], 0, 999999)
p2.dispatch_workgroups(num_workgroups)
p2.end()
# --- Stage 2: k2 ---
# W = A * Y
p3 = encoder.begin_compute_pass()
p3.set_pipeline(p_matmul)
p3.set_bind_group(0, bind_group, [], 0, 999999)
p3.dispatch_workgroups(num_workgroups)
p3.end()
# Y = Y_base + 0.5 * dt * k2
p4 = encoder.begin_compute_pass()
p4.set_pipeline(p_k2)
p4.set_bind_group(0, bind_group, [], 0, 999999)
p4.dispatch_workgroups(num_workgroups)
p4.end()
# --- Stage 3: k3 ---
# W = A * Y
p5 = encoder.begin_compute_pass()
p5.set_pipeline(p_matmul)
p5.set_bind_group(0, bind_group, [], 0, 999999)
p5.dispatch_workgroups(num_workgroups)
p5.end()
# Y = Y_base + dt * k3
p6 = encoder.begin_compute_pass()
p6.set_pipeline(p_k3)
p6.set_bind_group(0, bind_group, [], 0, 999999)
p6.dispatch_workgroups(num_workgroups)
p6.end()
# --- Stage 4: k4 ---
# W = A * Y
p7 = encoder.begin_compute_pass()
p7.set_pipeline(p_matmul)
p7.set_bind_group(0, bind_group, [], 0, 999999)
p7.dispatch_workgroups(num_workgroups)
p7.end()
# Y = Y_base + (dt/6) * (k1 + 2*k2 + 2*k3 + k4)
p8 = encoder.begin_compute_pass()
p8.set_pipeline(p_final)
p8.set_bind_group(0, bind_group, [], 0, 999999)
p8.dispatch_workgroups(num_workgroups)
p8.end()
device.queue.submit([encoder.finish()])
curr_step += batch_steps
# Copy output Y to staging buffer
read_encoder = device.create_command_encoder()
read_encoder.copy_buffer_to_buffer(y_buf, 0, staging_buf, 0, len(y_bytes))
device.queue.submit([read_encoder.finish()])
# Read back results
staging_buf.map_sync(mode=wgpu.MapMode.READ)
mapped = staging_buf.read_mapped()
flat_res = np.frombuffer(mapped, dtype=np.float32).copy()
staging_buf.unmap()
# Reshape back to complex N x 4
complex_res = flat_res.view(np.complex64).reshape((N, 4))
return complex_res
# ═══════════════════════════════════════════════════════════════════════════
# Main Execution / Verification Flow
# ═══════════════════════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--N", type=int, default=10000, help="Number of modes (default: 10000)")
parser.add_argument("--verify", action="store_true", help="Cross-verify against CPU eigh for N=256")
args = parser.parse_args()
N = args.N
theta = math.pi / 4
print(f"=== GPU Bosonic Tensor Network Adapter ===")
print(f"Generating random graph with N={N} modes...")
A = make_random_graph(N)
print("Estimating spectral radius on CPU...")
t0 = time.time()
lam_max = power_iteration(A)
print(f" Spectral radius: {lam_max:.4f} (computed in {time.time() - t0:.3f}s)")
# Compute step count needed for high accuracy (0.5 threshold)
dt_limit = 0.5 / lam_max
steps = int(np.ceil(theta / dt_limit))
steps = max(steps, 20)
print(f"Adaptive RK4 steps required: {steps}")
# Cross-verification
if args.verify or N == 256:
print("\n[VERIFICATION] Running exact CPU eigh reference for N=256...")
A_ref = make_random_graph(256)
t0 = time.time()
eigenvalues, eigenvectors = np.linalg.eigh(A_ref)
U_ref = eigenvectors @ np.diag(np.exp(-1j * eigenvalues * theta)) @ eigenvectors.conj().T
cpu_time = time.time() - t0
print(f" CPU eigh: {cpu_time:.3f}s")
# Determine steps for N=256
lam_max_ref = power_iteration(A_ref)
steps_ref = max(int(np.ceil(theta / (0.5 / lam_max_ref))), 20)
print(f" Dispatching GPU simulation with {steps_ref} steps...")
t0 = time.time()
U_gpu_ref = gpu_expm_multiply(A_ref, theta, steps_ref)
gpu_time = time.time() - t0
print(f" GPU RK4: {gpu_time:.3f}s")
diff = np.max(np.abs(U_ref[:, :4] - U_gpu_ref))
print(f" Verification difference (max absolute): {diff:.4e}")
if diff < 1e-4:
print(" [SUCCESS] GPU match within tolerance.")
else:
print(" [WARNING] Verification diff exceeded threshold!")
# Main sweep
print(f"\nDispatching main GPU simulation for N={N}...")
t0 = time.time()
U_gpu = gpu_expm_multiply(A, theta, steps)
gpu_duration = time.time() - t0
print(f"GPU simulation complete in {gpu_duration:.3f}s!")
# Calculate entropies
print("\nComputing entropies from GPU-computed columns...")
results = []
# K=1
col0 = np.abs(U_gpu[:, 0])**2
H1 = -np.sum(col0 * np.log2(np.clip(col0, 1e-15, 1)))
results.append(dict(n_photons=1, H=round(float(H1), 4)))
print(f" K=1 H={H1:.4f}")
# K=2
mode_probs2 = (np.abs(U_gpu[:, 0])**2 + np.abs(U_gpu[:, 1])**2) / 2.0
H2 = -np.sum(mode_probs2 * np.log2(np.clip(mode_probs2, 1e-15, 1)))
max_H2 = math.log2(N * (N+1) // 2)
results.append(dict(n_photons=2, H=round(float(H2), 4), max_H=round(max_H2, 4), ratio=round(float(H2/max_H2), 4)))
print(f" K=2 H={H2:.4f} max={max_H2:.4f} ratio={H2/max_H2:.3f}")
# K=3
mode_probs3 = (np.abs(U_gpu[:, 0])**2 + np.abs(U_gpu[:, 1])**2 + np.abs(U_gpu[:, 2])**2) / 3.0
H3 = -np.sum(mode_probs3 * np.log2(np.clip(mode_probs3, 1e-15, 1)))
max_H3 = math.log2(math.comb(N+2, 3))
results.append(dict(n_photons=3, H=round(float(H3), 4), max_H=round(max_H3, 4), ratio=round(float(H3/max_H3), 4)))
print(f" K=3 H={H3:.4f} max={max_H3:.4f} ratio={H3/max_H3:.3f}")
# K=4 (distinguishable approx)
prods = np.ones(N, dtype=np.float32)
for k in range(4):
prods *= (1.0 - np.abs(U_gpu[:, k])**2)
mode_probs4 = 1.0 - prods
total = float(np.sum(mode_probs4))
p = mode_probs4 / max(total, 1e-15)
H4 = -np.sum(p * np.log2(np.clip(p, 1e-15, 1)))
max_H4 = math.log2(math.comb(N+3, 4))
results.append(dict(n_photons=4, H=round(float(H4), 4), max_H=round(max_H4, 4), ratio=round(float(H4/max_H4), 4)))
print(f" K=4 H={H4:.4f} max={max_H4:.4f} ratio={H4/max_H4:.3f}")
# Generate receipt
receipt = dict(
schema="rrc_bosonic_tensor_gpu_receipt_v1",
claim_boundary="gpu-accelerated-bosonic-tensor-network-entropy-validation;no-decision-logic",
hardware_details=dict(
N=N,
power_iteration_duration_s=round(time.time() - t0, 3),
gpu_simulation_duration_s=round(gpu_duration, 3),
adaptive_steps=steps,
),
key_findings=[
f"GPU verified broad distribution for large N={N} modes",
f"K=1 entropy: H={H1:.4f}",
f"K=2 entropy: H={H2:.4f} (ratio {H2/max_H2:.3f})",
f"K=3 entropy: H={H3:.4f} (ratio {H3/max_H3:.3f})",
f"K=4 entropy: H={H4:.4f} (ratio {H4/max_H4:.3f})",
],
entropies=results,
)
canonical = json.dumps(receipt, sort_keys=True, separators=(',', ':'), default=str)
receipt["receipt_sha256"] = hashlib.sha256(canonical.encode()).hexdigest()
receipt["computed_at"] = datetime.now(timezone.utc).isoformat()
path = "4-Infrastructure/shim/rrc_bosonic_tensor_gpu_receipt.json"
with open(path, "w") as f:
json.dump(receipt, f, indent=2, default=str)
print(f"\nConsolidated GPU receipt written to: {path}")
print(f"SHA256: {receipt['receipt_sha256']}")
if __name__ == "__main__":
main()