api1.2 / app.py
bachvnju's picture
Upload 2 files
356103b verified
Raw
History Blame Contribute Delete
72.2 kB
import base64
import gc
import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import threading
import zlib
from pathlib import Path
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import Any, Iterable, Literal, Mapping, Optional, Sequence
# Must be configured before importing torch / llama-cpp.
CPU_THREADS = int(os.getenv("CPU_THREADS", "4"))
MAX_CONTEXT_TOKENS = int(os.getenv("MAX_CONTEXT_TOKENS", "4096"))
MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "1024"))
DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "bachvnju-vbpt-1-0.5B")
MAX_JSON_SCHEMA_BYTES = int(os.getenv("MAX_JSON_SCHEMA_BYTES", "24576"))
MAX_JSON_SCHEMA_DEPTH = int(os.getenv("MAX_JSON_SCHEMA_DEPTH", "16"))
os.environ.setdefault("OMP_NUM_THREADS", str(CPU_THREADS))
os.environ.setdefault("MKL_NUM_THREADS", str(CPU_THREADS))
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
import torch
from fastapi import FastAPI, HTTPException
from huggingface_hub import HfApi, hf_hub_download, snapshot_download
from jsonschema import Draft202012Validator, SchemaError, ValidationError
from pydantic import BaseModel, Field, model_validator
# GGUF and ONNX remain optional: the dedicated VBPT path only needs PyTorch
# and Transformers, so a missing optional backend must not block API startup.
try:
from llama_cpp import Llama
except ImportError:
Llama = None # type: ignore[assignment]
try:
from optimum.onnxruntime import ORTModelForCausalLM
except ImportError:
ORTModelForCausalLM = None # type: ignore[assignment]
from transformers import AutoModelForCausalLM, AutoTokenizer
# -----------------------------------------------------------------------------
# Embedded VBPT loader (activated only for MODEL_CATALOG entries with
# ``loader == "vbpt"``). This API has no dependency on vbpt_dataloader.py.
# -----------------------------------------------------------------------------
VBPT_MODEL_KEY = "bachvnju-vbpt-1-0.5B"
VBPT_REPO_ID = "bachvnju/vbpt-1-0.5B"
# -----------------------------------------------------------------------------
# Embedded audited GDN runtime
# -----------------------------------------------------------------------------
# The public VBPT repository currently ships a remote model implementation that
# imports FLA unconditionally. The benchmark supplied with this project embeds
# a compatible GDN v4.8 implementation with two explicit execution backends:
# * ``fla`` GPU / flash-linear-attention
# * ``reference`` pure PyTorch CPU reference (slow, but no FLA required)
#
# We download weights/tokenizer/config from the VBPT repository but replace only
# its custom Python runtime in a local overlay. This prevents arbitrary latest
# remote Python from being executed at startup and preserves the supplied
# benchmark's CPU fallback. Payload hashes are verified before materialization.
VBPT_RUNTIME_FILES = {'__init__.py': {'compressed_b85': 'c-l)V%Fk8MOU}<r%S<mVN-W9D&x=n_$&1fTPK?jZNmR(pEyyn_QE+$h^L2J~1}o!A1MA7nPf5+m%uC0u+%3PzIkB`jF~`SO2P)(X(!|9TAD@_$6CbZ&s}QY(Y_yV&f)bKK1Q*FDrC2Ti$_PAy',
'sha256': '4cc0967d745c890404f3a6a46ee93ee568b7af03e6628a36947528d60d064778',
'size': 181},
'configuration_gdn_mca_mla.py': {'compressed_b85': 'c-oa&ZBHCI68_Gw@b)U&nFJ<4F4+Vvt3&LuIVp)wr`3IlBID`y%(SN8V!MHX<G){(`>p&kkV8tq<)_N!a=HB2og~S}cD>H4HM<h2WNBTk@^!0($?J-(YQ@Y(vOk59+0h@eFyf0e>=pZbd2#gl(*;XKt3|;+eO@F<a<Ec$$v9uNrd5)2mX}Sf3=>sV8*5M>bQrU1aKyg<pGGRNEacFsM5STmQYyW0{Nb(13q7d)OG$v{mCP<J=-}WWErizW&p*C^f1PmVqvmG^41U1f%V4T!R{!j}GjLU_OFO+!pRX4OR_nSDJE=IhFZDIc!IjF$3iU-%?3k7+7e#(AvjzKW1Kp+AN^K)3??h_YX17#%#=uJ58f#I>M%7uH<}iP-ri~OCt5@*nN-AZ8WS9+$V4FP4WX0@spyx=ZDsK$S<Vv)KSq$^)TrKNN7S>aC&XRRj@iG;>EJR|vw{oYssd>JxYFH~_xE)beDdN8-jnIYzh~J2!l{)#>S%tA+fnj*V`OHf2N6TV$IJR!;R4loMFq~n0z}(5(lMip+O^9wH4&>K&@27r@DlC7NL)tm0)AuLDOx2dWH)YpFkFaAGBuu|bd;{SfVJD{(tQ2>=sda8qvRuk6gAk9<_a`3+Se2SX*h(%NK&*;dSab3yoFlCI8Pi7LBIvwm6FN&6&8Caukn(FenL1t^li_}5AIH3rs?VOq*o|x3pbfcFQf~&A-%JKb)}FB<*XApD^o{+OeW@#4Zu~`uv)i2w+ua=r^J<(?x@oH|nGyn4&70FRwyf&{5Lcqmj%h3!y`yXivradY`R}}_mB3hR$0ed+-`+d}_`Zc@X(Z3`a*Tr8t)A$%nNB<3{f&eWlDgoxd6m_-L;nY|+%^(Uu2O9~ER+EwNV@Rj<751{ICcVbdBI9q)@nBeW`&!v6LMYDM#tQTj@u}RZW#+|n+!LW-DNQ80Z07p%@|oiVhOC~G&g@e4v?^((zv;?gpkqP=#g1|13NW0JANzk^~OwwPvueQ0@UBN44@|t^k;U7(T6CvFrx*DI=1;z6p&}q^{%M3=8%RmISlZ*(7aI2ZajG#howc{5VsZ)SU-AX3U31NeE@T^qetBeRkjVcDL2(T8jpWVoo6i^%w>KD72`Dn#fH|5a+5-`R@+Ujpbi^b$AYr4g5p)Ht{T#Par%~IRB0LxR`Po|-3P$2+uhBHpE|Q5k-F!0HJ+ThMRc3Y)LU6aw?I*^;b2NPGTk;cbVCInE74!7C55QflBWWY`zn>mVaaNhIcGr}x~z5Q6VJ2Gt38EP{wS~!*^dEVzT84GtaT^psZEyx7ilVi9NfHF+_5Pl`gKRB?D@}bH#xA5fwdsn&_-05P#ODp#VS!sz1V-+f;V7s)_JgK>SmT8rt3@&LxuVLsZ)86IosD;^E8M(KUE{f#)afP7+_%6*94<ewQQmM&E|`c@%Zb0P8){~jIR2*+zX>?eoh-MERvV!5t*wrX8Za0mkV(7$mWtm?iLfL>mxe}JS5?EJ(Ng1Y{2t+jX>_{?MuZl<3hc7*2S{R)!jo7d;LAVo(;+yLD3ev85PE3i8qIHJh*0)Ac&;LB}el?56y2>_2^?iAx}CFb?`Xt-5Qz^6tY1;Vdb0+Z4X9hUq3jJcvwmShdq9?7ftx!V0n^+SQTJZ96K__VKWSE4@XI352YdxJyFOWI|=+e8tLSeMWe`H-+(;!)&sdWNWdq1y+9t*^C%`&^}76R7BJ6oieXn=cjBUbdIs{ip3hAEf&Kot6-ror3$)K)rU+T>-H-Il<GLMjB{@OGPv{_vjcQ@X{7FB(iAH<l7Vh`x-ao13aNj<yVqJcCL&7^$?x17t@1H2O2zY}x9-+m<NJJDt4=i~~8Y1-Y4r3k_9BELBJpMe^BTpXa1(bRy=aDWy>g3@8wD9F&MnrR#ujtzZKB!f->8P#*j@=pI%Qi1EPJlDdNuknBZe)rzLEGo6-~})F=BnVkR*r>eHYYDz;YbLcv!maqXOoF!2M;PR@-$4kOAEV&V~6Z3b~16-TES9e@(%uI_K2RvSq|a%<M0d7G_uNO$?&>|F`aehV30X`F*pJjsxGL>8lQJ~hR-{dG|3c9B_V=UXP87~4AmG6T%pMs7%`xJzu|B&vAw(t0q%U1$ich+;92?)rD`=xs6n`wL~jjcs|{O9*0HY}Nv0F?>&1SLe)>N>8b*e5A=mc(cDakWNIa5v4P+?<A3OSD61e#K^<E!?sZoaWDkL&6rC`Wsrs(wA7iD)0YTiabe#eIfX9CfMN&?HPWcCoa_c(Wf^Vpn64_>g-=awvR7?w=l$jVKWLRK@sWX&;t@vZpWptP!i>9yI=U<Kx=FO}VHHZe@Zr6cy>Z{R~V?*2zQr{2^vzOItCf{QMGQjwYaRQ#Z=aMl}|KSnm!coq0*d=GvuI0iqB<YiOH5;COw{F1gxK?6UcbiQbI_35H}i5|Z@;W+2N*g<|_bye&l{)8kDj})k^55tRO?<R-4I5sM%Wy#}nK8I$9T(VXV*=X|+%bqnFIu|7|zdc`eqI!b^zeC-MjOAQDex%wu3Zrpr+iL#ZejO{N+0@y1IN+x#UdXjbkszmVX(=EAo#2e2#2y$4{U%2eBuSFbqG6(Yr@HRGb9AU{tBs%5)DEdi&1~L$yz0ME+AogdrvZG<I%f~g6qD(Z0vDQ;C~O9g40a#I@9~NIT$0c`JF0odJBr917Mt{z>RTfP)@n`#3!CHR5*u`lrcCa<_gow=goq0+dngQUa--0@qxrVOo0yHAzeR}9zdP-*NH0jJ9l82Yad)xtC3tBGqY=565XJjY<ZL!OmEfk(Mb^Cs1T!P7-&!H89@`FS@0QqO<ef0!+C$#qi;6!eeP%=)eNV*p(&-;{AIaxXpp|$CJ#5d|jisni<8H8yc6)mPe7gi9j^$>HV%&kg{?{Zt>5wJ&Cf0DkMG%KU|JZSQOx}Fy7(V5SjpAd%aR&Lx={z9CP}gQJUv^_cK|G)ZZK32EE&-4<5GXEMQ*&D@XUPTp-50(2!T$hT_L%q',
'sha256': '1f6629d282e91ba48d67729daeb8b2487f4cad7a19f33720efaab9cef2b46e68',
'size': 8556},
'modeling_gdn_mca_mla.py': {'compressed_b85': 'c-rNCX?NSmwcvOC3XFVa0?Ht4DV@BGI_wj-m+n5XJD#>X?}Ls8f(S@NL;@55YH^bP{_awH6+p_eb24xGBr*Y1-P&&5x_j;K@Bg^DysYZW=uJK^qg7Ls%cxqdm*uLg`@FB3I=X1us9%)P4|!h}(^uuP&wnoa=xOxs<%{XNw=bf3zUlI1^!8o4xA##Nj8wr@byv30rYpPX4U`6mKfM1n%IgCDA2sW;rDmq(^`^SXm(XnV{{Car&KFTzUX*QFL&u=f%lE&g(NBF;mp1^e+njfO)o*%$n)f}_l3lf1Pxtos_xCQ^W))@G#irl1WtQOxo3@Ac>xKu}?d^$otGr*x-%TffE%I)$tj^`*ZQj-}X&nPD@;;w0^A1NS3aD3HqQ76`9E*>CS!0v=QnlIRXsw5|sJgzb&Nn!gyo>PBE>PF_zOLQ3>BXj=Gkh%YMs$F_Zu)B3rSnBOzgjmHOth%0-|UIvZC-aUnX9tx(#x_IYb4A6px(Tz?y5RahY$&doHd(%z3J7we4lsaJN)(+djFqQzj&W_eG<JS9D4iCem@481|#9VZ_AHuUe#qm&;#|X;>(7Res8aD?~nH)_>U^2^QOM2E;l^3?6Rn{)jZEu%Uq58)z5(SFR9pGc{eZDee@H3dfm27dmKeiV6ED6-bdwKIo~i=QfC%b2l%-x0D0r`j-X+ms{VeWd+@Lh?BO^7-b@Y%)~e6=Wt?Soz5-;Q?CtISN#i5^m0*IC9*18~c%ooCeduBH=4}!E_(rW;#0wqWLTMQKdRfh@9yV}S=D3rKD!;57*s=32#SKZImMiFM*Bz@3al&Z&X4&NZ$FlC4_7ukUb5oa8e$}rZR^ihF;g_qXz0dN^-Og277<SoyPL*nPo-cEtZ4a%uZ07kgyJ&OT<vZ2h6RZBqyAMCZc8GyWz&6<|OR=U!c>&b1s;fTB;;vj?B+;TOin7kS>JxMj2tyK;YnaIkgbrX2%jxe$W&E$(02z-bsRA<5p8#BXTUM6~tOqT>&s)Gt7)i_>h7NYI(cHwT3++Nh_`9e23VRwTaN%?nD0vB8WT-p?lW6*#^+I>KM!HY{WUG2~Ad0u$b=$|PH2!B5Olc-y$m`}dKANPfGOy#JTFs`1Npw|~YyADO-INnJDCIa5xJlYKu???)GosW(Z%a6;YT3&Ja~cQh%4Pm=TmA6%*Iibb$<47vRUWXcm(Y5)ZkxaH%Dx5qo3}9oO^qd$4W)ocB@U><iu~og%DdT{eA$%_@Xh+Gfi9ZcdJuR3P^eiG_!mV?vGR6D<2PwnEjO_}?KT=rCK6A?)VZHE9RRMUy??zrFN*?Bja@JW&{@y{U<LbJP=eiXrK)d$UX|BKH`3?PvnVDQ(ZN9^XaJCe`m&6lQ#(m?1pgvR&-g1f_1mKf)P&D2eQ3)|V2*$=o^LLIU5xi-bNh*EIf<Zi9Tuy5YtjCMb5qTM9xa+ekG(~<u*??u@*=+5dF7Vn1<X$?NZlPUTATVRKB7SZUGO$HLzx2^iKkR6ArKQ?!9y%-=!tr|-ju+|Sl!Yeugbws>!z!S)2IpuL8E`r(IHL{uE9g!fN*ePfwL-}>iKvQoqV4}KPJ(~BzkqKUZ3ave33*Mj<&w+@h3DxLrA+tzAiQ5Tv$DffP=!`99v}BDlv;Jgpv&u<xMp&XWRxqOwvu=!He<};N2tzmIauaSoF<YEW{rTxUA}Kvx@i6`=pqDcitC(Wo8eMI1^e*)Dy%xiR`yf%<7G9Zw<0WsoEp#pe`ib6OA1P3~Is{kkS2l12wy<28aY1c-$@qAp8IyVh=9~D9{opDGx7eN6>d7HdT#>-B?-pDoQHq^X|u&Z>zrirR=I=lP^8$1N`B~+hz*#PnoZfg`{FEMy3%8m@AYzE~EUeBAnooiI5DL=tW4SHLLmM&psH@)uL&Ux-y3%4vE!bp4s2*deC>|(5t=+vqL@YqWI+Gw<(arDf~T6qLV2-;lt^~J0}aA?aQ3jco^dJF#V6;xs{>QeR<bsc%*DSX-$Hp#z?a{LKH!sZmT<h?TlVAP_XlixkU*c<nub>4IG&2iM{<5eDUL%_>tl^4KidYpy6trtpN3jjeQ`vqg}y?-6ZNux?s$JWZS~rgG79Xc1OJ4J0D8CIWz%-18;s6#}c}PH}Ov?+;D|Q_^Q7p=l$YuNc$Gmz94HF8<5a(iyDwXP0@aJxoRpNq=~x|#d=#MOaxV(p7@iDeLYFjG>M*{0wJO%56stxr<(ndYNZd*<sNqaO|v9PJw%G$Z`yJy6;eXeRe;#PxUGOp&vj)Mec!x&F`YN7HE<Q@%Q9Nay@+J8OL?t->Lb))oTCn<T;8L&+@S90?2H-wvoq3;HA1k1_sDF*+f9wy1JuQJ05wASzKe1uFwpRC5QIA>sMa;8X)A$R_DD9*&Pa!ncKJogv;kqmZiMp_#{gm`LF8{R!_W@u7UYcV?JhH3m2F)v^&7x~Z<uvHj?SBA3D3-p?M`=%MhxK%p6p^q%TUaC+FdX$SL^;hwke=BBm)hBQ=w5=w1ir~*E_;D;FT~b0bq*PRd@xQ2s8lu5E-LETCzJ2OFY}h<kcw_38FiG_)xw5H35vu=RM%>3YjLvEEo5F(;09Duysx{2Z1uQwilb_a@v)^TA;eOs>KdXU9M4bbhlLjyA8Q)n`OL5ltQX;b6D8oJCI=12jW(XVI7YoOU5XW=+#PW_LBQM^RQ})*l^i4n{_v%-spOhlj9`9|8Z|mU1}wSx%-LRB_em#<)+yHkLJ@Wp}!#wcaT;IGG(aJn+sC9pPjwgbY)=zfIW{Buc!K9TiaC!{qD=}VXgW3{5?A;Ek14s!OB165Pm|708sPI5*WX}i8|O%i)V-i0+Q6t-Qht<>=<&*d=ld0L~n)LroAH67c7)UFXNnN1r`=fK`O}GGTu>1HL>;*wRJ#{Z6%as-)^e%Ha>_?4o{%~w^svL+#ZvOElCONk5OI_TL4EUK}|@axpTEy3i}CbrCle4{;!c9O_fCnEk=e3Nt&^6B`i?UGQS7Of>-|If@O-jLGzGdTG9q5vO=v<wcXyp`72@SqaNVF8Oq(des=Zj=2`yiJVK33Njh5;$QERG(WDaI8HTIq7Z@1yeZFa_^GsU2EzE2JE8aDD>K~2*I458j3K}`ih1%*^Z;y#vTlM#B|A8Lhps`?&rW8E#qqhx^Y5)z}3OJ3nL|z420ib}x=fH<7n_Ku7X*+LToDJP?>;}|;xq_Y}SKqWiOmmPfC8<)Y)s68N18NVCDrxQRW|b|<9JLzOdwiEkT^6AC(eHk*Ke7%WyU}#*2&66x=8XY0q#|L78-#qD<*~o{{7+)3r8AGV>hJjyW<0CQ%M7;R4N3<-oG(v_)pR1&mB0NWH+~`HjLc6$g+mOE`N8QHF4Gk*6!uE2S)j;`C<5~w%VI!W<U&qdp736jcLsIfwrbadY2K7>&6_~o8^5l*UI4;<wP)A3_BGQM4{8o&M8|#gWx(9*0!B^H2%vlyK*q$5fI9zjpkAOXKE_J;utJ(?*S^qa+t~ikf_t4YV+(+XWmtP*6){RQ?eQ?>Ye2o(8LELjJHWmCQV7{(NNdgRS?sm5tBK*AN$Tu@^7`a<W88K$V1`&SERXl(_s6=qQP$Hw`dQA$W15|P08KB}zdcHs^FgK%4tiu6=5S(b?y23>2FONeT#yZ#R~_<4tjF$<rk8Cs2O@VL-IgHK!AQ_A1G?9wz91xrD49SxIET$-?i>#j@tO9Qb(k0*6i4}GEpxzWWwc9Ot1+{zs@$FT-DT7rVKxg0ve)vkk(-`TO5sFeo)!k7xRUj*M(bU<^=@Rno6&kVZawVhHA?}`sO&>FanD%0%U6#BfxW*O2XW&AxnUp%V$&udeFl50{RVaia^1iw_lx7`?a>rTK)ytN30QmBS6x+<LMfEMWORV}20Qm8!qa7nI&L~@$QIIE^y?)^_~J->8j14)u>2hDqBAA<@kwWhe%GYlP<@EevN=;0MkZ3Eg+^h_Wu*=dqUrPWclN}a(5{A{>3y5uN?Sn?J$OjEd;y{Xijh-s_Q5>pb60IsUbsWjuYthG;-Q=+OB<uwe`=kywlt~$X@!j5c|5@ZO|v_ca~IQr_yk!xTTdq%pnncffH2YP1Cj$F9|B+7l7Mvbeez@S=j5-aslYfQ8i1S^Rojsy2ZAVhC7?36gRbY7r2$lcYwG3wx2%K)jLqgb;4mLDAXG5SXx=vDrO2coiFB75mI7YbyR<HEvri?Im78g76J7BE=j|$kYN7|=F%amkw+zSVpLB9+nSRj1qpd%@haH2@b%s*WHLBRK^r`jniauV2Ki<&CoA5_Ddj9u-F;^93E0tkkMf<DuWGfBZvZzQ*_Uy)stD6M=Ousu{+@Sdcn@h0YHiUxI0JA8@PER#J+#HBox@W_^u!r>2?+J9|20L<vJ%KJH+^h{ntY4b1*JWMAAs|;bayZurgxZ_97~oi-1Xw0Eho-S;n7UjFa`>5a-s_LS2a5i%UT>1NC7s5>Mif^!3Ht2FPD_9Whe!%8#YJcp7b$kMPU;<6x<d*s?LPhO10AQC2z@A*=$VSl-#M%SEN66ZUc$bU%pjcnUEgj<jdVb?gxfwl`{C8k*$*#1zJ8Uxdj0m}i=SVA%)Wo|^3Sh-el@#T=4WRSY~Hgo)$Z9@3Xl1wZ_sQdUB7P^C3BH4e|`0W2-{6wEps%KktPc|){>o}qmFh$n<U3#`=xKUM}5A9zePht$NRVG(z3p(T4?dI?Bo66nE?FX$06@eQaB+9Hac;e=507bG;CD20j>KxSAxngllYGs@%}%Lozo8|=dXx=<Ze9HnP|0v!{Z!g7Jw%afKDQ^=1JJPQ+QnHS)Q4rpP9YE`mz0ydQOXngApz(n<7uEE>nveyK<~VJ{vmBjolMs>uFLP5wNH;lv%XXg?wYqvjr<)OM^P5@Mmk6t>TWVaTf56@X9NuRhGsVN}DW5lvfKG&=LlXnpR&=t?UQHk)Gev_K>>T=(a@)-KKumg<@=GDLY-I=giV|db-TnHv=EFEE|{vAui8s9j48Z`KC_glyCk=?(u*NDQWAKC%-jAd-lM{JO4HPgA!_vy6CwNnRk`p#qX~^Q5uX|!9|hSi#wr$g93ZiEv8KNr)rH)a326qw;zV~MkwVqx_2eH_YaX-T~Ls0l;lxK?>8{v*@Dpyq5Z!Fw&R&IGj=dO4`B%(tuWI}tbuc{a}4HDtb%$I+g?iYt#VTWlB{t6<9aB7LIn7r5DweKaY#Z(i^MSSRJEGR6u%YPn%Kn-yDGdWX1aP>0RJ{$#G1=+8Mh}umgab#e>5Ukq8oH!g##Mo(|dhV8>$?@ae4so4meiMNW?J$fQ0`C5gZVE%eEp(ut=kKD8R_ii~Oc^K$1*BqDX=cCKrH-WxH+xC=w$&e8`3dNU&Pt(Ey#iDY1hXjl@ie2@xnZ9IsHkjeDWy+J{2~2LorK>m}kaUi(-qTQBo@xu6gmz3}$#=yK#CkQJpiPL3n;+dBF~BHke8mP<IXPL)hKUvFZ^GH>k_LLuSG6eBtSzc7adiT5}C#q<yRld<w0ju%~OV~rn#6d)Bp1PqM}JHNh<k<C26@5?Us9i>FIMR`|LmvD}oy9FcI+FoxmY2z&P=CR#x>9{h{2}u&{kNE<{w@#ycho@Ch;-Uye<K+eeh*pRm@D*iO18Uw&ezYywlq)bdHyZ;e)LxMq*0b9nK`@XEi~Nt-AUz2&T)%8!=~s&7vd&~<h&<{dDmE=`0gj~v=(MM39Q$8bBPmr9ilMSNDrYQ)i~@wq2@dc;Dkck=yMVS6iC}D4D-M4z{lL$~v$I`EGa<aQfoq&&q#4ttj4U5c8}uc`FG*C^$VL{kAu3=Cbl%{!IyhK~f1Er&#j#f%NVYx3WhnuUw&#SoQGOSiSN^g!u4hYEz5V(bos&}Kt8(7p*W%sV7jH3W?1em#L$W0lns)vD68VbTDV#f3(f@h&=fCo@6%tg`Qs@XdeWRNrkb}Danr_y%f!gOqhV9hDoQP8gBxzyX6;-aUGWpVR9tst0gGEi7u_&uv*(Eui@3yK7B3`1f`8HtqHjUA;Q%~(qdS>N(*HEhKYzYT@hBz={Kf6};GvT=usx98Nt1cU`on2?=p?+CVM$Gk|jnA=d-t0bXD7(wJp;&m}cKz)mhw{Op+i<+G$H7(CN~uww07Q<h^I@UU2sl9tW?np*13*p%zM#l2*L=F^2Mnm*n%lPG-GcIjXVf+)qQJ@ffIz7SL`V(BO9iZg_F3Xs2q{cQSqaZ$V_VAKxf=sG@v3tO(Zj57h-a?vss>Ed6LrA?+eB#BdRg_cjlB-8xsh+A(N~5^+%K`Y1=YjID5?6@&F<qH-dKYJz7hkbqTJlbV9lNChFqu(YHU|$Y{8Knx)V9lSJ49hka2I*Lw`D^juDO5GjTe}L?E_5+A(osfsZz_=qX9eLUMwyyh1dYqdpYmq@H!l3MrE~P_tFuUB&iJ!1--^$6b)Wz$`oO(Gez89^d<=MWX>XMdorPLE$Hpk_4H4jE`#p(SUK9_6_j;(zRX;aw>QU9Av6D(nl(KP1=L5ES2{@2Bt(;AQ|^ncX5xB%k|Xg@zcZTkAN-s=UNL|B*PUI2HbY6Y(mj&Q(sMqaf$LZP~E#~#nRg?nL05#?_Q7{5FSJYO_djUE8J-*x&=dFqq4YU+W;I1ORij%DE9%o3u|AYXo6~p`_!BsXZ-*d-M~;|$rE$^Vb`lQS7D<fL=#PJNa|0m<qiN>*pX;N0l)$|Jg{_pM8{r~KrF%kai#|{iT;=V@SX7?nwoe9&6&}9!~ho=2PBWh5NG*vITA-a_L=G0NP*CXfnGUph_Zn&P@p$a2-L{Vu#PnBN(ykwyNu0A7+!8cmth**G3d(ao~eQBF@~(Ho6Y4S0zpV1n(`w*AoKizM(Cb&Auz)tNENo90q?v-S7|6y<K|da0@Z038n})fMmsCWD}e8PWA2*Nr#}>^c!wk7$7w*OAXG4FQascHrbiPyYl`G)r`G*Z&~Ubt>ShO;?a=KR@3+8b*l{ydjZC9J5!IQQ-m_i-4Pl^=vF~Qy^e2v4-wQ-f0$wUgOYkNoha%v(8sd?Q4NQy@XPJ!AoQk6`!L44?T+Q$g5F0o$iNT&d#}MMG%lNc#IeJGOG8&o0JcuYrOz8<mDVx(AZQ<X;wsR>72-t>&Jn#Asuz}uDik^`E;$^d1mp$+tsAR^Y4$j7Dw`}_8wyiL;3x{XE|LsxK%^N&v@K|Bj2v(wY|2}$*3m9&Ooi`L2^iHHGqEAjdIyxmeRp{Mt71Ye569Jo1Wc)f8H@*eh5plcZl*CT$W{b|(J=eO~-!^1$XaO^#;vU0G*<aL1frSKB=(Z<JPsz|(w#dI25ZipUW}^aCMz5tPzvxAnEDt6+2V9;nQ1=a#HgC}zb<>p>o29^w)C-Eo9>_sCDg~j@&2T-odU)%FOgubrt>O|e#nfyAte1NfFaSZYY10B%e2LM-S?rDC0mZRdSI0tgn8~x(k>GvB!l1w&-T@h(SYHUNnzk<smdRRJhw-Qx@bWSoDec!7*wCoI45*e|{=VEO3Es`4;3<l&khCY07?Ew}L5#Rn(#I=_i<I*ZOlu=HzPF8r6UvBA3xOJK1ddfeY<X|1Jp!aSRxc>KBEm9GHrla6asboJAtrtfJ*bK?Rhisfv>l8Xg~W343x9p+QqV!4S&A@&8pXP^6R&$%-t1}2<JJXLk``_oMA_A585=F?*aQhknT$2_L<@8@nQ$VJMM_b*!+Q*Po$9hiA2;;<6nR1@!ALy)h8Q^f%h2v(_Y_YsSn!+4gq)|bfS1w(d8wJvib`v8=;Sf^W@0n?oeB?7q|TgPFTw%fu2?zjIE>C$$@zRS{qB6V*iVd}2nM#}v31>mU+X1v7N$Qbsfc*>j3wdr13ETA2W<~<6^NqYCmK(^U=;nu^gbsa2J4HAB-T+X6l89=-Vuv~o^bX6D1{06A;)qir%n-Xs|#323q(noddgUD_^6l7%0M%qKO@{;lG}J3Bn<}ikllLs%`BN$(Rr3D4o|JnSoThF!WP`M_f{OZC_G_w{XZEG{$|zMiF=JH@H-a#mJs6PB(oGJ;`gvEKOfg~BO;<AKb{`9MjTVurr40oB#BFoS@QVhgf^($@6bJKa-L(%Jx?1V3jLtCd&9Lezbx$?FKLuY27scvRR^=uvcG~Lk_et4$!wS0sVS!ObFom3FEIh>!v_z1aW;pt*3p6GzQf~dXP*ec^V<p3)G0Xe9L|L*T`0XX`aY14P8=JW50Zz5rIbf3k};iQs(7hDG5DZI!PCh_9;h<drXo;lCj_zq#v!nB7K1a$QlQQ(<Ku9=3@Mllec|KGZHyGeW?RQD>!6rN;xc9rlYo4ZI2`-LY9oU!A==q=hQp#kSc6?LxPpyJpk_<yYO`e`DawIrQQEKU3bc!z!ZLM(@cxO7xhCCV^gqWW|5^0haju49yw`@OmCJ}->B(h@BehIC_&lZDp@At4cEg6m(~5te?l`%!?VTo9c5j#^Pq1;Fws)PkUHR_ii+68d{6e{&Lwc@XN|^P&Sx*lcEipCx8VSWQdC^5|f5Ei;O&eV<n{#}6`2quYJ2V*y7szL<ss4%@FIS6VXF*(|TgHfo5?69S!aQf!ySj7iCV#5a!qG4W+vg12TRU(x=8@v1t0kbB$U(%Uiev9ELzR)Xmh$xM1Pi?)vvQRyc$i!(j*k)mi1D8f28#v>)WmD><6R94`lCPr5$fK7O1xYkE5cwlUqWKB8Ce2+jB}-9V;6Zv$9*zH#u6_2?3qO+#yrtXB{*UEE>{07zz5g`X7<H(5lsdhXd$*PO<z8rZ$QvRBCnTQQiMh0Tjc_N!P19RIsQ=MdXev*-2Bl6cONM=X2a#kA^}`FU<XbXo+Lv+&sh?HwzEak#}b0!R-aI4g1DSxb~N$1-{%gV`naCvMUlnyOD6FQ6k?u#6d2(+Lj%Tn+e)C7@ILGA>-nN>YLrwmHDN?9#G=q{2Zr2bnyIy~v{W<@ILJ{7_~VZ2B>M@n4A(dG4e9d`rB#G11%%Av=js3c!z8d6PFW@NF4v13IohKjY!ckN&3P7w#t$E}WvjgH>?T0)K{7fFmPZy(hak_F)jEmBzHUcKB$3{k8^SkP(9CWwFRc(L1X99z=Oq~-<<sLt`t^BEp)Ux3yWpv=#Nf<ZF#A}4Oy>N=SD%>agy9iNVz{<UlWnKJn>+;dImGb}#Mr#Ocl_ynrrd#`7(qR*r=jwnbjeR6QE`Lq1?!6g1uEe_;RTR8L-j{RBJG@L{>wN^dMVS&rX!p6BXZDVc>GD%ySRAzxbys)uUkRaGx6hwf2f4k%RH41rWT+f7r5(8v79WlSp3RBFn|^rRJ*JPvP~XRl`$58ph!fC@rE;t;S%*to(r?fDUDYsM8n{3ch2;HSv&;I*`=tYMBB=W^Phc?wiVWw{dg##(_aj6pnFz$pdm0zKOFKL!c0h%wpw0p5n|l9GMjDf4bKtMcs{Cn!jo*Ez8g;$5K(E>HErX{u=_ZM+ro`BukYhPunkFjjF22U^(C;<v90lJliOfybxQi+2~WJl?2T>8c1&vzgs8O#K16$#tmD;ESGjm~5_UBcSTra8jwy?&f%#*I<O7LbZPx71*SX7X0y_?em`f!oD|gWsqwA~XQkf&jSq-GvO)pX~<eUKs&3|>%2Y}vIDoC%Q>+;aYw8olCp^^}p+*9E^x^7F%|BT7TV1E?NDgadAk+}H^qmVe)7`b|MybRPq&Xhy8Im)g}juJdNKOSBg_SDT<cIp<OOG6(8h1pIvmrZ@Sg=~6=iViMtjAuRY`<u&!G}wC`@mT|FREDO4Re74>I-E=f$Lp|BLl#7TCStp$4ntDxuuAikx0hou7Z?&65a>K1Zd;Juwkf5Co(i^f02n3RA+AYAYY$$#WyQ2xmnK;FVY$aiWpWXUedMUeow0}A4TYbj>x?Xsa|D=6G}uxp<<c4l5v%nEX%9_)8S!93`vNOWx(6pRTAVp<$JmSM`9n9%1ABQm)=QUUS~Dm$5TQNhaDJ9w^&qd9$VG>;iQSfuvlOD-Zz1AlXZYWPFtU3-xpwP#Bt1W6<<gYXrUmw;#A*6hK%6)0dk>y?^cWEv5r*fN)zuDzEO-jC8wMvra@5{f?Ph&%?<5G0TzR&D3fCl$od?&PY@G({c?ZYb@N@?W&;uLa8O;JyfA}aovjHY{x10^_WGo|aZi0<Bw7*2O3iDBTtM5v=i~{SUl30){hhRcjbjHhLZGe4)a<kZtAQa?9A*(120>-U$l1;*obSW^fKy!3k_VQtDrvplml4GTZNwAzarbR)18a&s@pI;Oeuk5mp5zE=E!cc4$qB%}X);4%pFf8NYDd{j=_oC00aHOKabj&#(=kTL{%v3d-g(nF~coUdfB~uRF3r()d31cS2Gipuzv^@iQc^jf#PpkP7OSL91X;Vr-vlj}ZR0N)WFK{Nre8}r)kss847v_)@e~27}qlcepMbt2p`y_e|gbqa&c}vVixm?N&tYWL-7M^a_>A*yciuw+A88fzP9d9oDisCXhOXKZDMLM|=lj!eOGy8gb>>nF@#Xej1^{}CofegV?YPe8V{SFtkM+xQa7rN3vm1&zz$eB)^@;%hK`WM_#i*8<reyD!PUOW=1&o|i5!g(6QmSS;J8Mf`-cNnaIEgwC;qsukG5cmI%cI;@RV-HVJlSBsx*>TjIqaO4TDq8z4=9x2>u7XvsO{^DtI23#o_)!@6a;-rh4TGkzlilcq%)?~2_IG<vW3|ND!S*|IzuI5C?TMegt@ulMfo9VGH}_${Q|AAJUHIS7B*T0n>Rc}x<BHI+4Q<<1X68epIz5v=CZ9=K#HE=33fd7S2sk4~^y3_65M2O1V32$<d~^5y{{9DezbrYNz7V017X(1wX^J+B^QK=!>_@LJky3YHiOl4<(mLy5bBp13@fn9eh^s0ml>N`RN|JQHbrThU5EU>p;##N*`yxv84GbDjiZnWd57}+gqFr5#N%&2jVF&aznSh4(EG_LZcP3H+)3s8>0rr%-pC$w39ZV$*uX1rMXAWzEBf;$J9Fl6}WBiJVggImaDDt@z`l;VNE>0BJ)lM@d-qC#e*{00JV@EcPOkUTLA(*b)feL&JM=XwzQ4g{ojL5F~!cW8Ozq+~+g$VXt?)pMz6jhn`;7lY{=oZL0h}}UcRH2>=JfZ#Mlh_d-Gxr`8Z;Y{mDetS~3l>!SER;RU(GC4nDdF2pcO*p7D6B=RpB(QEcPJ8S5)Ccf#7I?Fcjd=wjIw?CZTZkm5DeG~qS9xg=a<DPqbCLueNn`5mpp_t11tXS<NF^`qx-L6Js-`|d7-_Z<9IwSS9wt{HQsy8+eRIZ4_z7wTUzBPZcj~1Y<v#2h8HO98IiRxBAB3>(x&oB+r@3u^c2U*%Q&!Hw&Nk3wLd9{d~oM52;hR}l#D>!LQT0(Fz%oH@bU2Hlb$l8zxr8lD`#h_@sxZf&xmzcx8)T03RJ%We{)lHXdS&oQ!_NQEZS1ptASOhT4V=G8CL=v4dt1|Hp!{}?952Lxe@~nX#*zMcpaTFE;7=0XT0zYaE=Fc(;uK=5r+zMPKoq!Jd9R+6=^4J<mDamP*u-2YLe-j*Htn>tFoy04)GjjE1xf97I@(AHg%C>q|dU<Z*U--NXEXYbW``u28LQ_9!TOS&6pUgvA$s!(p_g~)K8RMX<%(|1{q8$*UGgPaqsBsWJ^c%C22MYmvW01KbNPFrY?r!27mfGX4yPx2vqC#g?GUH4h=~iSYLNkLp>1Gbf9q<aOX5Fs2{Hz(hP3nN%UIcxJdz~C~Fi}#&t7|vdHjpS=M;f9AUUj;(<scgaEuo2pUQyrJ8I-*&#7&w>f?o1G#O6?Rb=-i8k_dh+c#fd+b=rc6b`<nxnB8&hxCC+K)rI|FK~Z_N8G8Eq$2~8-$kp<m9(FkyH44nnWj4dcucO9fa$_5$*z$p(aHH+htzW=|#1KNthm{{~>NgdldR?r+A_Z!->`>j-Kv)TYjXxH^n<lCmuV`0z6|Gd&uDB4#vs5$DXepJ2Wq`sf!BjAp7*p^d49!y!@i0)vDka%ygP{JUV6(MhQ_-IR7(BZYdc6^s3ws>-X_4n}E7;pMBpl)oNF)bE^&Yg)T8u<03XnL`|$Q6J*=p>u?EJNz^jPLl*-HJDs>*>!RS#sg+Fe7iN`YfRaUbV9(}wd(j15o#WrhzzOd=GtHvM&CvBnd33R<Nh6Di8d*2v-P|1so4Nvab~Z?pfYEpc-+jpmuU^tBoVqa>S=N96w_uF*>`W*`1fd4FF>*|(Kw<vKQ|73<#ilr`#%#fqPf)v4l(1w_6G32S%o+O~Upq%!pOKehy{0>C;6OJoqM$tbRY%O<%D9l?TfsZv`@Cj%KMz>N8TRH)+aj~ombA^p?NP)Pw&e=;5~{4)6`(RIyrOT?yKpW^!BPMHpN5*q<njbJzYE9oB>Jfqp<0ypYEvWWIEJ<03kEe8yQxvN##aT3Yu3@f5*1%9-Kk~3WZp?`OYMx=B4b#FZyAHt`N=+7ja7L96H^yUzH4Yi1embDsC&Dsc@0b156~7dYn<U<U1pA?Ac?{?GTcZr{mLM*HuYvLtWUj%DL4}=ld=Yg_aKerF$`79Mn;2fHp5??BY_Ws;PLR~WayyPf|Yc_%XT)Rl@_(h7b2|Yi@uAVvjbm1_2V|qLXmLkSNke(dn6Ui<n0tPlfHV{*=s6fE#c)dV4^mLVn9dF67+k5v0%}<?Du&zA?um|;Y_Bu#%iVs#akF&%&)(O3S$s&`B=5RO~#Q`zjUfCIr%_-SvOr@&0)<3Y(M}+5>iPJ9V&z|zI0~5IZ2mZ-HXY6)3h%sJ;mF1LJnxw;#__A@x1{O&*2{3O3ESu!6P3dVdgHGq?&SF0^wrinYK@8ra~=USovn|7hCaelOM0}m?*+{=EG;my64?c&qdmgtuj5`PN+L6a)peDo_tx(N7x_Yrkqe0P^UpxJDPy@hh+E%&5TFksmrNqo9K?b`p23(ZAKZo9nQRS@j~@L7|>J;=&u9t5P`{ekS>xQB7yq>FRx*Y5qWk;{G*0YLRHnMp<{}TkYXbQb?Ck)OtT6M3vWo?dpVpP^EvQNm`xKm;tGXaVLv4*3=`3;qF1lptE&oAldXEtneE`DJhy#+fJvml?jG8F9J=8U=-#Guyu2u$SxWtZP;ePBpl%Rb!ZbSGixiwY1coDbcY*`6QdH=%Y+blX?5%vS>0VG2r(!5R!EV~Xtl}6FSj!oG_=R5nWu1Qb_1$D^UF!HQl~;zsb~MPHdqZ${_lfX~CU2Yia@^lRCr{YT>p?Zs@s?Iiy}`vAuNU&h*xti69<G?KkQ3!ecdMb|>Jo6lRSy(nAvALgKs(e>6Sk!|pNJVU0^<b_4z6HLFV(;d?~?C92qzBrFVyybR{i2VMhgCe{Z6cNOB=h81E73+$gjoLVyREtF4+A5r&V=vuWn1gjyD$?!H~{aknOX~S+il4FpL~YBM@!)5ev16^rh(3y^L%LKubkLYx`}eg$md&6zzo=A7T2JNGN{bGk|DT(Bra5`2zC!)v~-Pm$RdZdw(wD&sazGRr7Qwg&3ddo`D1&$3m#}OnSZFm;JY@C-}nXq~EO&GC9HUeBH19N;fSVkDZZ((fBaB51b}~MWCJVd$Yk%35(@1Q8tVq!H!wSJzI?pQ?C^?C>`Hty0Nspwvu098D=`FV&b~jn;*kzEk}40xAWGH8S1A6v$}97^zJidKOZ=7>^!5}gZv(NNfXilgW5~9uzPHkaN_r5Lt~+F!)b!;8+`Dfc4x;i+D`@%+F$3#h0uStcfRWZ`><*|_~?h6c1fPkB-8eM&(}LB_Eo4WoZtnZ=+Qaq1Nb&wwkwn0upuv}k<c8-q8yrys@C%|Z~dqvB=H=kM7HvFQ%C(RUiB+3A(e9c)VMcQQeBd?(_l!=fUbaUOdw|-dFeR5F&J_hdNSbdA7VXAsaBB;9Z&|>4<e%yhP;YFkN9d54vcItcQ5Wsu81Hf!GijV&A|(>EowWa6^X53I1V*|C{K2H-7&U1;2SFEU@Ym#?#{957y`hfR_oy?Ev$QDda<ASqO`fwjR#rTaCaZ9Z0>sRl><oo$Pa?^s6qI>1K4q~KOIpm+dIP43kGkqHp$G>S#;<^4sz=roDiM&-#ztc|K7O2v*|JvxHfou-#)uvnqJh+O5AyLnAr0**`kX?mE*rOjLJjLXWaiCqpHG~=)w4>tFtrZt<XiqF2&&FC*Csf2hY88KoW}`&3b3!fz7tq6W|CiGg~P1ka6bs)W$I-mO7Bov~C$v3`KI-oC4bxJxu7Fd{^C7wb-)0otaG$U)D0siCTOcG0<njlq@X9e3zKgL$ag)x2E;?$$<S=#G;a+8>C!#-O0<$_;MudUpi_9{R*ZgzC3+qnY_XU`!&1s&y-84Z+)UdsImpD2=K8_qQ8I~qhk#cG^|dg2i-KkMJ~+oKw2XnUC_iWVVdiyZHpBuOmW#g`+JbR_BS1N3RZeUD>%iA<)aH&v$9>c$eoc|6?uqs&&;^Ai`~K@fVXth5liQ`_r#+u8?-!W5X9t5nIMdYf`ACb#2%11f<hy5E$~dg$Vg=FC*K)E-$)sVrSUcfA(k}*;Xa{z@2&B2d@fio^>fqzq!lYdK_xUrf=*}>00Uzz+qC3kKoL07fp*A-vhU0%-uqsNXe{cMu^&e;!=3;WND_&sW6Rh+w)%j&qK-jwSQA02Vs#CD_oxWDK5t>fgP*H<zT6aL_K6y0Mds(-StIVMgTp;~`I>{Tr2X>KloCWxdMkx>l)<1ZWP&H*0b_X_mC+}7->z`cDWF~_!4xt3|MzGTH7Fq>sgn9ZG2}G*b6KuU*|O^S2&mA(saXTAUjbh>o$}zNNvc_kvk`hOJ+=YWfL~!@(O3;2Pz7oDrW9APSz<B_z16}FH$cA~*?cfF@B`v+4TR;mS^R6>$xu=HCNVWNYK5LZ7uiQqJ2arhU~^+t$gH?uv5)f-(7&hLKcB61`Sf@Yw^h+E91SUNNJ_cEN=ZviqEaKOgopvn*~#H)!mtO^Ar94&=@GfMpO7yLr~Dkr%O{4y$e_`}sj=s&G;kV#+3JJpj3A{^S^vb>hf<Yr8aO^aEiJJK`w1#V0JAVbGlqf9TO0#W)fN-*3?!hkGREnw+cI~sSeohK=|I9HIan)Wcd#UrEY2G<!yTCnXmkqZt7iK!ko{efElI1gP2b1#MLbK1_;+LrD}o|5+L=pMLdyqT=&m1<*VqH{Nvxs-mSXdO0U|w<?vfyDds_xd@b-d=LQ4^7Qv&F{ttUGU;hRVc^eK}mGj)|%Z6o&v7Pj>ua_0VR1H)>xP;+{#noT&wLla!}^QnRs%uS@}>nBy=(?EGCmn+!M7;6TN5lIw(+Oep?GVz^09?!HxZjWVt4ioygOyJ&OF3C3Lk3alMC#A`lZetleMv>CR0qsg0)dtQ5<uf~WKG`H>X6^=?ar!R8_z+Vu@!!L(!N!vv92)-p+#q&!YIwNU#LjLInCp~jKcFlOv|Q7TS7>$G$yY+?U4&+9o5RG{GpJM0iO6JS@guRbYXnZu)UN@B9vd(0$Ha#v5qD-iC4&Z6+e$L3nslD+!rBu;{YnC932mI2g<b}$8G23cO%kAov>4=+5xQXB)xw*Lm3r!1<9ZuYoo=w0p0+(~BsNW*zzD^r3O*Sl_}g>-TE-=>9Ycvf-p-hnc?haj`fnM7m*sBA3N_S{V<dQJbL7O1G<w*`w_MRtg)9>3BBbeafO~LE1`K^Hi=Pjl?GL*g_M(CSp)ssqOIW)=2~L9MUzI}H{TlSqvA<}h=DX;~iP*&OdLl(5vYF$-!(0Z6PqynM_E1W)R-fYl19Nnm7>)aocr`M$&|x-7x0x6$(9tpF%eUB`05>kz4gYa2&I7Q_6lHiDx$KX%R#<BoT?awiEZ2d=x2Lw%TGItH7(ttTA~<6@;o~7Q%nQlvTjkHp5q<<-WRfNC+om)_sj&ZbNg0`p0Jl;*=$(9@d`$j&DinOFW!Cdp0^}jxnV|2(DvoJ@u{gD3ef7Qw&GZQTu?@eqUx!9Fc2!?kQ0?riqc%{XCRbVOrTNo3|3>Ygro=`PYJw^6x#}c@u%-u~E18V(YO#mtGioj{yE8GEN7+?JreHS%t3lpp;$L+H(Zuk(u=8LNBuq2;!v^G&?*TZ1CVhGg3{5X)`sZiD(&T2Qe?AVDVm@Xb$MQMgYz|`<W>9tpY>2k&7R}G%ZuJfwrLs#!W<NvV{cM5oUnvpqD144cbJ{zAKUod}KR4_JE(7?{tNK`_6LvhmDLssy@~G=eBTq7C2jFoY`>mq$%)VL;%n_`FwP@d-QTXV{Xp#03hmXV{{L}M(Ha+t#?keEm?w?L(_fPh1hy2q~hhwv5x^C97F-%WKXBIXu)GH~|5D!t_sTamdIBLp#1<>z&l%@seV&*FUQeJN=wAa0O|5LPRnk!D=2w-5hV5WgoBLYm0h!0ra8({;UncC^yma8Z~J7CPT;ctb34mzG^j=nb*DMr2l&V_t<;{6x!o2<u1X;`uU0`P%JV*',
'sha256': '840c232d2b230d3e5e586694340f58ec50618c58c184a5359730d7b409c8f77f',
'size': 55507}}
VBPT_RUNTIME_VERSION = "gdn-v4.8-embedded-cpu-reference-v5.7"
VBPT_RUNTIME_BACKEND_ENV = "VBPT_RUNTIME_BACKEND" # auto | fla | reference
VBPT_DEVICE_ENV = "VBPT_DEVICE" # auto | cuda | cpu
VBPT_REVISION_ENV = "VBPT_REVISION" # optional HF commit/tag
VBPT_OVERLAY_DIR_ENV = "VBPT_OVERLAY_DIR" # writable directory
VBPT_LOCAL_FILES_ONLY_ENV = "VBPT_LOCAL_FILES_ONLY"
def _parse_bool_env(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
value = raw.strip().lower()
if value in {"1", "true", "yes", "on"}:
return True
if value in {"0", "false", "no", "off"}:
return False
raise RuntimeError(f"{name} must be one of 1/0, true/false, yes/no, on/off.")
def _embedded_runtime_bytes(name: str) -> bytes:
try:
item = VBPT_RUNTIME_FILES[name]
except KeyError as exc:
raise RuntimeError(f"Embedded VBPT runtime is missing {name!r}.") from exc
try:
payload = zlib.decompress(base64.b85decode(item["compressed_b85"].encode("ascii")))
except Exception as exc:
raise RuntimeError(f"Embedded VBPT runtime payload is corrupt for {name}: {exc}") from exc
digest = hashlib.sha256(payload).hexdigest()
if digest != item["sha256"] or len(payload) != int(item["size"]):
raise RuntimeError(
f"Embedded VBPT runtime verification failed for {name}: "
f"sha256={digest}, size={len(payload)}"
)
return payload
def _runtime_manifest() -> dict[str, str]:
return {name: str(item["sha256"]) for name, item in VBPT_RUNTIME_FILES.items()}
def _safe_component(value: str) -> str:
cleaned = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in value)
return cleaned[:96] or "default"
def _link_or_copy(source: Path, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists() or target.is_symlink():
if target.is_dir() and not target.is_symlink():
shutil.rmtree(target)
else:
target.unlink()
try:
target.symlink_to(source.resolve(), target_is_directory=source.is_dir())
return
except OSError:
pass
if source.is_dir():
shutil.copytree(source, target, dirs_exist_ok=True)
return
try:
os.link(source.resolve(), target)
return
except OSError:
shutil.copy2(source, target)
def _materialize_vbpt_overlay(repo_dir: Path, revision: Optional[str]) -> Path:
"""Create a local weight/tokenizer overlay with verified embedded GDN code."""
root = Path(os.getenv(VBPT_OVERLAY_DIR_ENV, str(Path(tempfile.gettempdir()) / "vbpt_gdn_overlay")))
revision_key = _safe_component(revision or "main")
runtime_key = hashlib.sha256(json.dumps(_runtime_manifest(), sort_keys=True).encode("utf-8")).hexdigest()[:16]
destination = root / f"vbpt-{revision_key}-{runtime_key}"
manifest_path = destination / ".vbpt_runtime_manifest.json"
expected_manifest = {
"runtime_version": VBPT_RUNTIME_VERSION,
"runtime_sha256": _runtime_manifest(),
"source_repo": VBPT_REPO_ID,
"source_revision": revision,
}
if manifest_path.exists():
try:
loaded_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if loaded_manifest == expected_manifest:
return destination
except Exception:
pass
staging = root / f".{destination.name}.staging-{os.getpid()}"
if staging.exists():
shutil.rmtree(staging)
staging.mkdir(parents=True, exist_ok=False)
runtime_names = set(VBPT_RUNTIME_FILES)
try:
for source in repo_dir.rglob("*"):
rel = source.relative_to(repo_dir)
if rel.name in runtime_names or "__pycache__" in rel.parts:
continue
target = staging / rel
if source.is_dir():
target.mkdir(parents=True, exist_ok=True)
else:
_link_or_copy(source, target)
for name in sorted(runtime_names):
(staging / name).write_bytes(_embedded_runtime_bytes(name))
manifest_path_staging = staging / ".vbpt_runtime_manifest.json"
manifest_path_staging.write_text(json.dumps(expected_manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if destination.exists():
shutil.rmtree(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
staging.replace(destination)
except Exception:
shutil.rmtree(staging, ignore_errors=True)
raise
return destination
def _fla_available() -> tuple[bool, Optional[str]]:
try:
from fla.layers import GatedDeltaNet # noqa: F401
return True, None
except Exception as exc:
return False, f"{type(exc).__name__}: {exc}"
def _resolve_vbpt_execution() -> tuple[str, torch.device, torch.dtype]:
requested_backend = os.getenv(VBPT_RUNTIME_BACKEND_ENV, "auto").strip().lower()
requested_device = os.getenv(VBPT_DEVICE_ENV, "auto").strip().lower()
if requested_backend not in {"auto", "fla", "reference"}:
raise RuntimeError(f"{VBPT_RUNTIME_BACKEND_ENV} must be auto, fla, or reference.")
if requested_device not in {"auto", "cuda", "cpu"}:
raise RuntimeError(f"{VBPT_DEVICE_ENV} must be auto, cuda, or cpu.")
cuda_ready = torch.cuda.is_available()
fla_ready, fla_error = _fla_available()
if requested_backend == "fla":
if not cuda_ready:
raise RuntimeError("VBPT_RUNTIME_BACKEND=fla requires a CUDA-enabled PyTorch runtime.")
if not fla_ready:
raise RuntimeError(
"VBPT_RUNTIME_BACKEND=fla requires flash-linear-attention[cuda]. "
f"Import detail: {fla_error}"
)
if requested_device == "cpu":
raise RuntimeError("VBPT_RUNTIME_BACKEND=fla cannot run on VBPT_DEVICE=cpu.")
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
return "fla", torch.device("cuda"), dtype
if requested_backend == "reference":
if requested_device == "cuda":
raise RuntimeError(
"The verified VBPT reference runtime is intended for CPU. "
"Use VBPT_RUNTIME_BACKEND=fla with CUDA for GPU inference."
)
return "reference", torch.device("cpu"), torch.float32
# auto: choose fast FLA only when both CUDA and FLA are actually usable;
# otherwise use the embedded exact-operation PyTorch reference on CPU.
if requested_device == "cuda":
if not cuda_ready:
raise RuntimeError("VBPT_DEVICE=cuda was requested but torch.cuda.is_available() is False.")
if not fla_ready:
raise RuntimeError(
"VBPT_DEVICE=cuda requires flash-linear-attention[cuda] for VBPT. "
f"Import detail: {fla_error}"
)
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
return "fla", torch.device("cuda"), dtype
if requested_device == "cpu":
return "reference", torch.device("cpu"), torch.float32
if cuda_ready and fla_ready:
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
return "fla", torch.device("cuda"), dtype
return "reference", torch.device("cpu"), torch.float32
def _download_vbpt_assets(repo_id: str, revision: Optional[str]) -> Path:
# We need weights/tokenizer/config only. The public repository's custom
# Python files are deliberately not used; the local overlay writes the
# verified embedded runtime instead.
return Path(snapshot_download(
repo_id=repo_id,
repo_type="model",
revision=revision,
local_files_only=_parse_bool_env(VBPT_LOCAL_FILES_ONLY_ENV, default=False),
ignore_patterns=list(VBPT_RUNTIME_FILES),
))
def _load_vbpt_with_embedded_runtime(repo_id: str) -> tuple[Any, Any, "VBPTDataLoader", str, str, str, Optional[str]]:
revision = os.getenv(VBPT_REVISION_ENV) or None
runtime_backend, device, dtype = _resolve_vbpt_execution()
# The embedded runtime reads this setting during model construction.
os.environ["GDN_GATED_DELTANET_BACKEND"] = runtime_backend
source_dir = _download_vbpt_assets(repo_id, revision)
overlay_dir = _materialize_vbpt_overlay(source_dir, revision)
print(
f"[vbpt-api] VBPT loader=embedded-overlay backend={runtime_backend} device={device} overlay={overlay_dir}",
flush=True,
)
model, tokenizer, loader = VBPTDataLoader.load_pretrained(
str(overlay_dir),
device=device,
dtype=dtype,
trust_remote_code=True,
local_files_only=True,
)
return model, tokenizer, loader, runtime_backend, str(device), str(dtype).replace("torch.", ""), revision
@dataclass(frozen=True)
class VBPTPreparedBatch:
"""Tokenized, left-padded batch ready for `model.generate`."""
inputs: Mapping[str, torch.Tensor]
prompts: list[str]
prompt_style: str
prompt_token_count: int
class VBPTDataLoader:
"""Model-specific prompt renderer and batch tokenizer for VBPT.
The name is intentional: it owns data preparation for VBPT and can create
batches, while model loading remains explicit and auditable.
"""
def __init__(self, tokenizer: Any, *, device: torch.device | str = "cpu") -> None:
self.tokenizer = tokenizer
self.device = torch.device(device)
self._repair_special_tokens()
# Left padding keeps every generated continuation after the common,
# padded input width. It also preserves the newest user turns when
# context truncation is enabled.
self.tokenizer.padding_side = "left"
self.tokenizer.truncation_side = "left"
@classmethod
def load_pretrained(
cls,
repo_id: str = VBPT_REPO_ID,
*,
device: torch.device | str = "cpu",
dtype: torch.dtype = torch.float32,
trust_remote_code: bool = True,
local_files_only: bool = False,
) -> tuple[Any, Any, "VBPTDataLoader"]:
"""Load VBPT from a local verified overlay with the benchmark dtype fallback."""
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
repo_id,
trust_remote_code=trust_remote_code,
local_files_only=local_files_only,
)
# `dtype=` is the newer Transformers spelling used by the supplied
# benchmark. Older installations require `torch_dtype=`.
common = {
"trust_remote_code": trust_remote_code,
"low_cpu_mem_usage": True,
"local_files_only": local_files_only,
}
try:
model = AutoModelForCausalLM.from_pretrained(
repo_id,
dtype=dtype,
**common,
)
except TypeError as exc:
text = str(exc).lower()
if "dtype" not in text and "unexpected keyword" not in text:
raise
model = AutoModelForCausalLM.from_pretrained(
repo_id,
torch_dtype=dtype,
**common,
)
model.to(device)
model.eval()
loader = cls(tokenizer, device=device)
loader.apply_generation_token_ids(model)
return model, tokenizer, loader
def _repair_special_tokens(self) -> None:
"""Guarantee padding works without adding/resizing vocabulary entries."""
if self.tokenizer.pad_token_id is None:
if self.tokenizer.eos_token_id is None or self.tokenizer.eos_token is None:
raise RuntimeError(
"VBPT tokenizer has no pad token and no eos token; refusing to "
"invent a vocabulary token because that would require embedding resize."
)
self.tokenizer.pad_token = self.tokenizer.eos_token
def apply_generation_token_ids(self, model: Any) -> None:
"""Synchronize tokenizer pad/eos IDs into config and GenerationConfig."""
pad_id = int(self.tokenizer.pad_token_id)
eos_id = self.tokenizer.eos_token_id
for config in (getattr(model, "config", None), getattr(model, "generation_config", None)):
if config is None:
continue
if getattr(config, "pad_token_id", None) is None:
config.pad_token_id = pad_id
if eos_id is not None and getattr(config, "eos_token_id", None) is None:
config.eos_token_id = eos_id
if getattr(model, "generation_config", None) is not None:
model.generation_config.use_cache = True
@staticmethod
def _normalize_messages(messages: Sequence[Mapping[str, str]]) -> list[dict[str, str]]:
normalized: list[dict[str, str]] = []
allowed_roles = {"system", "user", "assistant"}
for item in messages:
role = str(item.get("role", "")).strip().lower()
content = str(item.get("content", ""))
if role not in allowed_roles:
raise ValueError(f"VBPT does not support chat role {role!r}.")
if not content.strip():
continue
normalized.append({"role": role, "content": content})
if not normalized:
raise ValueError("VBPT requires at least one non-empty message.")
return normalized
@staticmethod
def _raw_benchmark_compatible_prompt(messages: Sequence[Mapping[str, str]]) -> str:
"""Fallback compatible with the benchmark's raw-prompt route.
For the common one-turn request, this is simply system text followed by
the user text: no unverified ChatML/Qwen markers are injected. Multi-turn
histories use minimal Vietnamese role headers only to keep turns distinct.
"""
normalized = VBPTDataLoader._normalize_messages(messages)
non_system = [item for item in normalized if item["role"] != "system"]
systems = [item["content"].strip() for item in normalized if item["role"] == "system"]
if len(non_system) <= 1:
sections = [part for part in systems + [item["content"].strip() for item in non_system] if part]
return "\n\n".join(sections)
role_names = {
"system": "Hệ thống",
"user": "Người dùng",
"assistant": "Trợ lý",
}
chunks = [
f"{role_names[item['role']]}:\n{item['content'].strip()}"
for item in normalized
]
return "\n\n".join(chunks)
def render_messages(self, messages: Sequence[Mapping[str, str]]) -> tuple[str, str]:
"""Render one request using native template first, raw fallback second."""
normalized = self._normalize_messages(messages)
template = getattr(self.tokenizer, "chat_template", None)
if template:
try:
rendered = self.tokenizer.apply_chat_template(
normalized,
tokenize=False,
add_generation_prompt=True,
)
if isinstance(rendered, str) and rendered.strip():
return rendered, "native_chat_template"
except Exception:
# The raw fallback is deliberate: the supplied benchmark also
# ignores broken templates and preserves the original prompt.
pass
return self._raw_benchmark_compatible_prompt(normalized), "raw_benchmark_compatible"
def prepare_batch(
self,
messages_batch: Iterable[Sequence[Mapping[str, str]]],
*,
context_tokens: int,
) -> VBPTPreparedBatch:
"""Render, tokenize, left-pad, truncate, and move a VBPT batch to device."""
if context_tokens < 1:
raise ValueError("context_tokens must be positive.")
rendered = [self.render_messages(messages) for messages in messages_batch]
if not rendered:
raise ValueError("VBPT batch must contain at least one request.")
prompts, styles = zip(*rendered)
encoded = self.tokenizer(
list(prompts),
return_tensors="pt",
padding=True,
truncation=True,
max_length=context_tokens,
return_attention_mask=True,
)
device_inputs = {
key: value.to(self.device)
for key, value in encoded.items()
if isinstance(value, torch.Tensor)
}
return VBPTPreparedBatch(
inputs=device_inputs,
prompts=list(prompts),
prompt_style=styles[0] if len(set(styles)) == 1 else "mixed",
prompt_token_count=int(device_inputs["input_ids"].shape[1]),
)
def decode_new_tokens(
self,
output_ids: torch.Tensor,
*,
padded_prompt_tokens: int,
) -> list[str]:
"""Decode only model continuations, never the padded prompt prefix."""
if output_ids.ndim != 2:
raise RuntimeError("Expected generated token IDs with shape [batch, sequence].")
if output_ids.shape[1] < padded_prompt_tokens:
raise RuntimeError("Generated sequence is shorter than the prepared prompt.")
new_ids = output_ids[:, padded_prompt_tokens:]
return [
self.tokenizer.decode(row, skip_special_tokens=True).strip()
for row in new_ids
]
# Public clients can only request keys in this catalog.
# Add models here instead of accepting arbitrary Hugging Face repo IDs.
#
# GGUF quant policy when gguf_quant="auto":
# <= 1B -> Q6 first
# 1B–4B -> Q4 first
# > 4B -> Q3 first
# The loader falls back to the next available quantization only for "auto".
# Explicit choices such as gguf_quant="q3" remain strict.
#
# Do not set chat_format unless a particular GGUF has been tested and needs an
# explicit override. Modern GGUFs normally include tokenizer.chat_template;
# leaving chat_format unset lets llama-cpp-python use that model metadata.
MODEL_CATALOG = {
"jackrong-qwen35-0.8b-gguf": {
"backend": "gguf",
"repo": "Jackrong/Qwen3.5-0.8B-Claude-4.6-Opus-Reasoning-Distilled-GGUF",
"params_b": 0.8,
"label": "Jackrong Qwen3.5 0.8B GGUF (auto Q6)",
},
"jackrong-qwen35-9b-gguf": {
"backend": "gguf",
"repo": "Jackrong/Qwen3.5-9B-Claude-4.6-Opus-Reasoning-Distilled-GGUF",
"params_b": 9.0,
"label": "Jackrong Qwen3.5 9B GGUF (auto Q3)",
},
"qwen3-vl-2b-gguf-text": {
"backend": "gguf",
"repo": "Qwen/Qwen3-VL-2B-Instruct-GGUF",
"params_b": 2.0,
"label": "Qwen3-VL 2B GGUF text mode (auto Q4)",
"note": (
"Text mode only. Image understanding requires the compatible "
"multimodal projector/mmproj and an extra vision endpoint."
),
},
"hauhau-qwen35-2b-transformers": {
"backend": "transformers",
"repo": "HauhauCS/Qwen3.5-2B-Uncensored-HauhauCS-Aggressive",
"label": "HauhauCS Qwen3.5 2B Transformers FP32 CPU",
},
"mihai-qwen3-0.6b-transformers": {
"backend": "transformers",
"repo": "MihaiPopa-1/Qwen-3-0.6B-Claude-4.7-Opus-Distilled",
"label": "MihaiPopa Qwen3 0.6B Transformers FP32 CPU",
},
"bachvnju-vbpt-1-0.5B": {
"backend": "transformers",
"repo": "bachvnju/vbpt-1-0.5B",
"loader": "vbpt",
"label": "bachvnju/vbpt-1-0.5B Transformers F32 CPU",
"note": (
"Uses the dedicated VBPT dataloader: native tokenizer chat template "
"when valid, otherwise benchmark-compatible raw prompt batching."
),
},
# Example: enable only after replacing the repo with a real pre-exported
# ONNX causal language model repository.
#
# "my-onnx-model": {
# "backend": "onnx",
# "repo": "YOUR-ONNX-EXPORT-REPO",
# "label": "ONNX Runtime CPU model",
# },
}
if DEFAULT_MODEL not in MODEL_CATALOG:
raise RuntimeError(
f"DEFAULT_MODEL={DEFAULT_MODEL!r} is not in MODEL_CATALOG: "
f"{', '.join(MODEL_CATALOG)}"
)
model = None
tokenizer = None
loaded_model_key: Optional[str] = None
loaded_backend: Optional[str] = None
loaded_context_tokens: Optional[int] = None
# Non-None only for model families with a dedicated inference dataloader.
model_dataloader: Optional[VBPTDataLoader] = None
loaded_prompt_adapter: Optional[str] = None
loaded_vbpt_runtime_backend: Optional[str] = None
loaded_vbpt_device: Optional[str] = None
loaded_vbpt_dtype: Optional[str] = None
loaded_vbpt_revision: Optional[str] = None
# CPU inference is serialized. Multiple concurrent generation jobs typically make
# a small CPU Space slower and can trigger OOM.
engine_lock = threading.RLock()
class ChatMessage(BaseModel):
role: Literal["user", "assistant"]
content: str = Field(..., min_length=1, max_length=12000)
def _json_depth(value: Any, depth: int = 0) -> int:
if isinstance(value, dict):
if not value:
return depth + 1
return max(_json_depth(item, depth + 1) for item in value.values())
if isinstance(value, list):
if not value:
return depth + 1
return max(_json_depth(item, depth + 1) for item in value)
return depth
def validate_response_schema(schema: dict[str, Any]) -> None:
"""Reject pathological client schemas before llama.cpp compiles a grammar."""
try:
encoded = json.dumps(schema, ensure_ascii=False, separators=(",", ":"))
except (TypeError, ValueError) as exc:
raise ValueError(f"response_schema must be JSON-serializable: {exc}") from exc
if len(encoded.encode("utf-8")) > MAX_JSON_SCHEMA_BYTES:
raise ValueError(
f"response_schema exceeds MAX_JSON_SCHEMA_BYTES={MAX_JSON_SCHEMA_BYTES}"
)
if _json_depth(schema) > MAX_JSON_SCHEMA_DEPTH:
raise ValueError(
f"response_schema nesting exceeds MAX_JSON_SCHEMA_DEPTH={MAX_JSON_SCHEMA_DEPTH}"
)
try:
Draft202012Validator.check_schema(schema)
except SchemaError as exc:
raise ValueError(f"Invalid Draft 2020-12 JSON Schema: {exc.message}") from exc
class GenerateRequest(BaseModel):
model: str = DEFAULT_MODEL
# Either `prompt` or `messages` is required.
prompt: Optional[str] = Field(default=None, max_length=12000)
messages: list[ChatMessage] = Field(default_factory=list, max_length=40)
system: str = Field(
default="Bạn là trợ lý AI hữu ích. Trả lời rõ ràng bằng tiếng Việt.",
max_length=4000,
)
# Context budget for this request. GGUF models reload when this changes,
# because n_ctx is set during llama.cpp model initialization.
context_tokens: int = Field(default=2048, ge=256, le=MAX_CONTEXT_TOKENS)
max_new_tokens: int = Field(default=256, ge=1, le=MAX_NEW_TOKENS)
temperature: float = Field(default=0.6, ge=0.0, le=2.0)
top_p: float = Field(default=0.95, ge=0.05, le=1.0)
top_k: int = Field(default=20, ge=0, le=200)
repetition_penalty: float = Field(default=1.12, ge=1.0, le=2.0)
use_cache: bool = True
seed: Optional[int] = Field(default=None, ge=0, le=2_147_483_647)
max_time: Optional[float] = Field(default=300.0, ge=1.0, le=900.0)
# JSON output controls. response_schema enables llama.cpp grammar-based
# JSON Schema constraints and a second server-side validation pass.
json_mode: bool = False
response_schema: Optional[dict[str, Any]] = None
# GGUF only. "auto" => Q6 for <=1B, Q4 for 1B–4B, Q3 for >4B.
# Exact choice depends on which files actually exist in the repository.
gguf_quant: Literal["auto", "q3", "q4", "q5", "q6", "q8"] = "auto"
@model_validator(mode="after")
def validate_input(self):
if not self.prompt and not self.messages:
raise ValueError("Provide either prompt or messages.")
if self.response_schema is not None:
validate_response_schema(self.response_schema)
return self
def unload_model() -> None:
global model, tokenizer, loaded_model_key, loaded_backend, loaded_context_tokens
global model_dataloader, loaded_prompt_adapter
global loaded_vbpt_runtime_backend, loaded_vbpt_device, loaded_vbpt_dtype, loaded_vbpt_revision
model = None
tokenizer = None
model_dataloader = None
loaded_prompt_adapter = None
loaded_model_key = None
loaded_backend = None
loaded_context_tokens = None
loaded_vbpt_runtime_backend = None
loaded_vbpt_device = None
loaded_vbpt_dtype = None
loaded_vbpt_revision = None
gc.collect()
def quant_priorities(params_b: float, requested_quant: str) -> list[str]:
groups = {
"q3": ["q3_k_l", "q3_k_m", "q3_k_s", "q3_0"],
"q4": ["q4_k_m", "q4_k_s", "q4_0"],
"q5": ["q5_k_m", "q5_k_s", "q5_0"],
"q6": ["q6_k", "q6_k_l", "q6_k_m"],
"q8": ["q8_0"],
}
if requested_quant != "auto":
return groups[requested_quant]
if params_b <= 1.0:
# Q3 is deliberately excluded for models at or below 4B.
return groups["q6"] + groups["q5"] + groups["q4"] + groups["q8"]
if params_b <= 4.0:
# Q3 is deliberately excluded for models at or below 4B.
return groups["q4"] + groups["q5"] + groups["q6"] + groups["q8"]
return groups["q3"] + groups["q4"] + groups["q5"] + groups["q6"] + groups["q8"]
def choose_gguf_file(repo_id: str, params_b: float, requested_quant: str) -> str:
files = HfApi().list_repo_files(repo_id, repo_type="model")
candidates = [
item
for item in files
if item.lower().endswith(".gguf")
and "mmproj" not in item.lower()
and "projector" not in item.lower()
]
if not candidates:
raise RuntimeError(f"No usable GGUF files found in {repo_id}")
# Prefer a normal model file, not split model segments, when both exist.
candidates.sort(key=lambda name: ("-00001-of-" in name.lower(), len(name), name.lower()))
for needle in quant_priorities(params_b, requested_quant):
for filename in candidates:
if needle in filename.lower():
return filename
available = ", ".join(candidates[:20])
raise RuntimeError(
f"No GGUF matching requested quantization '{requested_quant}' in "
f"{repo_id}. Available: {available}"
)
def describe_chat_template(loaded: Any) -> str:
metadata = getattr(loaded, "metadata", {}) or {}
if metadata.get("tokenizer.chat_template"):
return "gguf tokenizer.chat_template"
return "llama-cpp-python default/fallback (GGUF has no tokenizer.chat_template)"
def load_model(
model_key: str,
context_tokens: int,
gguf_quant: str,
) -> None:
global model, tokenizer, loaded_model_key, loaded_backend, loaded_context_tokens
global model_dataloader, loaded_prompt_adapter
global loaded_vbpt_runtime_backend, loaded_vbpt_device, loaded_vbpt_dtype, loaded_vbpt_revision
if model_key not in MODEL_CATALOG:
raise HTTPException(
status_code=422,
detail={
"error": "Unknown model key",
"available_models": list(MODEL_CATALOG),
},
)
spec = MODEL_CATALOG[model_key]
backend = spec["backend"]
if (
backend == "gguf"
and gguf_quant == "q3"
and float(spec["params_b"]) <= 4.0
):
raise HTTPException(
status_code=422,
detail="Q3 is reserved for GGUF models larger than 4B parameters.",
)
# A llama.cpp model needs reload if n_ctx or quantization policy changes.
same_gguf_options = (
backend != "gguf"
or (
loaded_context_tokens == context_tokens
and getattr(model, "_selected_quant_request", None) == gguf_quant
)
)
if model is not None and loaded_model_key == model_key and same_gguf_options:
return
unload_model()
try:
if backend == "gguf":
if Llama is None:
raise RuntimeError(
"GGUF backend requires llama-cpp-python. Install it or select "
"the default bachvnju-vbpt-1-0.5B Transformers model."
)
filename = choose_gguf_file(
repo_id=spec["repo"],
params_b=float(spec["params_b"]),
requested_quant=gguf_quant,
)
local_path = hf_hub_download(
repo_id=spec["repo"],
filename=filename,
repo_type="model",
)
llama_options = {
"model_path": local_path,
"n_ctx": context_tokens,
"n_threads": CPU_THREADS,
"n_threads_batch": CPU_THREADS,
"n_batch": 512,
"n_gpu_layers": 0,
"verbose": False,
}
# Only catalog entries that explicitly provide a tested override use it.
if spec.get("chat_format") is not None:
llama_options["chat_format"] = spec["chat_format"]
loaded = Llama(**llama_options)
# Store runtime details without relying on private llama-cpp internals.
loaded._selected_quant_request = gguf_quant
loaded._selected_gguf_filename = filename
loaded._chat_template_source = describe_chat_template(loaded)
model = loaded
tokenizer = None
elif backend == "transformers":
if spec.get("loader") == "vbpt":
# VBPT uses its own loader instead of the generic Transformers path.
# It follows the supplied benchmark's dtype compatibility fallback,
# pad-token repair, template handling, and left-padded batching.
(
loaded,
loaded_tokenizer,
loaded_dataloader,
runtime_backend,
runtime_device,
runtime_dtype,
runtime_revision,
) = _load_vbpt_with_embedded_runtime(spec["repo"])
model = loaded
tokenizer = loaded_tokenizer
model_dataloader = loaded_dataloader
loaded_prompt_adapter = "vbpt"
loaded_vbpt_runtime_backend = runtime_backend
loaded_vbpt_device = runtime_device
loaded_vbpt_dtype = runtime_dtype
loaded_vbpt_revision = runtime_revision
else:
loaded_tokenizer = AutoTokenizer.from_pretrained(
spec["repo"],
trust_remote_code=True,
)
if loaded_tokenizer.pad_token_id is None:
if loaded_tokenizer.eos_token is None:
raise RuntimeError(
f"Tokenizer for {spec['repo']} has neither pad_token nor eos_token."
)
loaded_tokenizer.pad_token = loaded_tokenizer.eos_token
loaded_tokenizer.truncation_side = "left"
loaded = AutoModelForCausalLM.from_pretrained(
spec["repo"],
torch_dtype=torch.float32,
low_cpu_mem_usage=True,
trust_remote_code=True,
)
loaded.eval()
loaded.generation_config.use_cache = True
if loaded.generation_config.pad_token_id is None:
loaded.generation_config.pad_token_id = loaded_tokenizer.pad_token_id
model = loaded
tokenizer = loaded_tokenizer
model_dataloader = None
loaded_prompt_adapter = None
loaded_vbpt_runtime_backend = None
loaded_vbpt_device = None
loaded_vbpt_dtype = None
loaded_vbpt_revision = None
elif backend == "onnx":
if ORTModelForCausalLM is None:
raise RuntimeError(
"ONNX backend requires optimum[onnxruntime]. Install it or select "
"the bachvnju-vbpt-1-0.5B Transformers model."
)
loaded_tokenizer = AutoTokenizer.from_pretrained(
spec["repo"],
trust_remote_code=True,
)
if loaded_tokenizer.pad_token_id is None:
loaded_tokenizer.pad_token = loaded_tokenizer.eos_token
loaded_tokenizer.truncation_side = "left"
loaded = ORTModelForCausalLM.from_pretrained(
spec["repo"],
provider="CPUExecutionProvider",
)
model = loaded
tokenizer = loaded_tokenizer
model_dataloader = None
loaded_prompt_adapter = None
loaded_vbpt_runtime_backend = None
loaded_vbpt_device = None
loaded_vbpt_dtype = None
loaded_vbpt_revision = None
else:
raise RuntimeError(f"Unsupported backend: {backend}")
loaded_model_key = model_key
loaded_backend = backend
loaded_context_tokens = context_tokens
except HTTPException:
unload_model()
raise
except Exception as exc:
unload_model()
raise HTTPException(
status_code=500,
detail=f"Could not load {model_key}: {type(exc).__name__}: {exc}",
)
def make_messages(req: GenerateRequest) -> list[dict[str, str]]:
system = req.system
if req.response_schema is not None:
system += (
"\nTrả về đúng một JSON document khớp JSON Schema được yêu cầu. "
"Không thêm markdown, giải thích, hay văn bản ngoài JSON."
)
elif req.json_mode:
system += "\nTrả về đúng một JSON object hợp lệ, không thêm markdown hay văn bản ngoài JSON."
result: list[dict[str, str]] = [{"role": "system", "content": system}]
if req.messages:
result.extend(item.model_dump() for item in req.messages)
else:
result.append({"role": "user", "content": req.prompt or ""})
return result
def make_text_prompt(messages: list[dict[str, str]]) -> str:
if getattr(tokenizer, "chat_template", None):
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
lines = []
for item in messages:
if item["role"] == "system":
lines.append(item["content"])
elif item["role"] == "user":
lines.append(f"User: {item['content']}")
else:
lines.append(f"Assistant: {item['content']}")
lines.append("Assistant:")
return "\n".join(lines)
def response_format_for(req: GenerateRequest) -> Optional[dict[str, Any]]:
if req.response_schema is not None:
return {"type": "json_object", "schema": req.response_schema}
if req.json_mode:
return {"type": "json_object"}
return None
def parse_and_validate_structured_response(
answer: str,
schema: Optional[dict[str, Any]],
) -> Any:
try:
parsed = json.loads(answer)
except json.JSONDecodeError as exc:
raise HTTPException(
status_code=502,
detail=(
"GGUF backend returned non-JSON despite JSON mode. "
f"Parser error: {exc.msg}"
),
) from exc
if schema is not None:
try:
Draft202012Validator(schema).validate(parsed)
except ValidationError as exc:
path = "/".join(str(item) for item in exc.path) or "<root>"
raise HTTPException(
status_code=502,
detail=(
"GGUF backend violated the requested JSON Schema after "
f"grammar enforcement at {path}: {exc.message}"
),
) from exc
return parsed
def generate_gguf(req: GenerateRequest, messages: list[dict[str, str]]) -> tuple[str, str]:
kwargs: dict[str, Any] = {
"messages": messages,
"temperature": req.temperature,
"top_p": req.top_p,
"top_k": req.top_k,
"repeat_penalty": req.repetition_penalty,
"max_tokens": req.max_new_tokens,
"seed": req.seed,
}
response_format = response_format_for(req)
if response_format is not None:
kwargs["response_format"] = response_format
result = model.create_chat_completion(**kwargs)
answer = (result["choices"][0]["message"].get("content") or "").strip()
return answer, getattr(model, "_selected_gguf_filename", "unknown")
def generate_transformers_or_onnx(
req: GenerateRequest,
messages: list[dict[str, str]],
) -> tuple[str, int, int, Optional[str]]:
if req.json_mode or req.response_schema is not None:
raise HTTPException(
status_code=422,
detail=(
"json_mode and response_schema are currently enforced only by the "
"GGUF/llama.cpp backend; select a GGUF model."
),
)
do_sample = req.temperature > 0.0
kwargs: dict[str, Any] = {
"max_new_tokens": req.max_new_tokens,
"do_sample": do_sample,
"repetition_penalty": req.repetition_penalty,
"use_cache": req.use_cache,
"pad_token_id": tokenizer.pad_token_id,
"eos_token_id": tokenizer.eos_token_id,
}
if req.max_time is not None:
kwargs["max_time"] = req.max_time
if do_sample:
kwargs.update(
temperature=req.temperature,
top_p=req.top_p,
top_k=req.top_k,
)
if req.seed is not None:
torch.manual_seed(req.seed)
# VBPT does not pass through the generic `User:/Assistant:` formatter.
# Its dedicated dataloader first tries the repository tokenizer template and
# then deliberately falls back to the benchmark's raw-prompt behavior.
if loaded_prompt_adapter == "vbpt":
if model_dataloader is None:
raise RuntimeError("VBPT model is loaded without its dedicated dataloader.")
prepared = model_dataloader.prepare_batch(
[messages],
context_tokens=req.context_tokens,
)
with torch.inference_mode():
output_ids = model.generate(**prepared.inputs, **kwargs)
answer = model_dataloader.decode_new_tokens(
output_ids,
padded_prompt_tokens=prepared.prompt_token_count,
)[0]
new_tokens = int(output_ids.shape[1] - prepared.prompt_token_count)
return answer, prepared.prompt_token_count, new_tokens, prepared.prompt_style
prompt = make_text_prompt(messages)
inputs = tokenizer(
prompt,
return_tensors="pt",
truncation=True,
max_length=req.context_tokens,
)
with torch.inference_mode():
output_ids = model.generate(**inputs, **kwargs)
input_tokens = int(inputs["input_ids"].shape[1])
new_tokens = output_ids[0][input_tokens:]
answer = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
return answer, input_tokens, int(new_tokens.shape[0]), None
@asynccontextmanager
async def lifespan(app: FastAPI):
print("[vbpt-api] BUILD=space-ready-v5.7; VBPT uses a local embedded-runtime overlay, not the repository Python runtime.", flush=True)
torch.set_num_threads(CPU_THREADS)
try:
torch.set_num_interop_threads(1)
except RuntimeError:
pass
# Warm-load the default model.
with engine_lock:
load_model(DEFAULT_MODEL, context_tokens=2048, gguf_quant="auto")
yield
with engine_lock:
unload_model()
app = FastAPI(
title="CPU Multi-Backend AI API (all-in-one VBPT adapter)",
version="5.7.0-space-ready-vbpt-embedded-runtime",
lifespan=lifespan,
)
@app.get("/")
def root():
return {
"status": "ok",
"endpoints": ["/health", "/system", "/models", "/generate", "/docs"],
}
@app.get("/health")
def health():
details: dict[str, Any] = {
"api_build": "space-ready-v5.7",
"status": "healthy" if model is not None else "loading",
"loaded_model": loaded_model_key,
"backend": loaded_backend,
"cpu_threads": CPU_THREADS,
"context_tokens": loaded_context_tokens,
"max_context_tokens": MAX_CONTEXT_TOKENS,
"max_new_tokens": MAX_NEW_TOKENS,
}
if loaded_backend == "gguf" and model is not None:
details.update(
gguf_file=getattr(model, "_selected_gguf_filename", None),
gguf_quant_request=getattr(model, "_selected_quant_request", None),
chat_template_source=getattr(model, "_chat_template_source", None),
)
if loaded_prompt_adapter is not None:
details["prompt_adapter"] = loaded_prompt_adapter
if loaded_prompt_adapter == "vbpt":
details.update(
vbpt_runtime=VBPT_RUNTIME_VERSION,
vbpt_runtime_backend=loaded_vbpt_runtime_backend,
vbpt_device=loaded_vbpt_device,
vbpt_dtype=loaded_vbpt_dtype,
vbpt_revision=loaded_vbpt_revision,
)
return details
@app.get("/system")
def system():
try:
lscpu = subprocess.check_output(["lscpu"], text=True, timeout=3)
except Exception as exc:
lscpu = f"lscpu unavailable: {exc}"
return {
"processor": platform.processor(),
"logical_cores": os.cpu_count(),
"configured_cpu_threads": CPU_THREADS,
"lscpu": lscpu,
}
@app.get("/models")
def list_models():
return {
"loaded_model": loaded_model_key,
"models": [
{
"key": key,
"backend": spec["backend"],
"repo": spec["repo"],
"label": spec["label"],
"loader": spec.get("loader"),
"note": spec.get("note"),
}
for key, spec in MODEL_CATALOG.items()
],
"notes": [
"Only one model is loaded at once.",
"GGUF auto chooses Q6 for <=1B, Q4 for 1B–4B, and Q3 for >4B when available.",
"No catalog entry forces chat_format; GGUF tokenizer.chat_template is preferred.",
"JSON Schema is grammar-constrained by llama.cpp and validated again server-side.",
"Generic Transformers 4/8-bit bitsandbytes is not enabled on CPU.",
"ONNX backend needs a repository containing a pre-exported ONNX model.",
"VBPT uses a dedicated tokenizer/prompt/batch dataloader rather than the generic Transformer formatter.",
"VBPT uses the embedded verified GDN runtime: auto selects CUDA+FLA when available, otherwise CPU reference mode.",
"Set VBPT_RUNTIME_BACKEND=fla|reference, VBPT_DEVICE=cuda|cpu, and optional VBPT_REVISION to control VBPT startup.",
],
}
@app.post("/generate")
def generate(req: GenerateRequest):
with engine_lock:
load_model(
model_key=req.model,
context_tokens=req.context_tokens,
gguf_quant=req.gguf_quant,
)
messages = make_messages(req)
try:
if loaded_backend == "gguf":
answer, gguf_file = generate_gguf(req, messages)
payload: dict[str, Any] = {
"response": answer,
"model": loaded_model_key,
"backend": loaded_backend,
"gguf_file": gguf_file,
"context_tokens": req.context_tokens,
}
if req.json_mode or req.response_schema is not None:
payload["response_json"] = parse_and_validate_structured_response(
answer,
req.response_schema,
)
return payload
answer, input_tokens, output_tokens, prompt_style = generate_transformers_or_onnx(
req,
messages,
)
payload = {
"response": answer,
"model": loaded_model_key,
"backend": loaded_backend,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
}
if prompt_style is not None:
payload["prompt_style"] = prompt_style
return payload
except HTTPException:
raise
except Exception as exc:
raise HTTPException(
status_code=500,
detail=f"Generation failed: {type(exc).__name__}: {exc}",
)
def _run_vbpt_loader_self_test() -> None:
"""Offline test for the embedded VBPT adapter; never downloads model weights."""
class _Config:
pad_token_id = None
eos_token_id = None
class _GenerationConfig:
pad_token_id = None
eos_token_id = None
use_cache = False
class _FakeModel:
def __init__(self) -> None:
self.config = _Config()
self.generation_config = _GenerationConfig()
class _FakeTokenizer:
def __init__(self, *, has_template: bool) -> None:
self.pad_token_id = None
self.eos_token_id = 2
self.eos_token = "</s>"
self.pad_token = None
self.padding_side = "right"
self.truncation_side = "right"
self.chat_template = "placeholder" if has_template else None
def __setattr__(self, name: str, value: Any) -> None:
object.__setattr__(self, name, value)
if name == "pad_token" and value == "</s>":
object.__setattr__(self, "pad_token_id", 2)
def apply_chat_template(
self,
messages: Sequence[Mapping[str, str]],
*,
tokenize: bool,
add_generation_prompt: bool,
) -> str:
assert tokenize is False
assert add_generation_prompt is True
return "|".join(f"{item['role']}={item['content']}" for item in messages) + "|assistant="
def __call__(
self,
prompts: str | Sequence[str],
*,
return_tensors: str,
padding: bool,
truncation: bool,
max_length: int,
return_attention_mask: bool,
) -> dict[str, torch.Tensor]:
if isinstance(prompts, str):
prompts = [prompts]
rows = [[(ord(ch) % 80) + 3 for ch in prompt][-max_length:] for prompt in prompts]
width = max(len(row) for row in rows)
padded = [[self.pad_token_id] * (width - len(row)) + row for row in rows]
masks = [[0] * (width - len(row)) + [1] * len(row) for row in rows]
return {
"input_ids": torch.tensor(padded),
"attention_mask": torch.tensor(masks),
}
def decode(self, ids: Any, *, skip_special_tokens: bool = True) -> str:
values = ids.tolist() if isinstance(ids, torch.Tensor) else list(ids)
return "".join("Z" if value == 99 else f"<{value}>" for value in values if value != 2)
raw_tokenizer = _FakeTokenizer(has_template=False)
loader = VBPTDataLoader(raw_tokenizer)
fake_model = _FakeModel()
loader.apply_generation_token_ids(fake_model)
batch = loader.prepare_batch(
[
[{"role": "system", "content": "S"}, {"role": "user", "content": "Xin chào"}],
[{"role": "user", "content": "B"}],
],
context_tokens=32,
)
assert batch.prompt_style == "raw_benchmark_compatible"
assert raw_tokenizer.padding_side == "left"
assert raw_tokenizer.truncation_side == "left"
assert fake_model.generation_config.pad_token_id == 2
output = torch.cat([batch.inputs["input_ids"], torch.tensor([[99], [99]])], dim=1)
assert loader.decode_new_tokens(output, padded_prompt_tokens=batch.prompt_token_count) == ["Z", "Z"]
templated_loader = VBPTDataLoader(_FakeTokenizer(has_template=True))
_, style = templated_loader.render_messages([{"role": "user", "content": "hello"}])
assert style == "native_chat_template"
print("Embedded VBPT dataloader self-test: PASS")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="All-in-one multi-backend API with conditional VBPT loader")
parser.add_argument("--self-test-vbpt-loader", action="store_true", help="run the offline embedded VBPT loader self-test")
parser.add_argument("--host", default=os.getenv("HOST", "0.0.0.0"))
parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000")))
args = parser.parse_args()
if args.self_test_vbpt_loader:
_run_vbpt_loader_self_test()
else:
import uvicorn
uvicorn.run(app, host=args.host, port=args.port)