our_knp/pets-knp/server.py

87 lines
6.3 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.

"""CIFAR pets demo. All learned inference is KNP; host only image processing, fixed pooling/packet routing."""
from http.server import ThreadingHTTPServer,BaseHTTPRequestHandler
from pathlib import Path
from PIL import Image,ImageOps
import base64,io,json,threading,tempfile,subprocess,time,csv,warnings,hashlib
import numpy as np
from worker import DetectorWorker
WORKER=DetectorWorker()
S=Path(__file__).resolve().parent;R=Path.home()/'run/pets-knp';B=Path.home()/'KNP/build/bin';LOCK=threading.Lock();Image.MAX_IMAGE_PIXELS=16_000_000
DATA=np.load(R/'data/test.npz'); samples=[]
for c in (0,1):samples.extend(np.flatnonzero(DATA['labels']==c)[:12].tolist())
samples=sorted(samples)
def encoded(im):
b=io.BytesIO();im.save(b,format='PNG');return 'data:image/png;base64,'+base64.b64encode(b.getvalue()).decode()
MODELINFO=json.loads((R/'final-summary.json').read_text())
def metadata():return MODELINFO
def verify_models():
for name,sha in MODELINFO['files_sha256'].items():
if hashlib.sha256((R/name).read_bytes()).hexdigest()!=sha:raise RuntimeError('Frozen model was changed: '+name)
class Handler(BaseHTTPRequestHandler):
def send(self,code,value,ctype='application/json; charset=utf-8'):
raw=json.dumps(value,ensure_ascii=False).encode() if not isinstance(value,bytes) else value
self.send_response(code);self.send_header('Content-Type',ctype);self.send_header('Content-Length',str(len(raw)));self.send_header('Cache-Control','no-store');self.send_header('X-Content-Type-Options','nosniff');self.end_headers();self.wfile.write(raw)
def do_GET(self):
if self.path in ('/','/pets.html'):return self.send(200,(S/'pets.html').read_bytes(),'text/html; charset=utf-8')
if self.path=='/api/info':return self.send(200,{k:MODELINFO[k] for k in ['version','test_accuracy','test_total','model_sha256','pipeline_sha256','classes']})
if self.path=='/api/samples':return self.send(200,[{'id':i,'label':int(DATA['labels'][i]),'image':encoded(Image.fromarray(DATA['rgb'][i].transpose(1,2,0)))} for i in samples])
self.send(404,{'error':'Не найдено'})
def do_POST(self):
if self.path!='/api/recognize':return self.send(404,{'error':'Не найдено'})
if not LOCK.acquire(blocking=False):return self.send(429,{'error':'GPU занят предыдущей картинкой. Попробуйте через несколько секунд.'})
try:self.recognize()
finally:LOCK.release()
def recognize(self):
try:
size=int(self.headers.get('Content-Length','0'))
if not 0<size<=12_000_000:return self.send(413,{'error':'Слишком большой файл. Максимум 8 МБ.'})
body=json.loads(self.rfile.read(size));uri=body.get('image')
if not isinstance(uri,str) or not uri.startswith('data:image/') or ';base64,' not in uri:raise ValueError('Ожидается изображение')
raw=base64.b64decode(uri.split(',',1)[1],validate=True)
if len(raw)>8_000_000:return self.send(413,{'error':'Максимум 8 МБ'})
with warnings.catch_warnings():
warnings.simplefilter('error',Image.DecompressionBombWarning)
image=Image.open(io.BytesIO(raw))
if image.format not in ('PNG','JPEG','WEBP','BMP'):raise ValueError('Используйте PNG, JPEG, WebP или BMP')
if image.width*image.height>16_000_000:raise ValueError('Изображение больше 16 мегапикселей')
image=ImageOps.exif_transpose(image).convert('RGB').resize((32,32),Image.Resampling.BILINEAR)
px=np.asarray(image).transpose(2,0,1).copy()
except Exception as e:return self.send(400,{'error':'Не удалось прочитать изображение: '+str(e)[:120]})
start=time.monotonic()
try:
verify_models()
with tempfile.TemporaryDirectory(prefix='request-',dir=R) as d:
d=Path(d);px.tofile(d/'image.bin')
WORKER.infer(d/'image.bin',d/'det')
counts=np.fromfile(d/'det_counts.bin',np.int32)
# Fixed root-L2 contrast normalization and count routing, never learned arithmetic.
q=np.sqrt(counts.astype(np.float64));q*=2048/max(float(np.sqrt((q*q).sum())),1)
np.clip(np.rint(q),0,640).astype(np.int32).tofile(d/'encoded.bin')
with (d/'hidden.log').open('w') as log:subprocess.run([str(R/'pets_hidden'),str(d/'encoded.bin'),str(R/'head-v2'),'cuda',str(d/'hidden.bin')],stdout=log,stderr=subprocess.STDOUT,check=True,timeout=15)
with (d/'readout.log').open('w') as log:subprocess.run([str(R/'pets_readout'),str(d/'hidden.bin'),str(R/'head-v2'),'cuda',str(d/'result.csv')],stdout=log,stderr=subprocess.STDOUT,check=True,timeout=15)
row=next(csv.DictReader((d/'result.csv').open()));counts=np.fromfile(d/'det_counts.bin',np.int32);heat=np.fromfile(d/'det_heat.bin',np.int32).tolist()
result={'prediction':int(row['prediction']),'outputs':[int(row['cat']),int(row['dog'])],'tie':int(row['cat'])==int(row['dog']),'input':encoded(image),'heat':heat,'heatSide':14,'detectorSpikes':int(counts.sum()),'milliseconds':round((time.monotonic()-start)*1000),'backend':'KNP CUDA / AltAILIF','model':metadata()['model_sha256']}
self.send(200,result)
except subprocess.TimeoutExpired:self.send(504,{'error':'Превышено время расчёта. Повторите запрос.'})
except Exception as e:
print('INFERENCE ERROR',repr(e),flush=True);self.send(500,{'error':'Ошибка KNP. Подробности в журнале сервера.'})
class Server(ThreadingHTTPServer):
def __init__(self,*args,**kwargs):self.admission=threading.BoundedSemaphore(4);super().__init__(*args,**kwargs)
def process_request(self,request,address):
request.settimeout(15)
if not self.admission.acquire(blocking=False):
try:request.sendall(b'HTTP/1.0 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
finally:self.shutdown_request(request)
return
try:super().process_request(request,address)
except Exception:self.admission.release();raise
def process_request_thread(self,request,address):
try:super().process_request_thread(request,address)
finally:self.admission.release()
if __name__=='__main__':
verify_models()
# Warm graph before announcing readiness. No request/model is fabricated or cached.
with tempfile.TemporaryDirectory(prefix='warmup-',dir=R) as d:
d=Path(d);np.zeros((3,32,32),np.uint8).tofile(d/'black.bin');WORKER.infer(d/'black.bin',d/'black')
print('READY pets server :8082',flush=True)
Server(('0.0.0.0',8082),Handler).serve_forever()