| 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 |
|
|
| |
| 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 |
|
|
| |
| |
| try: |
| from llama_cpp import Llama |
| except ImportError: |
| Llama = None |
|
|
| try: |
| from optimum.onnxruntime import ORTModelForCausalLM |
| except ImportError: |
| ORTModelForCausalLM = None |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
|
|
| |
| |
| |
| |
|
|
|
|
|
|
|
|
| VBPT_MODEL_KEY = "bachvnju-vbpt-1-0.5B" |
| VBPT_REPO_ID = "bachvnju/vbpt-1-0.5B" |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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-*&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" |
| VBPT_DEVICE_ENV = "VBPT_DEVICE" |
| VBPT_REVISION_ENV = "VBPT_REVISION" |
| VBPT_OVERLAY_DIR_ENV = "VBPT_OVERLAY_DIR" |
| 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 |
| 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 |
|
|
| |
| |
| 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: |
| |
| |
| |
| 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() |
| |
| 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() |
| |
| |
| |
| 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, |
| ) |
|
|
| |
| |
| 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: |
| |
| |
| 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 |
| ] |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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." |
| ), |
| }, |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| } |
|
|
| 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 |
| |
| 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 |
|
|
| |
| |
| 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 |
|
|
| |
| 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_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_mode: bool = False |
| response_schema: Optional[dict[str, Any]] = None |
|
|
| |
| |
| 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: |
| |
| return groups["q6"] + groups["q5"] + groups["q4"] + groups["q8"] |
| if params_b <= 4.0: |
| |
| 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}") |
|
|
| |
| 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.", |
| ) |
|
|
| |
| 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, |
| } |
| |
| if spec.get("chat_format") is not None: |
| llama_options["chat_format"] = spec["chat_format"] |
|
|
| loaded = Llama(**llama_options) |
| |
| 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": |
| |
| |
| |
| ( |
| 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) |
|
|
| |
| |
| |
| 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 |
|
|
| |
| 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) |
|
|