From b76312b024e879a81ed8058906b850557e8bb40c Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 14 Apr 2026 22:11:12 -0400 Subject: [PATCH] feat: upstream TurboQuant watch tool and report (closes #15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monitoring tool for tracking when TurboQuant lands in upstream llama.cpp and Ollama. Checks GitHub PRs/issues for TurboQuant, PolarQuant, QJL mentions, checks Ollama releases, and compares fork freshness against upstream. scripts/upstream_watch.py — Automated monitoring: - Search llama.cpp/ggml/ollama for TurboQuant keywords - Check Ollama releases for KV cache mentions - Compare fork commit age vs upstream - Generate report or JSON output - Run: python3 scripts/upstream_watch.py --since 30d docs/upstream-watch-report.md — Current status: - TurboQuant has NOT landed upstream yet - Fork is CURRENT with upstream llama.cpp - Continue using TheTom/llama-cpp-turboquant fork --- docs/upstream-watch-report.md | 21 ++ .../upstream_watch.cpython-312.pyc | Bin 0 -> 13619 bytes scripts/upstream_watch.py | 225 ++++++++++++++++++ ...t_polar_quant.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 37060 bytes 4 files changed, 246 insertions(+) create mode 100644 docs/upstream-watch-report.md create mode 100644 scripts/__pycache__/upstream_watch.cpython-312.pyc create mode 100644 scripts/upstream_watch.py create mode 100644 tests/__pycache__/test_polar_quant.cpython-312-pytest-9.0.2.pyc diff --git a/docs/upstream-watch-report.md b/docs/upstream-watch-report.md new file mode 100644 index 00000000..827d1b3a --- /dev/null +++ b/docs/upstream-watch-report.md @@ -0,0 +1,21 @@ +# TurboQuant Upstream Watch Report + +Generated: 2026-04-15 02:07 UTC +Monitoring since: 2026-03-16 + +## Upstream Landing Status +**No TurboQuant/PolarQuant/QJL mentions found upstream.** +TurboQuant has NOT landed in upstream llama.cpp yet. + +## Fork Status +- **Upstream (llama.cpp):** 5d14e5d1 — hexagon: optimization for HMX mat_mul (#21554) +- **Fork (turboquant):** 45f8a066 — Merge: ci: fix turbo build + test failures (#66) +- **Fork freshness:** CURRENT + +## Errors +- turboquant OR polarquant OR qjl: HTTP Error 422: Unprocessable Entity +- kv cache type: HTTP Error 422: Unprocessable Entity +- ggml_type: Remote end closed connection without response + +## Recommendation +No upstream TurboQuant support detected. Continue using fork. Re-check weekly. \ No newline at end of file diff --git a/scripts/__pycache__/upstream_watch.cpython-312.pyc b/scripts/__pycache__/upstream_watch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71c243a6e078619d93ce4b2669c8d249e7613e11 GIT binary patch literal 13619 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 L^%zm1%={since_date}" + encoded_q = urllib.parse.quote(query) + url = f"/search/issues?q={encoded_q}&sort=created&order=desc&per_page=5" + result = github_api(url, token) + if "error" in result: + findings.append({"error": result["error"], "term": term, "repo": repo}) + continue + for item in result.get("items", []): + findings.append({ + "repo": repo, "term": term, "number": item["number"], + "title": item["title"], "url": item["html_url"], + "state": item["state"], "created": item["created_at"], + "is_pr": "pull_request" in item, + "labels": [l["name"] for l in item.get("labels", [])], + }) + return findings + + +def check_releases(repo, token=None): + url = f"/repos/{repo}/releases?per_page=5" + releases = github_api(url, token) + if isinstance(releases, dict) and "error" in releases: + return [{"error": releases["error"]}] + findings = [] + for release in releases: + body = (release.get("body") or "").lower() + name = (release.get("name") or "").lower() + text = body + " " + name + matched = [t for t in ["turboquant", "polarquant", "qjl", "kv cache", "kv_type"] if t in text] + if matched: + findings.append({ + "repo": repo, "type": "release", "tag": release["tag_name"], + "name": release.get("name", ""), "url": release["html_url"], + "published": release["published_at"], "matched_terms": matched, + "snippet": body[:300] if body else "", + }) + return findings + + +def check_fork_status(token=None): + upstream = github_api("/repos/ggerganov/llama.cpp/commits?per_page=1", token) + fork = github_api("/repos/TheTom/llama-cpp-turboquant/commits?per_page=1", token) + result = {"fork": "TheTom/llama-cpp-turboquant", "upstream": "ggerganov/llama.cpp"} + if isinstance(upstream, list) and upstream: + result["upstream_sha"] = upstream[0]["sha"][:8] + result["upstream_date"] = upstream[0]["commit"]["committer"]["date"] + result["upstream_message"] = upstream[0]["commit"]["message"].split("\n")[0][:100] + if isinstance(fork, list) and fork: + result["fork_sha"] = fork[0]["sha"][:8] + result["fork_date"] = fork[0]["commit"]["committer"]["date"] + result["fork_message"] = fork[0]["commit"]["message"].split("\n")[0][:100] + if "upstream_date" in result and "fork_date" in result: + u = datetime.fromisoformat(result["upstream_date"].replace("Z", "+00:00")) + f = datetime.fromisoformat(result["fork_date"].replace("Z", "+00:00")) + result["days_behind"] = (u - f).days + return result + + +def generate_report(findings, releases, fork_status, since_date): + now = datetime.now(timezone.utc) + lines = ["# TurboQuant Upstream Watch Report", + f"\nGenerated: {now.strftime('%Y-%m-%d %H:%M UTC')}", + f"Monitoring since: {since_date}", ""] + + seen = set() + unique = [] + errors = [] + for f in findings: + if "error" in f: + errors.append(f) + continue + key = (f["repo"], f["number"]) + if key not in seen: + seen.add(key) + unique.append(f) + + lines.append("## Upstream Landing Status") + tq = [f for f in unique if any(t in f["term"].lower() for t in ["turboquant", "polarquant", "qjl"])] + if tq: + lines.append(f"**{len(tq)} findings** mentioning TurboQuant/PolarQuant/QJL:") + for f in tq[:10]: + kind = "PR" if f["is_pr"] else "Issue" + lines.append(f"- [{kind} #{f['number']}]({f['url']}): {f['title'][:80]} ({f['state']})") + else: + lines.append("**No TurboQuant/PolarQuant/QJL mentions found upstream.**") + lines.append("TurboQuant has NOT landed in upstream llama.cpp yet.") + lines.append("") + + kv = [f for f in unique if any(t in f["term"].lower() for t in ["kv cache", "kv_type", "ggml_type"])] + if kv: + lines.append(f"## KV Cache Related ({len(kv)} findings)") + for f in kv[:10]: + kind = "PR" if f["is_pr"] else "Issue" + lines.append(f"- [{kind} #{f['number']}]({f['url']}): {f['title'][:80]}") + lines.append("") + + lines.append("## Ollama Releases") + if releases and not any("error" in r for r in releases): + tq_rel = [r for r in releases if r.get("matched_terms")] + if tq_rel: + for r in tq_rel: + lines.append(f"- [{r['tag']}]({r['url']}): matched {r['matched_terms']}") + else: + lines.append("No recent Ollama releases mention TurboQuant/KV cache compression.") + else: + lines.append("Could not check Ollama releases (API error).") + lines.append("") + + lines.append("## Fork Status") + if "error" not in fork_status: + lines.append(f"- **Upstream (llama.cpp):** {fork_status.get('upstream_sha', 'N/A')} — {fork_status.get('upstream_message', 'N/A')}") + lines.append(f"- **Fork (turboquant):** {fork_status.get('fork_sha', 'N/A')} — {fork_status.get('fork_message', 'N/A')}") + if "days_behind" in fork_status: + d = fork_status["days_behind"] + lines.append(f"- **Fork freshness:** {'CURRENT' if d <= 7 else f'{d} days behind'}") + lines.append("") + + lines.append("## Recommendation") + if tq: + merged = [f for f in tq if f["state"] == "closed"] + if merged: + lines.append("**ACTION REQUIRED:** TurboQuant PRs merged upstream! Evaluate migration.") + else: + lines.append("TurboQuant PRs exist upstream but not yet merged. Continue monitoring.") + else: + lines.append("No upstream TurboQuant support detected. Continue using fork. Re-check weekly.") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="TurboQuant upstream watch") + parser.add_argument("--json", action="store_true") + parser.add_argument("--since", default="30d") + args = parser.parse_args() + + days = int(args.since.replace("d", "")) + since_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d") + + token = None + gh_token_path = Path.home() / ".config" / "github" / "token" + if gh_token_path.exists(): + token = gh_token_path.read_text().strip() + + all_findings = [] + for name, repo in WATCH_REPOS.items(): + all_findings.extend(search_repo(repo, SEARCH_TERMS, since_date, token)) + + releases = check_releases(WATCH_REPOS["ollama"], token) + fork_status = check_fork_status(token) + + if args.json: + print(json.dumps({ + "generated": datetime.now(timezone.utc).isoformat(), + "since": since_date, + "findings": [f for f in all_findings if "error" not in f], + "errors": [f for f in all_findings if "error" in f], + "releases": releases, + "fork_status": fork_status, + }, indent=2)) + else: + report = generate_report(all_findings, releases, fork_status, since_date) + print(report) + docs_dir = Path(__file__).resolve().parent.parent / "docs" + docs_dir.mkdir(exist_ok=True) + (docs_dir / "upstream-watch-report.md").write_text(report) + + +if __name__ == "__main__": + main() diff --git a/tests/__pycache__/test_polar_quant.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_polar_quant.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94bccaa65bca483366cbff96f82d87aa4a8b4aa6 GIT binary patch literal 37060 zcmeHw32+?OncnoAJqPZCI4G)lb4U>&NbrJqftN@fq)1VRxsj0}dH@0fFksJsBt`?9 zw&ksXsDywehr`$#fi9;6l$15s>%{P`H&UFfc)hMvW;me8%+j%TR~4I;tqMgtt1VT^ zo9}cfWr9-v9pVz4zmyBA*S{*FO4hT@P)r+5VP1+9j%vyB`QP z+eMpfJ8qK&+1?`@x3jyW*V%3tY`w1IZh^;mj*BeDd)#Zcg~jelttjK(X_Fn_5D-^w zglVpKoNwE?`*=WhAx)hYW87u8b$EwU_PpXeUM!2&@yJ(V9TP6?u-D=ND8Y*o%4DB) zI@upCl>_1O8EYWQMaW+n_>IUt>g#-T^pZVFou9y@HM`KZ`Bce!8 zM|wiak%3TOObYk4N91s$9HyJJYU#%2V1v(hj3teo3`^}1C2UUL9_j5@!qI3~q)+M? z=xdL`5pD4`HAwq-Nqc!oMvoec)1zZp(SXMO7%q=S8ZVN^$n z$OG*$+{4kZaymq1wn#^Q@ZBd^N;u^3DXQ%%4xb1HeH$92=MNl{4lA*fk{JtqC&yDVS5vi%MY5kURJb>y3`)D{osjoXL^?6h(Se#rPKT9_p2(S~ z)YT`QKpXdm+F$DG>qHBl>_T8v3Q127qKbWz5=I8JL0IbQ?e7Wq;`!`F+4JbrAX|fU z3{4(tj}3%+e0$ccDNKro^z?*!Lu+FL%85urdw;(aJsBD3k)?jtgeY4^xn1p{9y~8P z&=ZR`_-@fCI}|MKd~5KB!IrQ5_}$LfvWx#X^11)`Zs)64{`%Hezy1ApJNI@)Zh!el z-+8z5TW^Gu?JvIbZs$W=C~)lE&iclmEOUS1%erHy7PV7Lyi?1*Q!8+%_K2O@Lw9!m z-I2*fU;ZDNot+)*dVlAHkaNkg!TxYg2<6;L81K~Aj=rj2Znzxyzq^Vds)ViE9IGaK zTfGn2Vw9F$3ESIh${|4(hU^J@LU;)%NI4dBX=!AHCG4^zj*z$2oO^9AZnD{i97d{y z1Aq27GHHVfX_g?3Ghs_O3B3%t$i>qzzdPYdxG4vPZ=*Z;fsT!I6#CjSr|q>ZP8;eV z#7j|*QA^>qV+ot=jCqiZUELbW)&g)(2--Ww{We>?93|ih&)9;lc;o(1G$uVC>WQ9Q zdmtpEzbLYFObPWxG4^|SjSLwv*w#l8wRki4g{le6#OB>09q|3h| ze&j{g+YXd@J7DwrlA&QwQn+1W^9Pcl^UH6S+g$Es#cRriGs9;_PMwcuytNr;Eh^^^ zoI7wAFJQ|#qDp&S@219Mz5AD}cgOU;ZSK96ecB##Qjv6Z(-= zPB_r#5K28~$Z7QUZbmfZ=S~Qwrv>uk$oGz28yQ2cScw*I^iGVIt$2#t^c3{=!V4qF zO`|773xI{DJX<}Zc&#D1{CT6Y-}pU zaOK=RU6^`1b543(&J_~c6)Y8W34`mG^n(!a}p8)4>ha`@!Psc+if5-*EW z#ldrr-7G2}X*&PH9h=>?Iw{<&tiHH^bbqR4EOcr6)S5>#^^ayNwbvbihF|z>#ibWs7=B?S1Z33l&E0PuxO`x$XzjWEtXPDWo&W3|o8a1( z6mHZ?z;$(FXU0!mX_^?g{JF`p%*vKb?W0L|^3+J{@ab2JZ#$3@)ee-8G$*&EoePv2 zytA^J0JX2O9H3pD{NS|N=qod%^m8~a+6<)BO*2Emm9WS3kZv6%=|Q%41RXUvjLFPE zML-QJ2>~UXvONw&COckQg-VssM2QeQkqsbc9HB;rXA$2H`UX=jXhq2ruMU<{i8dj3r1uBs!?HPvrQIOhw(U=0-83P=2Xec&)bhDBR zhV(5$)XGADpqmY3g{E3XB0$4dSweuOV8rKl7X_cl#>b#T20uX)5Fd3GLykA4(ug!q z<3IW(01UaBSqF~OwHCb_GwZ40jU%nEw%n+FcuW{Oa@l=p(fG1VZA0=ggg*X(DYPk5 z`v?!c?M3ci6x;musm&SR(lI&XTYYX1MykIg89g67x90r#5$? zlIE^&%)wTj9Nkgx=32KxmlERDd%!6T#6XfW9)}a?v4)@{=VD)qBT$DNi3RP9l2X=D z6H%2U6@3-4tZiymr5RB@l!iXcIopT+(l^R4R*zPvgjCb(3$y-3Y3CyLYVC;EFO9Ca zOW7~l!nWfASbk8Q#~rf$729#A>;QDhPC(3CfF9WmD9Ro{@1Pj;=4y!_>gO^9{5K+i5%b(*7(Y$*7tT0PA5l01=tP{M#T)F941(&Q7Kvv|DsWuj*g7#U5 zgcuDDb{PcDKf(J9*}*gz#19czc8xp$fu(%{Tek)2V~W*@AMGi?Y(m0mCWZ96xMT+> zIbNdYdlH`Lw_{aWJ}W5@r=)qN;4)J733$FyYpb+w?Tuy6*V!yFB${$a1x3RviwW1)Y=5ACZ`n(vnCwf4 zT>{NAL*Cd@EtjdJWm=eVPk6OwYxgf==$sXXJVWk5ThM>#7NN#ly8smGHehIlZ4*Mg zZ27AG!F9o?v`r${h9F3yu%bki346|g7y=tUsn^Gl#3H3wBqD(k>I}C?pOpG`grbmz z#8?R%Q3CR8oTyN#F0f*9q!)Zx7=#fqW^x}$swiWoJq){m{vtw$q28RMH*{863~$c2 ztDnfjSX)TRm9;@k(9@=b`xO%Egp_bDfatap16@53Mu4#Di6BKZSJTFY3@m9|XHVor zsHd$j)Emxu+oB5{MpTB&!#AS6FxN&RHrBQ_KGH)YLhoL z#~+yyyERNteHMjArvT2`Zi>DO-eK>B;^E>EWlF5O36k;P@WBg*hYzQAX9Evk53I-p zR(w-=D}Fhi4Xg(>xF9&bIJ@AH;Xu+o;=Eb8V(j#I?8?z><(8xub5vQyMfa%t4dvqL z(bM2rgO_@dHe0$j>AJC?E-khu2S%d9FJBj1@w>*o+@r$`g>qlka88M>BT+p~dePR& z<&z823#pibAD=yRbcMHkfVR;H570Im0er2720!I!TiJrQxpfYp6aTpWVW-l9n?ig! zLpRC}a=O*8?jvftByr zzvY|oWdj>#LM_i^E4L=SAC{IsKrCoh^Ywrpd_WK0cY2T`78~YI6|vZM{RR-r`i-0J z1+gIcdGvgxjR47i^4Kyz0vKE7cII=vR&1Fpm>4CbFT6~$V8JBt(HS?LRnjHYTo!F& zmGmO5WYG}f81WXe=pnb=mT)JW+B`Mnf!}3FhlfNkONtgB!4f|2#B zh$|4Q*SdzOBrDcDpgmE$tBkA*%&eOjTKtRn_YHX&W2?M~f|<;)^X1AveC)5|uW$+} zD8#==ENv6GTi?r?OWMx3)b&~z9BL4fHGuc@4XvWITeJjls0qN?@^au%gTO8~Kxv@n z*%XWheM9)E%ybi?xy150R52(jJ8>iCc)hZV>P(axc{dT*l5VS#nnr(3x3w2dtms$$TQG^_%8dV0&8X`M#1NnmR~qMd^#;QzH#DW$7lx} zqaCTwWUE)gh4AW?*^*UojyD6OOID3HXG$8ci}ZV64d6ZrINQwIK;H#`Mx_i92paeZ;`mC#S)$1UMzK+L5{}8|$S==ZoPYTJ!!%l1I*!)=7I!!iKcU`QzCNeKv=3j2{YFjdv^p7ggLA;>7w1T;Q+2Xe9sNF2 z4hyObl$=NQf6Zx9RRox-qA1~0DLj9S^s?+Sl_jY`rdz_VVG8XI3a=Oxop(qaEDn~$ zH#|w21?hCCXCNHaj0RO1yL6@tW+2h7-Y%#gU=G=d5kDY@8-h-ija{}&bG)ZIDz%Wx z_ij;jxPFpU@czzl=ckh!C^DW%|63WeShL;*wA6f+ztiH zxW>c9j8>Ea`s^WkGBaeD2a$G!5*w6SU`9$U=OrzD8)@NlB6Fgh6#6pC^8|>HQ$9oB zI6#zULNm?9^$bf2NG`oqq0MDRu3KCQOCz6Ay4a1+mE>*VYe4e&8Inv(Nru-gS zMfog&69l3JP80Y%zyue9u&<Zmxc*XW#or~`{Mb~cOPNj!nIfri7x;v#F zSN+{`pX*71RgC3qM<)H!sHeL$+-KIc4?1C5xsbj-vwv32pUu|K1Jt!=HGs~O;&_mn zNpX$>erlELdNW#4!QJ$Z1=oEM;@ZtheHbTUZl^0>p|4?8Cc$!I&eZ1(ST4Fu%6@lV zX>PqV6YUCPC69TYG^pByLwgSF1W^Yze~Pj*UuK2*(@>dP%!HtxNn+(DIhSev^xQW^ zzdqB=sp!{JGV0l=wUwe@r+Fdm;EH~~={+U-Ci#~PNuKP>n?L<*{tTETBulkCrm~i4 zVa7c%lZ1pzM2ZeE(Jm*uiHKJQ3A7N{Mu48n=eCFWk_OdgXS!-%d6|;#Ca?z}$W{8v zJ_^`R;4uOR2(a0%nOufQ;@&4ZE|SOzr*gteIe%9_o7l*_%n(rI{4YVbuj<=#MM$Hq z@__oCy_(g;H-oTYA)EQ~ESF4k-qzW$9#^-|ko9|Lh-fu1?60C7Gil~6)n|}OELb+y zI(8~s9ZdS+t66Q<)K{+>uf9@wC7P{%H0i%tvwSQxUZ1Vmm@Gods+yF4tT$7$AzRt} zeaGYz*)0eDqB*nWaHjHUb*em~O_fKki$|`B%nO(Km!VJ|kTslB;t`rE)s*Qat?vc@ zV$XXIr=`5SJi&B!#g7JKlfn(4&QI&4K!(=Le)G44rh#l>lnrKOFIftKS-E7OXDdNJ zkS#GQZpxE~Y{@Q;T~^}wJZ8lZ$Jc3jjJq)<0NENTE!YVoB}2Ber_~X&j%64f!Bg;tVw zhbV_IY5@O5_$RSW3I5B5N(cSH%J|mjm?noUL5+eH#yj(5h|shz$pR>K$OtAStg~qxom2&1%*x>r%mrT@g4U{rswDX zORB^!aMBP?&0-{LNs8YH6pc8BA4>`!Rxi~l@&et!jPy7bJ1u%REiOvFoH|0nsYUo* z<6iF3VTMAvFKalb#6>9(*ObltL*g3`OKDL`4y8_IN~G%|{jPC8_vkP~q1=}>oKvEd zI;E%49Y}nmV$s+`nTm$Ac;K~Yx}qUl(g^2wmR@-&yM8xZY53Rg&Xkb*y=Pd!?)RR_ zlpMG&((iprfdKB)VTMZMzO3QYqpykwMjq9pHK#dx>>)i`b560!=!v+4)m2aWouviQ zc+i!X^k~gVC9U7hgOOrd?e!Q{0O(fEp+_lSAn-*3Un20&2wWoY3IP)JbFtp?OPI5^*xK0FN)08FU5Rxk?%-*mfXq zBZ2`^Y8LKAU@fs*4HgZXa0pmjqCMPZCI5nURp^*&!JQ$!vRmnYvEXh8lWfiLq&f6e z##69%2=Xrni9ljr)^GrZbq&_GV|$S=V>wKRozlFvi`KhaX_gcAxDz#Vnc5(rm1o?s z7FqXLYSN6FxlJX}O4WkP6lF4Sac6Y-EoXK8@j7<>3_6IozIL~HC}K1*}|NLHN; zyeERuB#rF0Vt*`H8q<4PNbewv^W-hM@sZd__M0?Wi?vT+Dv3r}!DY!%(xMyEWMKvJ zA!RFaaD9^xuAhB$@^6#>_8&gnVUoserxfVvwoew(9wDS-is<`pi!Fi73CD7d@L+h}=0N$HrU_NkMExu$Jg+XDY>)KhJgSf|C-Pl8VuRfH z{CRrvytGUU*i4S?5+Kym_CYb0v=m|`E?%3S`fu>Os0TRVob5)S7!$8*(u38;BhP(v z{aY=UTc!dH&?eCy3$NGQu{pdFwpJ*wytry~RjLWAlpnwR_*8jga?g#bn&h4lVR-+| z>blg1*SnIfuZBj-lJcvM-S!|8_D-m%yts69X-XJfne>phY18PY)ME7+6%Eig<=Lut zj*FQ^>#_@)vZd>36-<3v+y_m6Y&daU+=t&a?&TgGW+;^VvW9a?+&2=_)1)8XJ{g{T zKK&3CQ}CNEO4Ic#53n`*3UK%XY)$RJuX_;Y{eOt9$tJ+S*2*7{w}k)^1g6C{|BAf3 z3G5;8CV_7N%%ESdvBZ4(nw$BdJ|zE$9--mB65zPs81D0Z+Hqgn{^?aCnCLxAx)MqT z(X`);-DK6IuU>#g*r1!ON^umF;xuWA?fJ{NfkFL5?}DCWR*p<8%NKDQ!XayV(%Lfe zehDCTd&fEHNj>E})D0GvktQn8nx4#G#I4bjhAriM=}BF$tWoh0r;#X)Y$*f5qIfWo z0JgxkbcqCnO^L)=+_6_(Sm_+pER)oYcMNeE-yEj^ml4Iwb->m8c=iJ0CIh`4noIbE z?V<*H`93@Jo^#lr=M$VkpJCdRE8=g`b|MUu@vuTF_*_5@3w6X`l*OuTMRF2#tqG2_ z<VsSFP;8cakA*c zKQz9z=yD3DjE-DYMQf^8z+l0mHS933lY&iC}m+-{xGNg zh>(Q8fDj}?5?WcJ4~dXeoggGlDbq{0P6j9I(~AqKNMur_7amLL#anoHWr6_N#ukb& zzD8jLeu{s#;RlH@>h3s;T>FJPi~N9h>wJJ*>_I851sSUlGnXKP@A@(54tjD6=qqYF zCYm-(j0fkvVPc+~16VcBGReT8m`w^VV=pdQAf=%;V&{^LG3taGBI%F2nAp+d|%8q7iFN z}99t07iCUotJL+J4cPGOyyhD$gma0oG@!mjFhN>8;ZI6 zRReu9k2!0gr)0Cz?6uXcnCEyaeWru5+{M;mc}zL1W@&xSk+pftqpE>KLse)h!^%oo zm%(WLK%KW7n7f?#cMrMP{I$gxnCf1jC$NQRKV%DMyRaWexMxs`4)*qju@`GQc88*! zFC<#Z)4*2^P!k1(Pwd;a2Zi`HTBdbxDw}P5@fVhK@f}9I{gEilhX(p$u%iuka%a%R z)@doP<6$QKOb5K!CrRIlmF7$AEkfJ6$?>whEl_C1nbL{$hp@(q2AD!aitkf(?=z&A z&yasiMSO?AzaT)G7Nr-!tZv!FJ$%8Hx-v^$ebr>OCT)7w72ZU)rK=SYhj z+;>@^1^OZEKCh%DzJcxsS{RZOM-BY zA7P4v;P{bDNduRzGsOV{xKD@kP#kRKiJ?~*QJs2j2=h~zCP^`YXwAtK2SFZ;6sBjU z*Qsg@y#$_fx(G&}NEEb+dC8QqFBHVv6F}oaOrKg3LM@ta$Zih7k_?2gaJCgDBfWvn zqhB^{7^x#+BvmgGbu+}z#M#>ZNv1qh!-C7$>u!#8y5)iFQjH&7rW{0x6+WQ;!03fmBgw7q;oq~>6B!p?93>1CVawMM~QREtuupB*-{i|6ly z1T8Sr=^lcHCgx$ijt_aFd(8uUquI0Bypt_gomMmY z*BGnpje)Ef@oPag6^?B+eI1SL@8Q`Xej!vs#ZYfnWf-iDnMYU4-k#1vB&X%Av z#Ily0z%0t2ppE0^b&s$aQQkn{p=`Oar6;~m0YpleG0jG9;&EIL=G4O$<3Fc?|T~hPM0hlYtJtc0j3gRB|y)bp~NXMI175&#?cF2i(kgvuFYTSrtdVc!2JT9!CJ8be>JBbYiPhsCTsJqH*Xr|i0K zHPUIMdDm!K4q`KBe+v^Lp7m`FI!Sha-N{%iocjT4#;H;>rUf^qC(@)|a2Xq&XzvBK ziKrPu#10@IqGYx_!w*ehc5&geIBx+=Qqv|$A8zbShUt56H{2rjcmmwg_U#iwd>x@2 z>_@?Ouz*7~RwDy96)}-f+jh<&5i+5!!BEMB)5H+%qR=ov9`YfMiWnp;e>3u#P|#D9 z>kt8}jVRol>|t`fA5bF3+z_@=m{AN30xsO+giRsa(zL0qCww~G6P-8PqUWG}A);SJ z`Gj%Grz)0aOO~a@)#G-6bjh-DV2ah(MfzP+1GrCz8F4k)E>ViC;nbtAimOqm9QX~yk(4&8wy46_gcd5+rR3k1GM;7bH13A{_-&k6jPz$*j{+oCJvB~U`ISqBFR z<~H1Mx?xpxr__mMf&qtX^W>vG~VRn6iyVb6`F&*1$?=|Q4P1AA^9=(Tcnao=jRcoze+zng` zVVO~PD-o04vZ#S+A-~m%6~m}&jxr5lnMcLs=53Q`ycAqmSa0#I^LSfr2Va8}eMl?B zR2vI5Qo@Q9C1WB@fJVFca)Y9?C{g(#fgcfIn0ht2{u1DmiszLG7+k*5JC@4Fr*G1$ z;p>S~lU?y+RKfuQ2MMepz>0o?Tu&0P!sGco?1V~QR#I}RQ%-2(M5PsGCz3^1+i>Jw z$+%Yadd{wW!Lvz^)|^nskRdN+LV^@__aP^K z>1CU-9&i7j`mYS7m#ATSBpi6!EM(>b)QEj=smB#u4^U%4gP81HO~}X!-F^yDrlmNf<@-5%5n0)R}>nDrS zOVuzv5)M3VI#oofEeHPZ{t4jkCc~Jp@3oBxFP;Zu2uV7WeE33S+X0(AP{c!SSlU6Z zpYSlfgP0I;T#C+iSlLHlVZR8GAX(>m3|TvD8p`;<0d-^{elk%|)@ePO@XRJsLKf|VQAmUdoM^yV||GHcY+YSoBOL$2CgwOA0@ zx)~34X1@2A;SW z4XN_cE!P7(@Vmyn+@r$`g>qlka83nwq{{U)>4&#WR!q9m4^c7Z-}`|bH|iFq1Fa*? zsU@RZuLoN3yT-lTqr(h^a$nYPP6b+1OY}5psb#Wxaz%O(6;trTLvZ#gUUvGVIxt=h?fgWRo1u0wbM?>{QQBv|~#$cF-uPnQ;^; z`Wcxk(ul?RiTSZ^njZDoAkI(JC8GtK^P5iUGSuC4@>mSz^2+D(-xxQvZ8WoEa+ zH#)m-t>rbQiM-a@Os!>liiI@N6l*ob!M=uGC#K69%^SIC(_R8oDr@VlXifdiyCvPJ zJBEB{c`wsa`f*eNwszOb7z!i;%oug4mbiPF<}&WkkoiHRc&QZAH0g_>U+}9NlJdeU zk4Q1fC&^;LWk`K7O&Td5DM$vdkrLCSPxjxp8tA5!bJjpli5mFjfNDo&+8L*t^eqLK zZ0wCv97mj7GVhw#np(u7?Tjz4eN*t9v$o;%9C(wG+4CfHqAOv3Q>!neZ*eD=syttD ztVwGQ(^J-KVa7e7?}DPe3rM>%$kBcH_YVaI%Yx9M3Ot>FZ)weP|^bG=}?>rQ&n?v-^Ru@Z@ zo+xQ{I3Hi+LHZ27GYIt%e&)auxNv@QPtP1m-+W~O2&iYNh)M#~EqSrDjDRma_V6Z? zl@%03h(h^*zzYO=2wWs^93by>fpt+*CYfj=a+;OL{FAq_<_<^O==;Zm){;Goq)!q> z^pkl~M0w}vVGq8#g>Tkesu^4Q&e3nZF!91v?WSZgEmn@cUWYT^y<3v4H%dxJnuZ5b z!mIHc%T|v!T@fZWT|RW>SZ3MQk>b>;tEG!SFa?J)%QmaQI8fHR1qaGjEFXJr{MeNZ z+49X;xLjU6a_055NzaX{g*f)Wd)fO|@#W(2Gg)c#AP85KXau7?=b<^ ze$o=k{LYeZt(aJGrEaqIj~<$8J(6iUl3jf?D?O7ulxn&fcnGIBBQ-35wQ~~=kMxZf zXR0@)#lv)r!r^SmX27c@o3D$9@w+Or1l*&;425!E)^O_4$TVK8M{7<5AYXbb6>I*X z1Uhhm!c5L7@h}~@poXTk`sf+2iH8{#cL?@U=ctc;fq|4c3d4b*37z>o@l?H#9s*2efW_!!(i?BIL1rrPzix=x02wc}sEPy)N|5<6$5 ziyw3ZoqUrII?oq-H`95{M`=Y^yirAXWV5Dim`S@Xh1ySc!FD1l;hals!~|1Th0@QU zgP(Hz-=tm>f|X?xET#!Tv8zBg?|^v4VU>s5RYT+g`jXLeIP4zOAu;`XL3eSHi=mG$ z)yg;SSX4tI7i=S+$Mnztz#_B0+loEcS$WVBMolb)A_jen8F((uxFK@UPN?;oO1Gdf zLAB*G{hEH;VEX5)-_~b;)4WW3g^|c5pte42Jm%Hx#DR43@v&$ZvsqO};iVIQfls*HrQU>=IG*7L z2}jQ4NnX@q7Vk2IH}QqmSxH3GA_ZqQW5~JtnD~gBGyJLonOw1U&V3s%O(#}zr5ZL3 z%WB-u@!Iz)r)h>POW(*Cco}jU!rJOGoP1o)Wi{40dT5qe1~3m<&5hbRn)H*!ALfxL zH|iI^x&6}i@up1uBWbbbgVIIm#Z8&grr*-ZzVC~z>;&Q6+9@g6dTaMJk$K@V|1uQH z1G0v5O59DSq^K#=i&`c(POeNZ%*)B6)d}8ZIl=!OeZ_*w{J&Eefi7xh61&l4zV)ut z=h~xA+7uhCXBK)nQ9GTQ+0Eo?+xU6b4g8!NeB}lud`9Vt;cRL>DCbeb7$Kc=^$qm) z4>C$U=jPd!1l59>=rF3=(IlB5{oOo}huuTMhp8zex zWyXP=Mq%%k|GuAy+xYPXJCwgiVj@4HMSvjk1mPpQUvPe0XA^{1Y*}0RU)hRom)a^9 zj;z19Wpv9Oo9iL_ZJ*5-xKKA-_Y1$PvMOyWyajTnDffUyA<+qd5iF*@R5z+$Gh!I vgoE%9{P?I46!ybI@Z*0f)Cg-nvH^U&qgi+w!2~}(EffiRggZ8ZtP}ko`!gbA literal 0 HcmV?d00001