From f60dbf49f10676727db9ba0acfdc916930433bd3 Mon Sep 17 00:00:00 2001 From: second_constantine Date: Mon, 26 Jan 2026 15:21:55 +0300 Subject: [PATCH] feat: Add context size support for benchmarks and update example usage This commit adds support for specifying context size when running benchmarks, which is passed to the Ollama client as the `num_ctx` option. The changes include: - Updated the `run` method in the base benchmark class to accept an optional `context_size` parameter - Modified the Ollama client call to include context size in the options when provided - Updated the `run_benchmarks` function to accept and pass through the context size - Added example usage to the help output showing how to use the new context size parameter - Fixed prompt formatting in the summarization benchmark to use `text` instead of `task` The changes enable running benchmarks with custom context sizes, which is useful for testing models with different context window limitations. --- run.sh | 3 ++- .../__pycache__/base.cpython-313.pyc | Bin 4237 -> 4938 bytes .../__pycache__/codegen.cpython-313.pyc | Bin 3311 -> 3681 bytes .../__pycache__/summarization.cpython-313.pyc | Bin 3316 -> 3692 bytes .../__pycache__/translation.cpython-313.pyc | Bin 3296 -> 3666 bytes src/benchmarks/base.py | 15 ++++++++--- src/main.py | 17 +++++------- .../__pycache__/ollama_client.cpython-313.pyc | Bin 3958 -> 4999 bytes src/models/ollama_client.py | 25 +++++++++++++++++- src/utils/__pycache__/report.cpython-313.pyc | Bin 13951 -> 13693 bytes 10 files changed, 44 insertions(+), 16 deletions(-) diff --git a/run.sh b/run.sh index 3b818bb..b8b424f 100755 --- a/run.sh +++ b/run.sh @@ -73,7 +73,8 @@ else echo "" echo "Примеры использования:" echo " * ./run.sh run -m second_constantine/t-lite-it-1.0:7b -b translation summarization" - echo " * ./run.sh run -m second_constantine/t-lite-it-1.0:7b --num-ctx 16000" + echo " * ./run.sh run -m second_constantine/t-lite-it-1.0:7b -u http://10.0.0.4:11434 -c 2048 -b translation summarization" + echo " * ./run.sh run -m translategemma:4b -u http://10.0.0.4:11434 -c 128000 -b summarization" echo " * ./run.sh gen" echo " * ./run.sh gen-mongo 507f1f77bcf86cd799439011" echo " * ./run.sh gen-mongo --id-file ids.txt" diff --git a/src/benchmarks/__pycache__/base.cpython-313.pyc b/src/benchmarks/__pycache__/base.cpython-313.pyc index 9a972bca87d9f3d0d4b563473f09f371bbead576..7e144bfe60546f9be3b7024dc9cd36ba93bfcd5e 100644 GIT binary patch delta 1860 zcmaJ?U2GFa5Z*oeZ0F8Dcd=7Ke$PoTu^Q}@BtSz$oAMV(98gwQ(IzOhor`gB>Go zfGIMqREa(yK&O_fR27d^)d#AATG1axszhm3RqHltB0te*UW@|bwYzpsiQ3YYZob{Q znQvxi_VzA^e~I|ld_Dot5kGn+`=xx%zb1%mNyplUmI!lk35ipFPsQlHZaK96dTYvG*#j-b@*G=uLIc;PMTH0Bqt%(2y z2YuECsrCbaAmT>BQK&l=pn?fT{*N)RzSj9T&|Q;m`?bm=#{gGb1;JrBasq%wx?VWj zW}SA$WVat6e$uyLZhh{0eRrMeL2edB7mx?l^+H`#9jOigPV7bEcB-dIcx;~yv?O<^ z8+lcSAM|kpu=b*w@iUB1A)m_84E&oi5Nt=P52zxga@iV61t%Id;@LBmQw3E-(u)q= z`|QkmkgR&U*L{+-jhljc?GVQxf-HNBxONuRx*fFnx{q>hv14uEiTkLY)yOWd?pK{1 z;9uH+>YCWP;bqTgg@eqSX6~u+s9x2t`qTglRrLWBZ&;r^w~kxZv5_I#8WZiT724Uv zq~-B!FY>l-)9zeB@S1FJabR#kFhZNH%cxbA_fnP_dKMD27OH%lOVCp%0)<-XvdyBp zP+QE|1;Svm$xhu3k_pNo{a2}r08F&nh8u{QRCdyuRf!hYX_M1_{%>t2+ieC_vVBlX zY}3(d2T0JRVTo@4AuWlZx6s8boCQOUb}$PKuC|+55N%(Sp>?G0`W}Uj&EMUiLT-{< zpm%tm)-i}{B+tpTWCa@X=e3| zO{TDqSTXZTp<_6eUo_b?oy_HwjHYWiX==)uta(bA&(CPNp@P(-Ea3e7f~n**J!764 zDhNGFe!;%DDzc29Hj*)yVv!FX!NknRb@_7f}Cb*Gd&8{j0Nhd*>u)>f(E13 zL!qr9MmcbtiC!jNU}C4$Cbs+f=(Nmh80RtWw?@T%^-|flXl8Rp%t+@k-5icsH^jJ$ zpV3l_8S7`Uv$%`-I1Cd<>v51#(OUuFPA0mTh)_`$OwBN-(@8@!@E}vbEUpF<3^R$% zX)`;oRriK>FduecFuTEUKQnf*;F?i}S{Y)ao~l^SYQviLMm#6!p}h#6G+LLvt-?x6 z|NCQ?hAUj3b=#XLK4|X#tdlgwuDVx)?WN%J526Df9Vvw(tD(m0qt`}PLfcBAZKV5^ zrIsZ>i5y!F9e*H&u8Y^i6)9Yj!ldiaQei1aCQ{4kS(2ImBdwEEp5)I}05}g{TfxW@p<2f5X7Y$d$A6yCks)V2Cjb9l9-v*HFFdn&-$QGeB65rEvVIY8o1&Gh2Ja$ZszK?f(aZoCGzX#s1 zdIQ?nqoy&)3pZOTfZ;cNHN2m$Avu{ODUD?2NNTQh^7PZHQSB9G7a?Z}A{UimEug{zJyfMM10ZvqC5xc~qF delta 1219 zcmaJ>O>7%Q6n?Yo_1gP0>z_6WPTbh0Y2(s3e~#h+sT5L$^5Y6+G!-aDRg`Xm!ChCg z8zBxT9D@*5s{tVp2e<$d2M$~~7O7NGxwJw+wD(?mG75z&GwUotATfvcz4zw*eDii@ z@1ALLm!@bn zA4eY{RcM-|NO}qpV?p%{PE*yL)Pxv>XHr-_eiRvNFMml)Suv*PSyZjSf4nx;RtMvp zBNTirnN8C|i|SB=TGXUAu_M1t@YzGfImFB%W+jqb&5Vy)g67ZsiS+3=60zqW1JUwu z$?=s*@D(vZF(}U=gVcFS)VLyM*)#2?6{?Y;TH+*>VQna;uETm$ z)WCdEA4J3ark>x)Q~42&QH){sZw4j!|AVg;wys-bqZd&TXranT{vyVrtt*Yo*~<<_N# zosdFy4>f9(AW_NPz`tX@=j>X4 zfR)YvHVwtOUW>n)`K12!vVFdx+%6a^IqUX#Qp$$0;%Gl8OK8Q=?iiXdstU_V&hk}W ztY-N{_DQtFU(3!Y>;nHh+d#WK%(@G`!ZS!SJ{#$Q#P4SNc&omr?vcIJ_vikM{7k+c hCT=N+Y094mTjcNM-bR;rIzN@&LddwGJ_3nROFbvt*IPGL+x%aMV zebFaTK`2~=KKLR&*t}UG&GbJpL%XqH|H|vp7WhM-^@AR z{j2kjp3IhQClOeSOM*YG?=pAsSp!lPo~<9iP2fA|BJ2(OOev!|9U%vg;0BUI7a?z$ zqd2OgIr^xopa_@otjVX$ZvMBK<*}K#T1Mf{QRKiHQ9h6?puOk-8hB4s0rIMnL%k>m zYXOa`y{L+d#*8rMJ;SvT|=)E_Dyp&(eX{^TB7fby?dqg<8QMMmtNSDzZ`m%?|*Rm z-s$^i9?aaEd6^%+F};@F`>%>JC-J&H`MW*&j;4U+Zhp#Af3=cR?WQn@&*~(aheKZ?Pf6B-3*&S*>Ef= zj*}e45F6WZYEa#g*-6W_qu8fU`GnS=dWAlyWAjK`9w0Wbkc@OK;5Ov{Z%CO1B62HY zp)rqs!6)>d!HiI1D$FpTtP+4cepI1L0lP%9@&&Etu}ebxJPsn#1^~aM@7qm2k|m=W zlSPjeP14RE>ihbLY*Sv2i1`r{dd(w@QWL;j!$=q3(ua**fY3F5(74^Xf4wlaS{Pd| zlvWF+XZkbmd19qdS}9!MKO3Vx$4=$-C;8;5!_$Z!;e&dwPLt zL#hGyOQk-1G>n&gLiPhhKH(cycdA{6N2JU1mSi6uHd~+-1GHU{~*}55B(2i2K-$B delta 724 zcmYLGJ!lj`6rQ>Lz1!?1Ar{75V$9F!65|?3G)5DDQmf#3sE8~_ZZ28b>>e|F1T9Po z5d#Klx3mcJ$T8Tvp5l0)#R0=VYPUj_xZ<+Vrym>$0`}SMmdwYJ}b#p|? zjf*_|Z9mIDp!1r#;#u!gYJ%P)mzCX?^R!H^7=)DQ2;Gi!lWF2*&PZ!2sj_Jndl_kn zMxAkK2SFHzX%MK8E?88P*&vvTDnbN-$f)Y>1v%=M1>?f6RATKbD1@E=7>=Qy_t*o6hpvBk$ zJ^CsgfS>wN-z;U-2Bd73Othuk0qy@XB?rF|gr}sNoPG*-jDF`eA>-O^QlnlIyfl{g zO3JSrjkGp6>QQO4SRoJN=`}>NhP9G#G_YhA4mQb5BBT|CBCUps78X~h%ixSCdfroK>pu$?VZRi#J2g>d_>v!I0HSTj8uVY`UtcSb;@r$ryNi^5G2l_L0J# zN!NJe<|YeSnV(e@MgeD{p2UjT&gVDn7n!bh#D&y&rql#Wr4c1_jPrgK;zhXRbU4lE z4x#FVd8d~if=7<0rhaht(IWhE-kQ8ky)MJMrc=c{<{^0;o2r&6=Q>_}yc)GAsib@y z2HpKW{z8GgoZzjPk^QGEe3(>f^A`E6U)a#8(f7lI+wNi71}p9^+677%Q6rR~%ukCf4v?7j)6t{J(ZrUaiByH2sir7i0ga{>zMNx`YD|?+Ttar`K zm{h$4E(lchG^$hq2PzJ2j|dK2kctW-+!C6SGU5n_NIf8>6;)5XiD@FqD7StJh4K0A}%9~0Jy`v-K;&I$Xif94i zuDFV;x|*v`s0xa44bPkWBekH+Thc7oP_&zgTv+4Evl@%&06K~e|5v90wVO&24Wc5f zMRZynL{01%bHb*bR?8>hWuJvfe7V0(l2)71(f`3C>=F~0Jk>j_dFCu0h^43ZLzl#haUul`jOY|;HW~QLDUquDJ;*c zM?R&VN9B}?yicj(UcR6>%To|P`4Ih)vOhJ~Qu*tH>nZ0?yJxlc*e&|qTff>fD?JZN zLpNt{%zkzD=G=|Bd!?~Yrq{Cv{!vkG7H`-yzu7ZSX$Go%QJS*UAFT9LpD7Fyunvjq zFqIv#z}x2NA)wdLD9qc%{AXpQt!xu5r__i%;(rB147KF_DreXl&fQ(?!c z8LlOXanlnRd}G_sjHtT|yIHw**?R3K{<=1ld4QfVWAkP0)?s2p6_S%`52#If-%Uxe zKtvh`3ylSr7@yMzMsh+)s4%0DvPKAW1aX5dh3q2PCl6?CpIsE%yGa<6{Q&UK^n*R* zIk{yt6VmdTW0F38M?ZLolb8iD6MEYxj8YT8Vpm9ktHvo~FCcWCSBBSe$om<;u1l=zHP`1hHkqy2zT zIrEoEL1xG&vaRGSXcg3xkJ0bixyPEOkN=(J`RoZi&Cg{Ec!a@E)@qA~YK*p8qj-?^5(|pxvLussCD~nOHXuED z@Z!PKLnwHuh!8;p@h%m;dGQA?gCKYi3M~|(;L({@@eS{H-p|K-+r^)S?0}FolGM7! z{B7%H_7M&==_(!^kzyV{L^q_~r>l4w-82wt;90yMDWetSB$fo>y>8cI{+bv2Vc;Z$ z!Fv&9!g5_d@MG7NA)V7sTcljK+3`4cU7nCV0BdzUtBh}U;nKfsg^3@K;`d$j)gt$k zAFYw~@h_G$kTE1oM&lj}ntws47_ubq!F~Vwrj;qRp7LmSm$%b9hAPVMGI=M_#-t^xL5?ZAQHo?3fd} zlVmw4Cnq$4+i*Eq!g-*{nn`45z6np04~(h)!sAuAVx2A?suSZaN!N=hk?n>p+Ho1> zQ5bN_O0PDojRY>hf<0_b%3@Z5TlT1(Q|1XJN^s9Uj88(}cI1pt_B5^l&b%|(koL^U@i>b8=jmM1X%AHZJ(e8v^%;um}lht9J_sAeZ~HC$Z3{tHR9m-Gh;Jf{2r diff --git a/src/benchmarks/__pycache__/translation.cpython-313.pyc b/src/benchmarks/__pycache__/translation.cpython-313.pyc index 0aeaf4985753297809fdf6a0e56102a672958d0c..f34c1bc2257e98d862886b274fd251677d32d32a 100644 GIT binary patch delta 1012 zcmZ8gPiPcZ7=LeO-t5lq%xa>z$!3F5H>>NP zoA3Ag=2iQ%d}i6QQb@?k7vLXB-N{riuCOz>`uXjiaV!JtqKjP>&+@NA4dd(R+%3N|v4Emps>cCHIe zvJtK0@+3N4oFb zgda|pbWY-C!vmgl96$7_<8Uo%cp=xob0C4 zl17d|ZY9h$>RqJlD9PgtJ5RFPV2ccE4VPZv+Lw_Z!dBs6-;$jzuuX>adIVE0EhQk! zejz&v$adHw>BKpYWDYV1)?f@sHpJYnt2G<*kLn= qyV+;v$89qRYO+R7)<_-}O*R8>7;N^l*}Z8P=Wh(Ih(9*Jn)DCe#r>rK delta 784 zcmY*WPe@cj7@v9n*WLHF#Y^UDYX0e=X&tOpQ`7Ylp&*+PMLZt6v-;xo-tx^`q%L_9 z3c_FrJO$kfBCw!C2Pq1=bf~Ct0LE?ELq)`%9dCK$3p--vjQAHfvvBG{Tlh|P+ zYG2`i2<)w(FP44F{9qkiZ~JWZ?MQBJ07>c){Wqp168mFrvQO$Le$2Akcihi@X=4SW zloCUt1*2q0Uuqgu|IJ%U%Wqh+3+q6$PY_BAzks{hl+x*V+9v;i$vwP5=4vJQi^SP9!DS@TQl; z`<8xVK(oj`V$U Dict[str, Any]: + def run(self, ollama_client: OllamaClient, model_name: str, num_ctx: int = 32000, context_size: int = None) -> Dict[str, Any]: """ Запуск бенчмарка. Args: ollama_client: Клиент для работы с Ollama model_name: Название модели - num_ctx: Размер контекста Returns: Результаты бенчмарка @@ -73,12 +72,20 @@ class Benchmark(ABC): # Получение ответа от модели prompt = test_case['prompt'] self.logger.debug(f"Prompt: {prompt[:200]}...") # Логируем начало промпта + # Подготовка опций для вызова + options = {'temperature': 0.7} + if context_size is not None: + # Для Ollama параметры контекста передаются в options + options['num_ctx'] = context_size + self.logger.debug(f"Setting context size to {context_size}") + + self.logger.debug(f"About to call generate with model={model_name}, prompt length={len(prompt)}, options={options}") model_response = ollama_client.generate( model=model_name, prompt=prompt, - num_ctx=num_ctx, - options={'temperature': 0.7} + options=options ) + self.logger.debug(f"Generate call completed, response length={len(model_response) if model_response else 0}") # Замер времени latency = time.time() - start_time diff --git a/src/main.py b/src/main.py index b477a9b..11b15bc 100644 --- a/src/main.py +++ b/src/main.py @@ -18,7 +18,7 @@ def setup_logging(verbose: bool = False): ] ) -def run_benchmarks(ollama_client: OllamaClient, model_name: str, benchmarks: List[str], num_ctx: int) -> List[dict]: +def run_benchmarks(ollama_client: OllamaClient, model_name: str, benchmarks: List[str], context_size: int = None) -> List[dict]: """ Запуск выбранных бенчмарков. @@ -26,7 +26,6 @@ def run_benchmarks(ollama_client: OllamaClient, model_name: str, benchmarks: Lis ollama_client: Клиент для работы с Ollama model_name: Название модели benchmarks: Список имен бенчмарков для запуска - num_ctx: Размер контекста Returns: Список результатов бенчмарков @@ -46,7 +45,7 @@ def run_benchmarks(ollama_client: OllamaClient, model_name: str, benchmarks: Lis logging.info(f"Running {benchmark_name} benchmark...") benchmark = benchmark_classes[benchmark_name]() - result = benchmark.run(ollama_client, model_name, num_ctx) + result = benchmark.run(ollama_client, model_name) results.append(result) return results @@ -56,11 +55,11 @@ def main(): parser = argparse.ArgumentParser(description='LLM Benchmarking Tool') parser.add_argument('-m', '--model', required=True, help='Название модели для тестирования') parser.add_argument('-u', '--ollama-url', default='http://localhost:11434', help='URL подключения к Ollama серверу') + parser.add_argument('-c', '--context-size', type=int, default=32000, help='Размер контекста для модели (по умолчанию 32000)') parser.add_argument('-b', '--benchmarks', nargs='+', default=['translation', 'summarization', 'codegen'], help='Список бенчмарков для выполнения (translation, summarization, codegen)') parser.add_argument('-o', '--output', default='results', help='Директория для сохранения результатов') parser.add_argument('-v', '--verbose', action='store_true', help='Подробный режим вывода') - parser.add_argument('--num-ctx', type=int, default=32000, help='Размер контекста для модели (по умолчанию 32000)') args = parser.parse_args() @@ -72,12 +71,11 @@ def main(): logging.info(f"Benchmarks to run: {', '.join(args.benchmarks)}") logging.info(f"Context size: {args.num_ctx}") + # Инициализация клиента + ollama_client = OllamaClient(args.ollama_url) try: - # Инициализация клиента - ollama_client = OllamaClient(args.ollama_url) - # Запуск бенчмарков - results = run_benchmarks(ollama_client, args.model, args.benchmarks, args.num_ctx) + results = run_benchmarks(ollama_client, args.model, args.benchmarks, args.context_size) # Генерация отчетов report_generator = ReportGenerator() @@ -89,9 +87,8 @@ def main(): report_generator.generate_summary_report(results, args.output, args.model, args.ollama_url) logging.info("Benchmarking completed successfully!") - except Exception as e: - logging.error(f"Error during benchmarking: {e}", exc_info=True) + logging.error(f"Error during benchmarking: {e}") return 1 return 0 diff --git a/src/models/__pycache__/ollama_client.cpython-313.pyc b/src/models/__pycache__/ollama_client.cpython-313.pyc index 7d3f8cec4cf0dd6f223d679be01d863a22aa53e1..d3f70a16392781f0afd3c409e8c332ee435f5a46 100644 GIT binary patch delta 1601 zcmZux&2Jk;6rZ(s*NNlrZ@RI)CLgwwCT^8932h;5sG}$;LWu7cj zNklQ~1vt@a4}@MYQYDb+At)RwP6)`EwOnCq$qDfX1gJP7X6!g04RiR-%$xUqznM30 z=AXz%amU|wyA|LzJ^P+?-}$ZM1GDL0llIV3^7jaoV9Jkj@B)dOKr*&iXpzGZaezWEg?If*nMLY$U_r4)lWzd&PnPTwvBo{UwtT(>M!%ivXW6f9Fo z+wS}HW2&;{TZGDbNmd}QsHmJ*%LrcPx12{-b#omXj4pKOX3#3!P`9Q88E&k@yb23^ zXNg}fp*2zEM|io!6ED7mPAp2WSisc1CnH@Q@pkBCUK?U#luNtGa&I#Qc&B_9>){<3 zmK9MERTUwUHbk;xSS3Z06;+h;(4Y%aUNsm=g=>mo#wRhXOJcDM(+HK2(VgDN!*x|E z$p!<7CE2BIvEi4^t8mj`-X)F(1(hz$${#jFY7<4ZZ`f$}CVo;Mf`dp*tFyBe8?d`S zUHD|7VGGx6VVxh}xwSK|N2Z_It~G70yc~EY}cQ&z!iM3Q9aji zp07F2H-pheFi{I8bXQ{E9oi06CmQadntKQ@e0J~QJ>Av6@8-7W9v2$!;hKB6=?zyy zb#EovdqvMl`oKH&bE~>6RXw zuLl;)x@$oj?FndCETQxaPSX9ENZJ8@jP(+HEsF7{h?QV6f$?X*ncz!<825mKpu?KJ zYTgS))05`CNhW=DSGMe#sFXHtO;o~|Fv%h}Nj`qB0Fi&mpz|xDipUE>NetM*L&oHp zqZEN500fH^k{{Te;SU3Ox delta 555 zcmZXQKWx)L6vln^A7aOGWjhTiP2;Ajlx9dPwSc;SY5@Z*fM8)jmWeK46rAvUNFbrM zLP#(oy`=*K0|OgESy&^)4g(T+0Wrc70b=H!Gjh`X-n-MM?|XMo+JBqQ?@Gl|~@qzpOYOZDi zpi1H9b44B!`@Vp l<<)jk-d_1br|C)j=0&W5yMCSR;59#(zr>jLOCc7X{{WOPe-;1$ diff --git a/src/models/ollama_client.py b/src/models/ollama_client.py index 3ac8b96..20faacd 100644 --- a/src/models/ollama_client.py +++ b/src/models/ollama_client.py @@ -45,7 +45,30 @@ class OllamaClient: options=options, **kwargs ) - return response['response'] + # Проверяем структуру ответа + self.logger.debug(f"Response structure: {response}") + self.logger.debug(f"Response type: {type(response)}") + + # Если это объект GenerateResponse (как в ollama 0.3+) + if hasattr(response, 'response'): + return response.response + elif hasattr(response, 'text'): + return response.text + elif isinstance(response, dict): + if 'response' in response: + return response['response'] + elif 'text' in response: + return response['text'] + else: + # Попробуем извлечь любое строковое значение + for key, value in response.items(): + if isinstance(value, str): + return value + raise ValueError(f"Unexpected response format - no text or response field: {response}") + elif isinstance(response, str): + return response + else: + raise ValueError(f"Unexpected response format: {response}") except Exception as e: error_msg = f"Error generating response for model {model}: {e}" self.logger.error(error_msg) diff --git a/src/utils/__pycache__/report.cpython-313.pyc b/src/utils/__pycache__/report.cpython-313.pyc index e1b31d92964159a25e29b8c8c394125c8c050781..c6a5895bcc4cd7294107675c8e25d043771eea4d 100644 GIT binary patch delta 4805 zcmcgwZERat89vw7U)R^ZcH(b4j_t;7zUtVSla@4X(otQA!}0FuDKmI z4W(t^A^uTm3>}bQLI@!Q`-L(^nz9KYCOWB;CP=APRTi2i!4T|M3qsR=Oz^(fzV>x? zT>?nh%5%?s&-0%1p7-ONJ(v?{0y(skCU<>dl!ix2 z{h%Q*pFPN@s?Ttm?$bT5KNVNC&)e1LQ^cAqW01KuSx737x~iu}+i##rRU?;bM#`?9 zn%9@DV2+D+d&w+Eld{+(7wd9&liXdGd+PE4jN`#&b@epneI&1UZ@hA!2~Voei}p+P zc?8<81>t~~7*aAP*VYYLHBhu-SQ`PE;!2Z*h$Ph_S$#C$AY(o=yEDn-v&WgF#>{xK zDKlbVn0d84W-zB{td>#+bp9W?@l11Ww8#d<+bSOvpWGrNls4HUC$c6vp|qPaO|nAq zn=#GuLJ7z%GD8XOAs9ol7cyi>_G#FrVY`NY4FehmH5BF>sRk;I{nVIj!^LnTcOcO| z|ENTx0y`m=#Dn!h4zVAJ=|l)}%ratu^sMLFG}AMU{XrZnhg!@Isa_u|Y}nDmh8;a3 zM@)D0Ly)7N!cv08HgBw-;FobLp(CJhcy9_Ce-!vfYW$lQzm_JAYCqBJc>aW`Ip^Q#2slN_D1l)P0nC8k8>Sbe*=PRN-s8h{@<79(#Db=qqsp=eW)(H*-2#B-G>jNDwje`X86$xM9(zWw+o?$Q>H@(?#Cb7`W;q zc3c_`Ov-kN6fKQD*`8sg?l}9Y z%#yOawt8dhJH9ne+7AvupxGLL%fR61TZXSikS z?vbg7O4M7u1P$2W&LhYil-H|J3of`G__0v^WI=2yJdp-uv8nDvL*2O*vD0u$bVA|z zVHE8{gHTj%R>&j<-TGp&@Zb&-QamlS!n)S+PijfBFkdfheI9U7+Gf`azlBkU> zc|v6ujbWE=fMHs^RCs7yyA*Zol1sXrwh!2JUD&3!Yul8$&l3%%9yjoa@k2YCDXg3<7?!TjbJq;zloM=?n2T4A#oz~b3&ZJ~12>5yezn!910wYKJQt*!YN`Za&j zOi^qszx2aL`qFTD(|u`lB<$<(Q>O6~1bz4y&4=cp*{|ow?xJWhc(-9mJx%3VA748X zb(?4}DCp~MR`{XQK8nXsef@e2%gr44m0-G=9jF!qZ8qMV zo2vzMz}z#sQ}9W9B8@PWKB&f==JAV1+h9R<>^j;U88~ZF@TZ99OJR|ClbcLW;alsouX30@3`AMHAhBS--+v3)rx9im@TA01CO(62tw&M63H@&( zXC6V*?Z6iNL?CJwVTApr|HH@rjWyah?;yQy15Q7(m+9}JT4VY#d_B%C<)*nwwv{`V z-OJSPv(bUL70t(KXZAAfpMkvI4)9m)5gXp%^H5+u^Znq;wCJut=ZOJtobH3eSN8C4V2hs0*Uc{tp zHpgBXS_*y(?XV(z1pyB<^(K>sgs8$M7zX|Y8&1Dofzi<=i->3l%&{Wh&WcP_2f z8rPruht~6O(MmZ`7}~aO(_eROFTN(-DNa2gDBO`3YYlay+IT?7jODbY{_>RNtmQ+E zTNSAR!rmG7ax!~=_*6_wcK9~|(e8;A=aIE5D;t+rkE!2fGY7NYRi2CfjUa*=A^!rT CDV==) delta 4409 zcmcgvYit}>6~43Yotb@k-@9J#!(PAbU1#kiPGcO~P&;Lx96UF&b{ZJ$Gyk@S^Q|p`%jODC-Av-WpTk1%9vR&e+%?<-Ep?1j*=qTA_2lER%*wwTr>8r>QA~~s(x~O~5 zY9X>qavcreN-5r?FO-aRGenp0)UhGu(QSY|Yig#vu(nULt)|4ZA0z^`BxnN-(hv>P z2#wMhjnf28(#BrCz{zgVNuf@VjB^vCgvdN?lK6*!Gna*Px{n6gjcm9ljWRStv)jI~ zif(y^l)FHKKN?0~*mF7+Z>B8+mWt=}Us+IR11%x{)}$=zDuGrL*JE(oio{sJYjE35 zvOeA5S=C@kXs_%`=4@b^Ks$V7lA~Ektm2CX-&4i+7<{k655YR#$`SLOTRZ;`$*u>L zq}??nea7y13ZJnTPkYp`^!kV?@$3>$)zd>N6-qYR$iC~0TD25qe|3rm?Qc zif$#yI9L8GAp*&$$)HsjVeh2{PGBRMF!Knzgag*rY9GB zQ5YVH*4Z19*jOtzvvmi@Om}cxifec9Zb-|)TDPDfu?Hj`x}QXPFGvh&5}z`sHXX(< zdqoTnYPaUG#EPXrRxCj>;epd6XtR{S+w%S5fweL$1 zptAWOOL_QoQul2SXvmE-QQfC>#4H!AmP_f8_l@fMm5AW|8y}Mz@uHwr_UjtSPfICv z_A@n37ib*_J}f!Cq+~Iz>tuVp1unrBy?JhY?TYsb*ZiR;e+>&j>_>rSp$KPE`Ve-o*8@LUd$!?yj@!kSLT9)u>`rL0 zbsx+fRt})VL4Xx}GBOz@fQ-WklK|gjbKxX+ggqU;nNb|T*tE(|Jh|ywxuBfAumF47 zo;W`*Z`z+!7A~-3kru9rJr&9DZk$`WFt>c3y&UO@??gpmY5oiI^2)gj^9%B(?bOoz zMP(0rJMu%W%$|?7gW`>7hHXTTa3d@i8|wLesPXEdZyjQX?}obWgu1Rzz7g8{y!)+4 zYNM(5PNc9AF0OK`9jnRpv5nxqwezvJ?Yp|CY|}aGZ;K=DY2Nw9U=Rjxiu`oS@n*t- zm>IPI>n_hvCxp8pFVZ=Fy2zeuDq4yzXj`t=X&0hKYau(RO>#gP+e=p2iH%D8@iwDc zT&8MqQ3uwG+OfFdnN&L#pW75+jpyNO_emZ>mxb*_Hk?UXHI2MSvLer%OOnlaw8BsY zn&jhN$+lgR`lz1UeXxv=g=_DW9FX+JE)E$^1!uSA3P z;g`Wf(6>W!ZF>k@EpRty;7=CM)}Y{lNV$iGtZ8zV>nN>#r}Yn9Mrnk@!Xt=yC)j=BU$Ik zU}ER6{}(7Qg>VGHf?8wrK3X{S9KbJ;cmzRJOd-{VfKgTUvUfT^DBN#%l)`oHB2h5_ z_a1zZz28Eet-VKZILcn=p5VsW@48RtA7tNm*~uP!C}GcsA7oFcDYDW4@E&=FY~;sp z9a#PRb@vaJZXNh{#RC#^^>%W{SgAM5PWCnrKZaJFM3_T3g)j@SX@$2~!7fcX4bb6N z{765I5JAA^M)?YRtvA*Jg+Y`Bghhlh!WR)PA$$p7#ShPLkbTrU?tB@CF&irv_U7t5 z#^Kbpg=;g{tsB8^c0Sg*_G;m(_25t2Mk7;B>(7Sx$=$A>3l7A*X=zH zz+aKEoxpm1CqHFz{o3Y0dfCCwLQXtLjSsLoxaw*e*C8Q zPHFreLFR^ZTqBf=72+NNrNb6u@Tlc6%Ll4#g{6cDcw2FUO&z=