test: separate topology identity from FP64 table validation
This commit is contained in:
@@ -17,12 +17,12 @@ sys.path.insert(0, str(Path(__file__).parents[1]/'taylor_green'))
|
||||
from CelerisLab.config import LBMConfig
|
||||
import oracle as tg
|
||||
import spacetime_gpu as gpu
|
||||
import spacetime_table_guard as table_guard
|
||||
ref = gpu.ref
|
||||
|
||||
CPU_SHA = '180eef90a1c3f43113fe18cc54981ee4723cfc0cc2724334907ff188022fa770'
|
||||
UNIFORM_SHA = '01e7415ae39456f2de7f0c3d331afb971c49d19353b43038650e32e80737f1e7'
|
||||
CUDA_SHA = 'c47869688f7ac2ec93411c0af233887fe8b77405838299e4ae52c8963fd97fe4'
|
||||
TABLE_SHA = 'f2e62038dc620c5a10bb61d51a88be2afd80488f2f9f4a10c2677cfbe11009cf'
|
||||
STEPS = (256, 512, 768, 1024)
|
||||
TIMES = (.12, .24, .36, .48)
|
||||
FACTORS = dict(velocity_l2=.8, pressure_l2=1., energy_relative=1.)
|
||||
@@ -127,7 +127,7 @@ def main():
|
||||
source = dict(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: sha(p) for p in (Path(__file__), Path(gpu.__file__), Path(gpu.__file__).with_suffix('.cu'),
|
||||
Path(__file__).with_name('test_spacetime_gpu.py'))},
|
||||
Path(__file__).with_name('test_spacetime_gpu.py'), Path(table_guard.__file__))},
|
||||
reference_hashes=gpu.PINS, numpy=np.__version__, checkout=str(ROOT))
|
||||
if source['dirty']:
|
||||
raise RuntimeError('clean committed checkout required')
|
||||
@@ -173,8 +173,7 @@ def main():
|
||||
run = gpu.SpaceTimeGPU(config, (8,8,56,48), context=context)
|
||||
report.update(device=context.name, sm_arch=context.sm_arch, build_metadata=run.build_metadata,
|
||||
table_sha256=gpu.table_hash(run.tables))
|
||||
if report['table_sha256'] != TABLE_SHA:
|
||||
raise RuntimeError('frozen coordinate tables changed')
|
||||
report['table_validation'] = table_guard.validate_tables(run.plan,run.tables)
|
||||
initial = ref.cpu.State(tg.initial((64,64),0,64,.0192), tg.initial((128,128),1,64,.0192))
|
||||
phase = 'warm_reset'
|
||||
initial, report['warmup'] = warm_and_reset(run, initial, check_cap)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""N64 admission check: exact topology and independently rational interpolation.
|
||||
|
||||
Raw FP64 bytes are diagnostic: different LAPACK builds need not produce them
|
||||
identically. The 2e-14 ceiling is the existing interpolation constraint policy,
|
||||
not a universal LAPACK error theorem. This module never changes runtime weights.
|
||||
"""
|
||||
from fractions import Fraction as F
|
||||
from functools import lru_cache
|
||||
|
||||
import numpy as np
|
||||
from spacetime_gpu import table_hash
|
||||
|
||||
STRUCTURE_SHA = 'cd1842952c4dd4df82da8015454c25da9be12b983270236cc65b4044fa38e6e4'
|
||||
ATOL = 2e-14
|
||||
|
||||
|
||||
def _transpose(a):
|
||||
return list(map(list, zip(*a)))
|
||||
|
||||
|
||||
def _multiply(a, b):
|
||||
return [[sum(x*y for x,y in zip(row,column)) for column in zip(*b)] for row in a]
|
||||
|
||||
|
||||
@lru_cache(None)
|
||||
def exact_weights(kind, points):
|
||||
"""Solve the small documented constrained fit using exact rational arithmetic."""
|
||||
if kind == 'face':
|
||||
a = [[F(1),F(s),F(t),F(s*t)] for t in (0,1) for s in (-1,0,1)]
|
||||
e = [[F(1),F(s),F(t),F(s)*F(t)] for s,t in points]
|
||||
b = [[sum(column)/4 for column in zip(*e)]]
|
||||
d = [[F(int(j==1)) for j in range(6)]]
|
||||
elif kind == 'corner':
|
||||
a = [[F(1),F(x),F(y),F(t)] for t in (0,1) for x,y in ((0,0),(-1,0),(1,0),(0,-1),(0,1))]
|
||||
e = [[F(1),F(x),F(y),F(t)] for x,y,t in points]
|
||||
b = [[sum(column)/4 for column in zip(*e)], [F(0),F(0),F(0),F(1)]]
|
||||
d = [[F(int(j==0)) for j in range(10)], [F(int(j==5)-int(j==0)) for j in range(10)]]
|
||||
else:
|
||||
raise ValueError('unsupported interpolation kind')
|
||||
kkt = [r+s for r,s in zip(_multiply(_transpose(a),a),_transpose(b))]
|
||||
kkt += [row+[F(0)]*len(b) for row in b]
|
||||
rhs = _transpose(a)+d
|
||||
n = len(kkt)
|
||||
augmented = [row+data for row,data in zip(kkt,rhs)]
|
||||
for j in range(n):
|
||||
pivot = next(i for i in range(j,n) if augmented[i][j])
|
||||
augmented[j],augmented[pivot] = augmented[pivot],augmented[j]
|
||||
divisor = augmented[j][j]
|
||||
augmented[j] = [v/divisor for v in augmented[j]]
|
||||
for i in range(n):
|
||||
if i != j:
|
||||
factor = augmented[i][j]
|
||||
augmented[i] = [x-factor*y for x,y in zip(augmented[i],augmented[j])]
|
||||
solution = [row[n:] for row in augmented]
|
||||
if _multiply(kkt,solution) != rhs:
|
||||
raise AssertionError('exact interpolation solve failed')
|
||||
return np.array(_multiply(e,solution[:4]), dtype=np.float64)
|
||||
|
||||
|
||||
def validate_tables(plan, tables):
|
||||
"""Reject topology, layout, padding, operator, or constraint changes separately."""
|
||||
structure = table_hash({key:value for key,value in tables.items() if key != 'weights'})
|
||||
if plan.n != 64 or structure != STRUCTURE_SHA:
|
||||
raise ValueError('frozen integer topology changed')
|
||||
w = tables['weights']
|
||||
if (w.dtype != np.dtype(np.float64) or w.shape != (532,4,10) or len(plan.groups) != w.shape[0]
|
||||
or not all(a.flags.c_contiguous for a in tables.values()) or not np.isfinite(w).all()):
|
||||
raise ValueError('weight dtype/shape/layout/finiteness changed')
|
||||
errors = dict(coefficient=0., constant=0., mean=0., temporal=0.)
|
||||
for i,g in enumerate(plan.groups):
|
||||
size = len(g.samples)
|
||||
if size != tables['sizes'][i] or size != {'face':6,'corner':10}.get(g.kind) or len(g.points) != 4:
|
||||
raise ValueError('interpolation group coverage/size changed')
|
||||
if w[i,:,size:].tobytes() != np.zeros((4,10-size),np.float64).tobytes():
|
||||
raise ValueError('weight padding is not exact positive zero')
|
||||
actual = w[i,:,:size]
|
||||
errors['coefficient'] = max(errors['coefficient'],float(abs(actual-exact_weights(g.kind,g.points)).max()))
|
||||
errors['constant'] = max(errors['constant'],float(abs(actual.sum(axis=1)-1).max()))
|
||||
central = np.zeros(size); central[1 if g.kind=='face' else 0] = 1
|
||||
errors['mean'] = max(errors['mean'],float(abs(actual.mean(axis=0)-central).max()))
|
||||
if g.kind == 'corner':
|
||||
early,late = (g.points.index((0.,0.,t)) for t in (-.25,.25))
|
||||
slope = np.zeros(size); slope[0],slope[5] = -1,1
|
||||
errors['temporal'] = max(errors['temporal'],float(abs(2*(actual[late]-actual[early])-slope).max()))
|
||||
if any(error > ATOL for error in errors.values()):
|
||||
raise ValueError(f'exact interpolation/constraint check failed: {errors}; bound={ATOL}')
|
||||
return dict(status='passed', structure_sha256=structure, raw_table_sha256=table_hash(tables),
|
||||
coefficient_oracle='exact rational KKT, rounded once to FP64', atol=ATOL, rtol=0.,
|
||||
maximum_absolute_errors=errors, positive_zero_padding=True)
|
||||
@@ -193,6 +193,40 @@ class Host(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError,'pinned reference'):
|
||||
trajectory.load_references(path,path)
|
||||
|
||||
def test_n64_table_guard_and_balanced_mutation(self):
|
||||
guard=trajectory.table_guard
|
||||
plan=ref.Plan(64,(8,8,56,48)); tables=gpu.pack_plan(plan)
|
||||
self.assertEqual(guard.validate_tables(plan,tables)['status'],'passed')
|
||||
face=next(i for i,g in enumerate(plan.groups) if g.kind=='face')
|
||||
changed={key:value.copy() for key,value in tables.items()}
|
||||
changed['weights'][face,0,0]=np.nextafter(changed['weights'][face,0,0],np.inf)
|
||||
self.assertEqual(guard.validate_tables(plan,changed)['status'],'passed')
|
||||
for mutation in ('coordinate','order','dtype','shape','layout','padding','negative_zero','nan','inf','balanced'):
|
||||
changed={key:value.copy() for key,value in tables.items()}
|
||||
w=changed['weights']
|
||||
if mutation=='coordinate': changed['incoming1'][0,0]^=1
|
||||
elif mutation=='order': changed['origins']=changed['origins'][::-1].copy()
|
||||
elif mutation=='dtype': changed['weights']=w.astype(np.float32)
|
||||
elif mutation=='shape': changed['weights']=w.reshape(532,10,4)
|
||||
elif mutation=='layout': changed['weights']=np.asfortranarray(w)
|
||||
elif mutation=='padding': w[face,0,9]=1e-30
|
||||
elif mutation=='negative_zero': w[face,0,9]=-0.
|
||||
elif mutation=='nan': w[face,0,0]=np.nan
|
||||
elif mutation=='inf': w[face,0,0]=np.inf
|
||||
else:
|
||||
w[face,:2,:2] += 2.**-30*np.array([[1.,-1.],[-1.,1.]])
|
||||
np.testing.assert_array_equal(w[face].sum(axis=1),tables['weights'][face].sum(axis=1))
|
||||
np.testing.assert_array_equal(w[face].mean(axis=0),tables['weights'][face].mean(axis=0))
|
||||
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
|
||||
guard.validate_tables(plan,changed)
|
||||
changed={key:value.copy() for key,value in tables.items()}
|
||||
changed['weights'][-1,0,0]+=1.
|
||||
with self.assertRaises(ValueError):
|
||||
guard.validate_tables(SimpleNamespace(n=64,groups=plan.groups[:-1]),changed)
|
||||
groups=list(plan.groups); groups[0]=replace(groups[0],samples=groups[0].samples[:-1])
|
||||
with self.assertRaisesRegex(ValueError,'coverage/size'):
|
||||
guard.validate_tables(SimpleNamespace(n=64,groups=groups),tables)
|
||||
|
||||
|
||||
class Device(unittest.TestCase):
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user