test: add bounded space-time GPU packet prototype
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// Test-only arrival-f space-time coupling. No production scheduler changes.
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Space-time experiment requires unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void collide(float* f, float omega) {
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt_no_force(f, feq, omega);
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void health(float value, unsigned int* counts) {
|
||||
if (!isfinite(value)) atomicAdd(counts + 4, 1u);
|
||||
if (value <= 0.0f) atomicAdd(counts + 5, 1u);
|
||||
}
|
||||
|
||||
extern "C" __global__ void CoarseStart(
|
||||
const float* coarse, const float* fine, float* out, float omega,
|
||||
unsigned int x0, unsigned int y0, unsigned int x1, unsigned int y1,
|
||||
unsigned int* counts)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
const bool covered = x >= x0 && x < x1 && y >= y0 && y < y1;
|
||||
float f[NQ];
|
||||
for (unsigned int q = 0; q < NQ; ++q) {
|
||||
const unsigned long s = q * 4ul * TOTAL_CELLS + 2ul * y * (2u * NX) + 2u * x;
|
||||
f[q] = covered ? 0.25f * (fine[s] + fine[s+1] + fine[s+2u*NX] + fine[s+2u*NX+1])
|
||||
: coarse[index_f(k, q)];
|
||||
}
|
||||
collide(f, omega);
|
||||
for (int q = 0; q < NQ; ++q) if (!covered) health(f[q], counts);
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void Collide(
|
||||
const float* in, float* out, const unsigned char* active, float omega,
|
||||
unsigned int* counts)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
float f[NQ];
|
||||
if (active == nullptr || active[k]) {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = in[index_f(k, q)];
|
||||
collide(f, omega);
|
||||
if (active != nullptr) for (int q = 0; q < NQ; ++q) health(f[q], counts);
|
||||
} else {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
}
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void Pull(
|
||||
const float* in, float* out, const unsigned char* active, unsigned int* counts)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
float f[NQ];
|
||||
if (active == nullptr || active[k]) {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j);
|
||||
stream_pull_load(k, f, in, j);
|
||||
if (active != nullptr) for (int q = 0; q < NQ; ++q) health(f[q], counts);
|
||||
} else {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
}
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void Interpolate(
|
||||
const float* g0, const float* gp, const uint32_t* samples,
|
||||
const uint32_t* origins, const int* sizes, const double* weights,
|
||||
double* pre, float* h, unsigned int groups, unsigned int frozen,
|
||||
unsigned int* counts)
|
||||
{
|
||||
unsigned int group = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (group >= groups) return;
|
||||
const int size = sizes[group];
|
||||
for (int child = 0; child < 4; ++child) {
|
||||
double value = frozen ? (double)g0[origins[group]] : 0.0;
|
||||
if (!frozen) for (int j = 0; j < size; ++j) {
|
||||
const float* source = j < size/2 ? g0 : gp;
|
||||
value += weights[(4u*group+child)*10u+j] * (double)source[samples[10u*group+j]];
|
||||
}
|
||||
const float stored = __double2float_rn(value);
|
||||
pre[4u*group+child] = value;
|
||||
h[4u*group+child] = stored;
|
||||
if (!isfinite(value)) atomicAdd(counts, 1u);
|
||||
if (value <= 0.0) atomicAdd(counts+1, 1u);
|
||||
if (!isfinite(stored)) atomicAdd(counts+2, 1u);
|
||||
if (stored <= 0.0f) atomicAdd(counts+3, 1u);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" __global__ void Inject(
|
||||
float* fine, const float* h, const uint32_t* rows, unsigned int count,
|
||||
unsigned int omit_first)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count || (omit_first && row == 0)) return;
|
||||
fine[rows[2u*row]] = h[rows[2u*row+1]];
|
||||
}
|
||||
|
||||
extern "C" __global__ void Register(
|
||||
const float* predicted, const float* fine, const float* h,
|
||||
const uint32_t* targets, const uint32_t* sources, const int* times,
|
||||
double* registers, unsigned int count, int substep, int mutation)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
double value = substep == 0 ? -(double)predicted[targets[row]] : registers[row];
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const unsigned int slot = 4u*row+j;
|
||||
const int when = times[slot];
|
||||
if (when != substep && !(substep == 0 && when == -1)) continue;
|
||||
if ((mutation == 1 && when == -1) || (mutation == 2 && when == 0)
|
||||
|| (mutation == 3 && when == 1)) continue;
|
||||
value += 0.25 * (double)(when == -1 ? h[sources[slot]] : fine[sources[slot]]);
|
||||
}
|
||||
registers[row] = value;
|
||||
}
|
||||
|
||||
extern "C" __global__ void ApplyRegisters(
|
||||
float* predicted, const double* registers, const uint32_t* targets,
|
||||
unsigned int count, unsigned int* counts)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const unsigned int slot = targets[row];
|
||||
predicted[slot] = __double2float_rn((double)predicted[slot] + registers[row]);
|
||||
health(predicted[slot], counts);
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
"""Test-only arrival-f GPU owner; CUDA compilation/execution requires admission."""
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
from numbers import Integral
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import spacetime_reference as ref
|
||||
|
||||
PINS = {"spacetime_reference.py": "1fe13242b9a41062a1227db4a6f9b695165aefab7f4c0b3b2adff0c9023552bf",
|
||||
"cpu_reference.py": "bba2a06a9f6517b32079ffff15e4060d8c3b7ed35f16ca134e1877226c9e4e1e",
|
||||
"../taylor_green/oracle.py": "83e4d7425d45538278e9fb181af450a36358bd9a5ed6be57ff611dd9ae5eb35b"}
|
||||
|
||||
|
||||
def check_sources():
|
||||
for name, expected in PINS.items():
|
||||
if hashlib.sha256((Path(__file__).parent/name).read_bytes()).hexdigest() != expected:
|
||||
raise RuntimeError(f"reference source changed: {name}")
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
cfg = replace(config)
|
||||
cfg.validate()
|
||||
if (cfg.nx != cfg.ny or not 8 <= cfg.nx <= 64 or cfg.nz != 1
|
||||
or cfg.collision != "SRT" or not cfg.is_d2q9
|
||||
or cfg.streaming != "double_buffer" or cfg.store_precision != "FP32"
|
||||
or cfg.data_type != "FP32" or cfg.ddf_shifting or cfg.les_enabled
|
||||
or cfg.velocity != 0 or cfg.wall_slot_width or cfg.wall_normal_velocity):
|
||||
raise ValueError("experiment requires periodic unforced square N8..64, unshifted FP32 D2Q9/SRT")
|
||||
fine = replace(cfg, nx=2*cfg.nx, ny=2*cfg.ny, viscosity=2*cfg.viscosity)
|
||||
fine.validate()
|
||||
return cfg, fine
|
||||
|
||||
|
||||
def pack_plan(plan):
|
||||
"""Pack reviewed coordinate events; no production transfer builder imports."""
|
||||
n, nf = plan.n, plan.nf
|
||||
groups, receivers = plan.groups, plan.receivers
|
||||
data = dict(samples=np.zeros((len(groups), 10), np.uint32),
|
||||
origins=np.empty(len(groups), np.uint32), sizes=np.empty(len(groups), np.int32),
|
||||
weights=np.zeros((len(groups), 4, 10), np.float64),
|
||||
targets=(receivers[:, 0]*n*n+receivers[:, 1]).astype(np.uint32))
|
||||
lookup = {tuple(row): i for i, row in enumerate(receivers)}
|
||||
packets = [[] for _ in receivers]; incoming = [[], []]
|
||||
for i, g in enumerate(groups):
|
||||
size = len(g.samples)
|
||||
if size not in (6, 10):
|
||||
raise ValueError("unexpected interpolation stencil")
|
||||
data['samples'][i, :size] = g.q*n*n+g.samples
|
||||
data['origins'][i] = g.q*n*n+g.source
|
||||
data['sizes'][i] = size
|
||||
data['weights'][i, :, :size] = g.weights
|
||||
for k, event in enumerate(g.events):
|
||||
if event is None:
|
||||
packets[lookup[(g.q, g.receiver)]].append((-1, 4*i+k))
|
||||
else:
|
||||
m, slot = event
|
||||
incoming[m].append((g.q*nf*nf+slot, 4*i+k))
|
||||
for m, outgoing in enumerate(plan.outgoing):
|
||||
for q, source, target in outgoing:
|
||||
packets[lookup[(int(q), int(target))]].append((m, int(q*nf*nf+source)))
|
||||
if any(len(row) != 4 or len(set(row)) != 4 for row in packets):
|
||||
raise ValueError("each receiver needs four distinct quarter-packets")
|
||||
if len({packet for row in packets for packet in row}) != 4*len(receivers):
|
||||
raise ValueError("packet has multiple receiver owners")
|
||||
data['times'] = np.array([[t for t, _ in row] for row in packets], np.int32)
|
||||
data['sources'] = np.array([[s for _, s in row] for row in packets], np.uint32)
|
||||
for m, rows in enumerate(incoming):
|
||||
data[f'incoming{m}'] = np.array(rows, np.uint32)
|
||||
if len({slot for slot, _ in rows}) != len(rows):
|
||||
raise ValueError("duplicate incoming writer")
|
||||
return {key: np.ascontiguousarray(value) for key, value in data.items()}
|
||||
|
||||
|
||||
def table_hash(data):
|
||||
digest = hashlib.sha256()
|
||||
for key, value in sorted(data.items()):
|
||||
digest.update(f"{key}:{value.dtype}:{value.shape}".encode()+value.tobytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def rounding_ledger(plan, tables, before, after, g0, predicted, fine_phases, pre, h, registers):
|
||||
"""Audit actual stored FP32 phases; never call a missing packet 'rounding'."""
|
||||
maps, n = plan.maps, plan.n
|
||||
f64 = lambda a: np.asarray(a, dtype=np.float64)
|
||||
collision = (f64(g0[:, maps.coarse_active])-f64(before.coarse_f[:, maps.coarse_active])).sum(axis=1)
|
||||
for f, g in fine_phases:
|
||||
collision += .25*(f64(g[:, maps.fine_active])-f64(f[:, maps.fine_active])).sum(axis=1)
|
||||
np.testing.assert_array_equal(h, pre.astype(np.float32))
|
||||
origins = f64(g0).ravel()[tables['origins']]
|
||||
np.testing.assert_allclose(pre.mean(axis=1), origins, rtol=0, atol=2e-14)
|
||||
group = np.bincount(tables['origins']//(n*n), weights=f64(h).mean(axis=1)-origins, minlength=9)
|
||||
packet = np.empty(tables['times'].shape, np.float64)
|
||||
for when, source in ((-1, h), (0, fine_phases[0][1]), (1, fine_phases[1][1])):
|
||||
mask = tables['times'] == when
|
||||
packet[mask] = f64(source).ravel()[tables['sources'][mask]]
|
||||
p = f64(predicted).ravel()[tables['targets']]
|
||||
ideal = -p + .25*packet.sum(axis=1)
|
||||
reg_error = registers-ideal
|
||||
bound = 16*np.finfo(float).eps*(abs(p)+.25*abs(packet).sum(axis=1))
|
||||
if np.any(abs(reg_error) > bound):
|
||||
raise AssertionError("receiver packet register mismatch (not roundoff)")
|
||||
actual = after.coarse_f.ravel()[tables['targets']]
|
||||
np.testing.assert_array_equal(actual, (p+registers).astype(np.float32))
|
||||
q = tables['targets']//(n*n)
|
||||
store_error = np.bincount(q, weights=f64(actual)-(p+registers), minlength=9)
|
||||
register_error = np.bincount(q, weights=reg_error, minlength=9)
|
||||
# directional() must sum converted arrays, not accumulate in FP32.
|
||||
total = lambda s: ref.directional(ref.cpu.State(f64(s.coarse_f), f64(s.fine_f)), maps)
|
||||
raw = total(after)-total(before)-collision
|
||||
unexplained = raw-group-store_error-register_error
|
||||
limit = float(64*np.finfo(float).eps*total(before).sum())
|
||||
np.testing.assert_allclose(unexplained, 0, rtol=0, atol=limit)
|
||||
return dict(raw=raw.tolist(), physical_collision=collision.tolist(),
|
||||
child_rounding=group.tolist(), receiver_rounding=store_error.tolist(),
|
||||
register_rounding=register_error.tolist(), unexplained=unexplained.tolist(),
|
||||
unexplained_bound=limit, minimum_pre=float(pre.min()), minimum_stored=float(h.min()))
|
||||
|
||||
|
||||
class SpaceTimeGPU:
|
||||
"""Caller-owned context, one stream, explicit synchronized arrival state."""
|
||||
def __init__(self, config, bounds, *, context, mode='spacetime', identity=False):
|
||||
check_sources()
|
||||
self.configs = validate_config(config)
|
||||
if mode not in ('spacetime', 'frozen') or type(identity) is not bool:
|
||||
raise ValueError("unsupported interpolation or debug collision mode")
|
||||
self.bounds = tuple(bounds)
|
||||
if len(self.bounds) != 4 or any(isinstance(v, bool) or not isinstance(v, Integral) for v in self.bounds):
|
||||
raise ValueError("rectangle bounds must be four integers")
|
||||
self.plan = ref.Plan(config.nx, self.bounds)
|
||||
self.tables = pack_plan(self.plan)
|
||||
self.ctx, self.mode, self.identity = context, mode, identity
|
||||
self.allocations, self.modules, self.build_metadata = [], [], []
|
||||
self.status, self.step_count, self.device_bytes = 'new', 0, 0
|
||||
import pycuda.driver as cuda
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
if Path(compiler._KERNEL_DIR).resolve() != root/'src/CelerisLab/lbm/kernels':
|
||||
raise RuntimeError("wrong production kernel checkout")
|
||||
self.cuda = cuda
|
||||
with context.activate():
|
||||
self.stream = cuda.Stream()
|
||||
try:
|
||||
source = Path(__file__).with_suffix('.cu')
|
||||
for cfg in self.configs:
|
||||
build = compiler.generate_config(cfg)
|
||||
try:
|
||||
Path(build, source.name).write_bytes(source.read_bytes())
|
||||
ptx = compiler.compile_kernel(context.sm_arch, source=source.name, build_dir=build)
|
||||
self.modules.append(compiler.load_module(ptx))
|
||||
import json
|
||||
metadata = json.loads(Path(ptx+'.json').read_text())
|
||||
metadata['test_entry_sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
self.build_metadata.append(metadata)
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
self.device_tables = {key: self.upload(value) for key, value in self.tables.items()}
|
||||
self.active = self.upload(self.plan.maps.fine_active.astype(np.uint8))
|
||||
self.coarse = [self.allocate(9*config.nx**2*4) for _ in range(4)]
|
||||
self.fine = [self.allocate(9*(2*config.nx)**2*4) for _ in range(3)]
|
||||
self.pre = self.allocate(len(self.plan.groups)*4*8)
|
||||
self.h = self.allocate(len(self.plan.groups)*4*4)
|
||||
self.registers = self.allocate(len(self.plan.receivers)*8)
|
||||
self.counts = self.allocate(6*4)
|
||||
except BaseException:
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
def allocate(self, size):
|
||||
pointer = self.cuda.mem_alloc(size)
|
||||
self.allocations.append(pointer); self.device_bytes += size
|
||||
return pointer
|
||||
|
||||
def upload(self, array):
|
||||
pointer = self.allocate(array.nbytes)
|
||||
self.cuda.memcpy_htod(pointer, array)
|
||||
return pointer
|
||||
|
||||
def read(self, pointer, shape, dtype=np.float32):
|
||||
self.stream.synchronize()
|
||||
out = np.empty(shape, dtype)
|
||||
self.cuda.memcpy_dtoh(out, pointer)
|
||||
return out
|
||||
|
||||
def launch(self, level, name, *args, count=None):
|
||||
cfg = self.configs[level]
|
||||
nt = cfg.threads_per_block
|
||||
grid = ((cfg.nx+nt-1)//nt, cfg.ny) if count is None else ((count+nt-1)//nt, 1)
|
||||
self.modules[level].get_function(name)(*args, block=(nt,1,1), grid=grid, stream=self.stream)
|
||||
|
||||
def initialize(self, state):
|
||||
if (self.status not in ('new', 'ready') or state.phase != 'arrival_f'
|
||||
or isinstance(state.tick, bool) or not isinstance(state.tick, Integral) or state.tick != 0):
|
||||
raise ValueError("initial synchronized arrival state with tick=0 required")
|
||||
arrays = [np.array(a, dtype=np.float32, copy=True, order='C') for a in (state.coarse_f,state.fine_f)]
|
||||
for a, mask in zip(arrays, (self.plan.maps.coarse_active,self.plan.maps.fine_active)):
|
||||
if a.shape != (9,*mask.shape) or not np.isfinite(a[:,mask]).all() or (a[:,mask] <= 0).any():
|
||||
raise ValueError("positive finite leaf populations and exact shapes required")
|
||||
a[:,~mask] = np.nan
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self.cuda.memcpy_htod(self.coarse[0], arrays[0])
|
||||
self.cuda.memcpy_htod(self.fine[0], arrays[1])
|
||||
except BaseException:
|
||||
self.status = 'failed'
|
||||
raise
|
||||
self.status, self.step_count = 'ready', 0
|
||||
|
||||
def download(self):
|
||||
if self.status != 'ready':
|
||||
raise RuntimeError(f"GPU owner is {self.status}")
|
||||
try:
|
||||
with self.ctx.activate():
|
||||
arrays = [self.read(self.coarse[0], (9,self.plan.n,self.plan.n)),
|
||||
self.read(self.fine[0], (9,self.plan.nf,self.plan.nf))]
|
||||
except BaseException:
|
||||
self.status = 'failed'
|
||||
raise
|
||||
for a, mask in zip(arrays, (self.plan.maps.coarse_active,self.plan.maps.fine_active)):
|
||||
a[:,~mask] = np.nan
|
||||
return ref.cpu.State(*arrays, tick=2*self.step_count)
|
||||
|
||||
def step(self, *, mutation=0):
|
||||
"""One audited interval; mutation 1/2/3 drops packets, 4 omits a ghost."""
|
||||
if self.status != 'ready' or type(mutation) is not int or mutation not in range(5):
|
||||
raise RuntimeError("ready owner and known test mutation required")
|
||||
before = self.download()
|
||||
n, nf, t = self.plan.n, self.plan.nf, self.device_tables
|
||||
ng, nr = len(self.plan.groups), len(self.plan.receivers)
|
||||
u32, null = np.uint32, np.uintp(0)
|
||||
omega = [np.float32(0 if self.identity else cfg.omega) for cfg in self.configs]
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self.status = 'advancing'
|
||||
self.cuda.memset_d32_async(self.counts, 0, 6, self.stream)
|
||||
fc, g0, predicted, gp = self.coarse
|
||||
self.launch(0, 'CoarseStart', fc,self.fine[0],g0,omega[0],
|
||||
*(u32(v) for v in self.bounds),self.counts)
|
||||
self.launch(0, 'Pull', g0,predicted,null,self.counts)
|
||||
self.launch(0, 'Collide', predicted,gp,null,omega[0],self.counts)
|
||||
self.launch(0, 'Interpolate', g0,gp,t['samples'],t['origins'],t['sizes'],t['weights'],
|
||||
self.pre,self.h,u32(ng),u32(self.mode=='frozen'),self.counts,count=ng)
|
||||
coarse_g = self.read(g0,(9,n,n)); prediction = self.read(predicted,(9,n,n))
|
||||
fine_phases = []
|
||||
for m in (0,1):
|
||||
f, out, g = self.fine
|
||||
old_f = self.read(f,(9,nf,nf))
|
||||
self.launch(1, 'Collide', f,g,self.active,omega[1],self.counts)
|
||||
fine_phases.append((old_f,self.read(g,(9,nf,nf))))
|
||||
self.launch(0, 'Register', predicted,g,self.h,t['targets'],t['sources'],t['times'],
|
||||
self.registers,u32(nr),np.int32(m),np.int32(mutation),count=nr)
|
||||
rows = self.tables[f'incoming{m}']
|
||||
self.launch(1, 'Inject', g,self.h,t[f'incoming{m}'],u32(len(rows)),u32(mutation==4),count=len(rows))
|
||||
self.launch(1, 'Pull', g,out,self.active,self.counts)
|
||||
self.fine[0], self.fine[1] = out, f
|
||||
self.launch(0, 'ApplyRegisters', predicted,self.registers,t['targets'],u32(nr),self.counts,count=nr)
|
||||
counts = self.read(self.counts,(6,),np.uint32)
|
||||
if counts.any():
|
||||
raise AssertionError(f"boundary/leaf health failed: {counts.tolist()}")
|
||||
coarse_after = self.read(predicted,(9,n,n))
|
||||
coarse_after[:,~self.plan.maps.coarse_active] = np.nan
|
||||
after = ref.cpu.State(coarse_after,self.read(self.fine[0],(9,nf,nf)),tick=2*(self.step_count+1))
|
||||
ledger = rounding_ledger(self.plan,self.tables,before,after,coarse_g,prediction,fine_phases,
|
||||
self.read(self.pre,(ng,4),np.float64),self.read(self.h,(ng,4)),
|
||||
self.read(self.registers,(nr,),np.float64))
|
||||
self.coarse[0], self.coarse[2] = predicted, fc
|
||||
self.step_count += 1
|
||||
self.status = 'ready'
|
||||
return ledger
|
||||
except BaseException:
|
||||
self.status = 'failed'
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
if self.status == 'closed':
|
||||
return
|
||||
with self.ctx.activate():
|
||||
error = None
|
||||
try:
|
||||
self.stream.synchronize()
|
||||
except Exception as exc:
|
||||
error = exc
|
||||
failed = []
|
||||
for pointer in self.allocations:
|
||||
try:
|
||||
pointer.free()
|
||||
except Exception as exc:
|
||||
error = error or exc
|
||||
failed.append(pointer)
|
||||
self.allocations = failed
|
||||
self.status = 'cleanup_failed' if failed else 'closed'
|
||||
if not failed:
|
||||
self.modules.clear()
|
||||
if error:
|
||||
raise error
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, kind, value, traceback):
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
if kind is None:
|
||||
raise
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Explicit host-only checks or admitted tiny GPU audit; never a long trajectory."""
|
||||
import argparse
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(ROOT/'src'))
|
||||
from CelerisLab.config import LBMConfig
|
||||
import spacetime_gpu as gpu
|
||||
ref = gpu.ref
|
||||
RECORDS = []
|
||||
REPORT = None
|
||||
OUTPUT = None
|
||||
GPU_ADMITTED = False
|
||||
|
||||
|
||||
def record(value):
|
||||
RECORDS.append(value)
|
||||
if OUTPUT is not None:
|
||||
ref.write_receipt(OUTPUT,REPORT)
|
||||
|
||||
|
||||
def config(n):
|
||||
return LBMConfig(nx=n,ny=n,viscosity=.0192,velocity=0.,ddf_shifting=False)
|
||||
|
||||
|
||||
def initial(plan, tagged=True):
|
||||
arrays = []
|
||||
for n, mask in ((plan.n,plan.maps.coarse_active),(plan.nf,plan.maps.fine_active)):
|
||||
a = np.broadcast_to(ref.cpu.W[:,None,None], (9,n,n)).copy()
|
||||
if tagged:
|
||||
a *= 1+1e-3*np.random.default_rng(n).standard_normal(a.shape)
|
||||
a[:,~mask] = np.nan
|
||||
arrays.append(a)
|
||||
return ref.cpu.State(*arrays)
|
||||
|
||||
|
||||
class Host(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.plan = ref.Plan(12,(3,3,9,8))
|
||||
|
||||
def test_pins_and_supported_config(self):
|
||||
gpu.check_sources()
|
||||
for n in (12,16,64):
|
||||
c,f = gpu.validate_config(config(n))
|
||||
self.assertAlmostEqual(c.omega,1/.5576)
|
||||
self.assertAlmostEqual(f.omega,1/.6152)
|
||||
for change in (dict(ddf_shifting=True),dict(collision='TRT'),dict(nx=128,ny=128),
|
||||
dict(ny=16),dict(les_enabled=True),dict(velocity=.03)):
|
||||
with self.subTest(change=change), self.assertRaises(ValueError):
|
||||
gpu.validate_config(replace(config(12),**change))
|
||||
|
||||
def test_packed_weights_and_packet_ownership(self):
|
||||
for plan in (self.plan,ref.Plan(16,(4,5,11,12))):
|
||||
t = gpu.pack_plan(plan)
|
||||
self.assertEqual(t['sources'].shape,(len(plan.receivers),4))
|
||||
self.assertEqual(np.count_nonzero(t['times']==-1),24)
|
||||
self.assertEqual(gpu.table_hash(t),gpu.table_hash(gpu.pack_plan(plan)))
|
||||
for i,g in enumerate(plan.groups):
|
||||
np.testing.assert_array_equal(t['weights'][i,:,:len(g.samples)],g.weights)
|
||||
np.testing.assert_allclose(t['weights'][i].sum(axis=1),1,rtol=0,atol=2e-14)
|
||||
np.testing.assert_array_equal(t['samples'][i,:len(g.samples)],g.q*plan.n**2+g.samples)
|
||||
for m in (0,1):
|
||||
self.assertEqual(len(t[f'incoming{m}']),len(plan.outgoing[m]))
|
||||
|
||||
def test_duplicate_writer_is_rejected(self):
|
||||
plan = self.plan
|
||||
groups = list(plan.groups)
|
||||
a,b = next((i,k) for i,g in enumerate(groups) for k,e in enumerate(g.events) if e is not None)
|
||||
event = groups[a].events[b]
|
||||
other = next(i for i,g in enumerate(groups) if i!=a and g.q==groups[a].q and event[0] in [e[0] for e in g.events if e])
|
||||
events = list(groups[other].events)
|
||||
k = next(k for k,e in enumerate(events) if e is not None and e[0]==event[0])
|
||||
events[k] = event; groups[other] = replace(groups[other],events=tuple(events))
|
||||
fake = SimpleNamespace(n=plan.n,nf=plan.nf,groups=groups,receivers=plan.receivers,outgoing=plan.outgoing)
|
||||
with self.assertRaisesRegex(ValueError,'duplicate incoming'):
|
||||
gpu.pack_plan(fake)
|
||||
|
||||
def test_rest_ledger_and_missing_packet_discrimination(self):
|
||||
p,t = self.plan,gpu.pack_plan(self.plan)
|
||||
state = initial(p,False)
|
||||
state = ref.cpu.State(state.coarse_f.astype(np.float32),state.fine_f.astype(np.float32))
|
||||
g0 = np.broadcast_to(ref.cpu.W.astype(np.float32)[:,None,None],(9,p.n,p.n)).copy()
|
||||
pre = np.repeat(g0.ravel()[t['origins'],None].astype(float),4,axis=1)
|
||||
h = pre.astype(np.float32); registers = np.zeros(len(p.receivers))
|
||||
phases = [(state.fine_f,state.fine_f)]*2
|
||||
args = (p,t,state,state,g0,g0,phases,pre,h)
|
||||
ledger = gpu.rounding_ledger(*args,registers)
|
||||
self.assertEqual(max(map(abs,ledger['unexplained'])),0)
|
||||
registers[0] = .01
|
||||
with self.assertRaisesRegex(AssertionError,'register mismatch'):
|
||||
gpu.rounding_ledger(*args,registers)
|
||||
|
||||
def test_second_substep_characteristic_donor(self):
|
||||
p = ref.Plan(16,(4,4,12,12)); t = gpu.pack_plan(p)
|
||||
row = t['incoming1'][t['incoming1'][:,0]==5*32*32+10*32+7][0]
|
||||
self.assertEqual(t['origins'][row[1]//4],5*16*16+4*16+3)
|
||||
|
||||
def test_failed_free_remains_retryable(self):
|
||||
run=gpu.SpaceTimeGPU.__new__(gpu.SpaceTimeGPU)
|
||||
run.ctx=SimpleNamespace(activate=nullcontext); run.stream=Mock()
|
||||
first,second=Mock(),Mock(); first.free.side_effect=[RuntimeError('free failed'),None]
|
||||
run.allocations=[first,second]; run.modules=[]; run.status='ready'
|
||||
with self.assertRaisesRegex(RuntimeError,'free failed'):
|
||||
run.close()
|
||||
self.assertEqual(run.allocations,[first]); self.assertEqual(run.status,'cleanup_failed')
|
||||
run.close()
|
||||
self.assertEqual(run.status,'closed'); self.assertEqual(run.allocations,[])
|
||||
second.free.assert_called_once()
|
||||
|
||||
|
||||
class Device(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not GPU_ADMITTED:
|
||||
raise unittest.SkipTest('CUDA compilation/execution requires explicit tiny-job admission')
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cls.context = CudaContext(0)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.context.close()
|
||||
|
||||
def test_tiny_equivalence_budgets_and_phase(self):
|
||||
for mode,identity in (('spacetime',False),('spacetime',True),('frozen',False),('frozen',True)):
|
||||
with self.subTest(mode=mode,identity=identity), gpu.SpaceTimeGPU(
|
||||
config(12),(3,3,9,8),context=self.context,mode=mode,identity=identity) as run:
|
||||
spec = ref.Spec(n_coarse=12,patch_lo=1,halo=2,tau_coarse=.5576,tau_fine=.6152,
|
||||
collision='identity' if identity else 'srt')
|
||||
for tagged,steps in ((False,2),(True,8)):
|
||||
expected = initial(run.plan,tagged); run.initialize(expected)
|
||||
before = run.download()
|
||||
for k in range(1,steps+1):
|
||||
expected,_ = ref.advance(spec,run.plan,expected,mode)
|
||||
ledger = run.step(); actual = run.download()
|
||||
self.assertEqual(actual.tick,2*k); self.assertEqual(run.step_count,k)
|
||||
for a,b in zip((actual.coarse_f,actual.fine_f),(expected.coarse_f,expected.fine_f)):
|
||||
np.testing.assert_array_equal(np.isnan(a),np.isnan(b))
|
||||
np.testing.assert_allclose(a,b,rtol=0,atol=3e-6,equal_nan=True)
|
||||
total = lambda s: ref.cpu.MOMENTS.T @ ref.directional(
|
||||
ref.cpu.State(s.coarse_f.astype(float),s.fine_f.astype(float)),run.plan.maps)
|
||||
np.testing.assert_allclose(total(actual),total(before),rtol=0,atol=2e-4)
|
||||
if tagged and not identity and k==1:
|
||||
for a,b,tau,mask in zip((actual.coarse_f,actual.fine_f),
|
||||
(expected.coarse_f,expected.fine_f),(.5576,.6152),
|
||||
(run.plan.maps.coarse_active,run.plan.maps.fine_active)):
|
||||
wrong = ref.cpu._collide(spec,b,np.isfinite(b),tau,mask,np.zeros(mask.shape,int))
|
||||
self.assertGreater(float(np.nanmax(abs(a-wrong))),3e-6)
|
||||
if k in (1,2,8):
|
||||
record(dict(case='tiny',mode=mode,identity=identity,tagged=tagged,
|
||||
step=k,device_bytes=run.device_bytes,ledger=ledger))
|
||||
|
||||
def test_omitted_packets_and_ghosts_fail(self):
|
||||
for mutation in (1,2,3,4):
|
||||
with self.subTest(mutation=mutation), gpu.SpaceTimeGPU(
|
||||
config(12),(3,3,9,8),context=self.context) as run:
|
||||
run.initialize(initial(run.plan))
|
||||
with self.assertRaises(AssertionError):
|
||||
run.step(mutation=mutation)
|
||||
self.assertEqual(run.step_count,0)
|
||||
self.assertEqual(run.status,'failed')
|
||||
with self.assertRaises(RuntimeError):
|
||||
run.download()
|
||||
|
||||
def test_frozen_donor_trap(self):
|
||||
with gpu.SpaceTimeGPU(config(16),(4,4,12,12),context=self.context,mode='frozen',identity=True) as run:
|
||||
state = initial(run.plan,False)
|
||||
state.coarse_f[5,4,3]=1.; state.coarse_f[5,5,3]=2.
|
||||
run.initialize(state); run.step()
|
||||
self.assertEqual(run.download().fine_f[5,11,8],1.)
|
||||
# Deliberately negative boundary data are reported and remain unclipped.
|
||||
with self.context.activate():
|
||||
data=np.full((9,16,16),-.01,np.float32)
|
||||
run.cuda.memcpy_htod(run.coarse[1],data); run.cuda.memcpy_htod(run.coarse[3],data)
|
||||
run.cuda.memset_d32_async(run.counts,0,6,run.stream)
|
||||
t=run.device_tables; ng=len(run.plan.groups)
|
||||
run.launch(0,'Interpolate',run.coarse[1],run.coarse[3],t['samples'],t['origins'],
|
||||
t['sizes'],t['weights'],run.pre,run.h,np.uint32(ng),np.uint32(0),run.counts,count=ng)
|
||||
counts=run.read(run.counts,(6,),np.uint32)
|
||||
self.assertEqual(counts[1],4*ng); self.assertEqual(counts[3],4*ng)
|
||||
self.assertTrue((run.read(run.h,(ng,4))<0).all())
|
||||
|
||||
def test_n64_one_interval_only(self):
|
||||
sys.path.insert(0,str(Path(__file__).parents[1]/'taylor_green'))
|
||||
import oracle as tg
|
||||
with gpu.SpaceTimeGPU(config(64),(8,8,56,48),context=self.context) as run:
|
||||
arrays = [tg.initial((64,64),0,64,.0192),tg.initial((128,128),1,64,.0192)]
|
||||
for a,mask in zip(arrays,(run.plan.maps.coarse_active,run.plan.maps.fine_active)):
|
||||
a[:,~mask]=np.nan
|
||||
state = ref.cpu.State(*arrays); run.initialize(state)
|
||||
spec = ref.Spec(n_coarse=64,patch_lo=8,halo=16,tau_coarse=.5576,tau_fine=.6152,collision='srt')
|
||||
expected,_ = ref.advance(spec,run.plan,state)
|
||||
started=time.monotonic(); ledger=run.step(); seconds=time.monotonic()-started
|
||||
actual=run.download()
|
||||
errors=[]
|
||||
for a,b in zip((actual.coarse_f,actual.fine_f),(expected.coarse_f,expected.fine_f)):
|
||||
np.testing.assert_array_equal(np.isnan(a),np.isnan(b))
|
||||
np.testing.assert_allclose(a,b,rtol=0,atol=3e-6,equal_nan=True)
|
||||
errors.append(float(np.nanmax(abs(a-b))))
|
||||
record(dict(case='N64_one_interval',errors=errors,ledger=ledger,seconds=seconds,
|
||||
device_bytes=run.device_bytes,table_sha256=gpu.table_hash(run.tables),
|
||||
build_metadata=run.build_metadata,device=self.context.name))
|
||||
|
||||
|
||||
def main():
|
||||
global REPORT,OUTPUT,GPU_ADMITTED
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
mode=parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument('--host-only',action='store_true')
|
||||
mode.add_argument('--gpu-admitted',action='store_true')
|
||||
parser.add_argument('--output',type=Path)
|
||||
args=parser.parse_args()
|
||||
GPU_ADMITTED=args.gpu_admitted
|
||||
if args.gpu_admitted and (not os.environ.get('CUDA_VISIBLE_DEVICES') or args.output is None):
|
||||
parser.error('GPU job requires explicit CUDA_VISIBLE_DEVICES and --output')
|
||||
if args.output is not None and args.output.exists():
|
||||
parser.error('output exists; retain evidence and choose a new path')
|
||||
started=time.monotonic()
|
||||
report=dict(scope='tiny GPU equivalence and rounding audit; no long trajectory',completed=False,
|
||||
requested_mode='admitted_tiny_gpu' if args.gpu_admitted else 'host_only',
|
||||
commit=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip(),
|
||||
dirty=bool(subprocess.check_output(['git','status','--porcelain'],cwd=ROOT,text=True)),
|
||||
hashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in
|
||||
(Path(__file__),Path(gpu.__file__),Path(gpu.__file__).with_suffix('.cu'))},
|
||||
reference_hashes=gpu.PINS,cuda_visible_devices=os.environ.get('CUDA_VISIBLE_DEVICES'),records=RECORDS)
|
||||
REPORT,OUTPUT=report,args.output
|
||||
if args.output:
|
||||
ref.write_receipt(args.output,report)
|
||||
suite=unittest.TestLoader().loadTestsFromTestCase(Host)
|
||||
if args.gpu_admitted:
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(Device))
|
||||
result=unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
report.update(completed=True,passed=result.wasSuccessful(),tests=result.testsRun,
|
||||
failures=[dict(test=str(test),error=error) for test,error in result.failures+result.errors],
|
||||
seconds=time.monotonic()-started)
|
||||
if args.output:
|
||||
ref.write_receipt(args.output,report)
|
||||
return 0 if result.wasSuccessful() else 1
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user