From 466a71ecea9bc19557e95be7a052ee0c2c06ad3b Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sun, 18 Oct 2026 06:34:21 +0800 Subject: [PATCH] =?UTF-8?q?ml:=20=E6=98=BE=E5=AD=98=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E6=8C=89=2019=E2=80=9320=20GB=20=E8=90=BD=E5=9C=B0=20+=20ort?= =?UTF-8?q?=20=E6=9C=AC=E5=9C=B0=E5=89=AF=E6=9C=AC=E5=85=A5=E5=BA=93=20+?= =?UTF-8?q?=20R4=20=E5=A4=8D=E8=B7=91=EF=BC=88feat=5Fdyn=EF=BC=89=E9=80=9A?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 显存:APG batch 128→48(实测 d=192 下 128 无论 plans 60/150 都 OOM,崩前已分配 17.85 GiB;48/plans150=13.08、48/60=7.67);新增 ml/common/vram.py 每进程硬帽 VRAM_FRAC=0.78 并把 vram 峰值写进 train/APG 日志、监控页加「显存峰值」卡与按 20 GB 预算的显存条;smoke.json 补 apg 段并让自检带 --config。 - 自检修复:find_bash 兼容便携版 git 的 mingw64 布局(本机原返回 None → 网页「开始训练」起不动);selftest_proc.alive 读中文 tasklist 的 GBK 崩溃;rec 值非法 JSON 时静默丢项;gen_resume 判据剔除 plan_ms/cert.ms 两个墙钟字段(旧判据恒 false)。整轮重跑 7/8 通过、3.5 分钟,仅 train_term 为 Windows 真实限制(TERM 送不到原生 python)。 - vendor: onnxruntime-web 1.20.1 本地副本入库(约 33 MB),查看器与 R4/R9 探针不再依赖 npm/CDN;.gitignore 同步移除该目录。 - docs: 第八节 R4 复跑(feat_dyn 新模型):部署口径 P99 12.67 通过;WebGPU 首次实测 27.09(有界面窗口,adapter nvidia ampere);viewer_ref 随机模型 planP99 131 / starve 32.7% / lookaheadP99 170 / certFailWhy time 948 / tickGapMax 736,三条判读留给实现方。 --- .gitignore | 4 +- docs/ML运行手册审查.md | 118 +- ml/apg/apg_finetune.py | 4 +- ml/common/vram.py | 21 + ml/configs/apg.json | 3 +- ml/configs/smoke.json | 10 +- ml/monitor.html | 9 +- ml/monitor.py | 12 +- ml/selftest.sh | 20 +- ml/selftest_proc.py | 12 +- ml/train.py | 4 +- webapp/vendor/ort/VERSION | 1 + .../ort/ort-wasm-simd-threaded.jsep.mjs | 130 + .../ort/ort-wasm-simd-threaded.jsep.wasm | Bin 0 -> 21663894 bytes webapp/vendor/ort/ort-wasm-simd-threaded.mjs | 81 + webapp/vendor/ort/ort-wasm-simd-threaded.wasm | Bin 0 -> 11246032 bytes webapp/vendor/ort/ort.min.js | 1827 ++++++++++++ webapp/vendor/ort/ort.webgpu.min.js | 2644 +++++++++++++++++ 18 files changed, 4872 insertions(+), 28 deletions(-) create mode 100644 ml/common/vram.py create mode 100644 webapp/vendor/ort/VERSION create mode 100644 webapp/vendor/ort/ort-wasm-simd-threaded.jsep.mjs create mode 100644 webapp/vendor/ort/ort-wasm-simd-threaded.jsep.wasm create mode 100644 webapp/vendor/ort/ort-wasm-simd-threaded.mjs create mode 100644 webapp/vendor/ort/ort-wasm-simd-threaded.wasm create mode 100644 webapp/vendor/ort/ort.min.js create mode 100644 webapp/vendor/ort/ort.webgpu.min.js diff --git a/.gitignore b/.gitignore index 931d2cd..db06042 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,5 @@ webapp/models/*.meta.json # 从头重跑时旧产物挪到这里(不入库) ml/archive/ -# onnxruntime-web 本地副本(bash ml/prep/vendor_ort.sh 生成,约 50 MB,不入库) -webapp/vendor/ort/ +# onnxruntime-web 本地副本(约 33 MB):**入库**,离线/无 npm 时直接可用,不再依赖 CDN(2026-10-18 用户要求) +# 来源 onnxruntime-web@1.20.1 dist,六个文件 + VERSION;ml/prep/vendor_ort.sh 在缺失时仍可补齐 diff --git a/docs/ML运行手册审查.md b/docs/ML运行手册审查.md index 7ab7f84..2fdcf3d 100644 --- a/docs/ML运行手册审查.md +++ b/docs/ML运行手册审查.md @@ -3,7 +3,7 @@ 六轮历史(`0218d7f` → `a5e779c`):第一轮 13 条、第二轮可行性、第三~五轮处置、第六轮本机实跑。**已解决的条目按用户要求删去,完整原文在 git 历史里**。本文件只剩三部分:仍有效的实测数字 / 仍未解决的问题 / 已复核正常的项,供实现方下一轮处置。 环境:本机 CBB-003(RTX 3090、28 核、Python 3.12 + torch 2.10 + onnxruntime 1.22、Edge + onnxruntime-web 1.20.1)。 -当前结论:第 1–6 步已实测可跑;**第 7–13 步零证据**;R4 部署口径(纯推理)已判通过,`bash ml/pipeline.sh r4` 端到端 rc=0(2026-10-18 本机修复后复跑)。A 组原三处中两处已修,只剩离线部署 A-3。 +当前结论:第 1–6 步已实测可跑;**第 7–13 步零证据**;R4 部署口径(纯推理)已判通过,`bash ml/pipeline.sh r4` 端到端 rc=0(2026-10-18 本机修复后复跑)。A 组原三处中两处已修,A-3 由实现方第七轮改掉、待本机复跑验证。**第七轮本机复核与显存预算(19–20 GB)见第七节。** --- @@ -43,8 +43,8 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮 - `pipeline.sh` 7 处 `json.load(open(...))` 全部补 `encoding='utf-8'`(`:90 :110 :118 :127 :136 :138 :144`)。 - 附带发现并修掉:r4 分支末行 `[ "$r" = null ] && echo` 在 `r=pass` 时整体返回 1,`set -e` + EXIT trap 把**通过的 r4 记成 rc=1**(`all` 会因此停)。已改为 `if ... then ... fi`。 -**A-3 仍未修:`&ortLocal=1` + 默认 proxy 必然失败。** -`ik-policy-glue.js` 把 `ort.env.wasm.wasmPaths` 设为相对前缀 `vendor/ort/`,ORT 对 `.mjs` 走 `import()` 需要绝对说明符 → `no available backend found. ERR: [wasm] TypeError: Failed to resolve module specifier 'vendor/ort/ort-wasm-simd-threaded.mjs'`。README 明确支持的离线部署路径在默认配置下不可用。(`ortLocal=1` + `proxy=0` 未测;`wasmPaths` 与 proxy 无关,大概率同样失败。) +**A-3 离线 ort 加载:实现方第七轮已改(绝对 URL + `vendor_ort.sh`),本机待复跑验证。** +`ik-policy-glue.js` 把 `ort.env.wasm.wasmPaths` 设为相对前缀 `vendor/ort/`,ORT 对 `.mjs` 走 `import()` 需要绝对说明符 → `no available backend found. ERR: [wasm] TypeError: Failed to resolve module specifier 'vendor/ort/ort-wasm-simd-threaded.mjs'`。README 明确支持的离线部署路径在默认配置下不可用。(`ortLocal=1` + `proxy=0` 未测;`wasmPaths` 与 proxy 无关,大概率同样失败。)**复跑判据**:断网/CDN 不可达时 `bash ml/pipeline.sh r4` 仍能出数(探针默认走本地 `webapp/vendor/ort/`),且 `&ortLocal=1` 的查看器徽标显示「策略模式就绪」。 ### B. 待实现方判断(数据已摆在上面;分歧按第五轮第〇节规则,以实现方为准) @@ -60,12 +60,13 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮 2. **3 mm 间隙过滤的真实剔除比例(`gapDrop`)没测过**:只测过沙箱 30 条(剔 9/29,不具代表性);上表 p50 4.4 mm 是未过滤口径。生成后看 `stats.json` 的 `gapDrop`。 3. **R2 新判据(`label_std_norm`)的池无关性**:需在 1.7 M 样本上 `--pool 50000` 与 `--pool 200000` 两跑、差 <0.02 才成立;没做。默认 `R2_STRICT=0` 只警告,开着就是一道未校准的闸。 4. **R8 出题耗时**:沙箱 2 题 9 分钟没出完;本机未测(`policy_bench.json` 会记 `requested` / `genS`,不足额时看警告)。 -5. **Windows 侧生命周期全部未实测**:关控制台黑窗口训练是否继续(`CREATE_NO_WINDOW`)、`/proc/$$/winpid` + `taskkill /T` 能否结束整树、Git Bash 自带 `timeout` 能否把 TERM 送到 python / node。 -6. **断点续跑的沙箱自测未在本机复核**:gen 截断续写(「续写:已有 N 条」)、train STOP 存档(rc 143、`last.pt` 第 N 步)、apg `--resume`、prep 二次运行「待打包 0 个」。 +5. ~~**Windows 侧生命周期未实测**~~ → **已实测**(见 7.2):整树杀净 ✅、关黑窗口训练继续 ✅、`timeout` 的 TERM 送不到 python ❌(超时只能硬杀,靠 STOP 文件与定时存档)。 +6. ~~**断点续跑未在本机复核**~~ → **已实测**(见 7.2):gen 按条续写逐条一致、train STOP 2 s 存档续训、apg `--resume`、prep 二次运行「待打包 0 个」全部通过。 +7. **APG 的机时仍然零实测**(显存已量:`batch=48/plans=150` → 13.08 GiB)。`batch` 从 128 降到 48 后每步代价与收敛都要重算一遍才知道。 ### D. 小遗留 -`webapp/models/README.md` 过时:还写"由 `--fp16` 生成"、指向"第 8 步"(现为第 10 步、默认 fp32)。 +~~`webapp/models/README.md` 过时~~ → 实现方第七轮已改,本机复核过:现在写的是 `bash ml/pipeline.sh export`(默认 fp32)、第 10 步、新输入签名 `feat[1 或 K,83]` 与 `feat_dyn`,并说明离线要先跑 `ml/prep/vendor_ort.sh`。 --- @@ -81,7 +82,7 @@ tickGapMax 全部 ≈21.5 ms(20 ms 节拍没被卡住)。背景:第五轮 1. ~~A 组修复并重跑 `r4`~~:已完成(本机 2026-10-18,rc=0、P99 11.25)。 2. 接着 `FROM=gen TO=prep` 跑一次(数小时),拿 `gapDrop` 与 R2 两个口径的数——C-2 / C-3 随之消掉,B-2 的 Node 侧安全层数字可顺手从 m5 前的 bench 拿。 -3. B 组等上面的数字齐了再定;C-1 / C-5 / C-6 随第一次真跑逐项核对;A-3(离线 ort 加载)交给实现方。 +3. B 组等上面的数字齐了再定;C-1/C-7 随第一次真跑逐项核对;A-3 由实现方已改,本机复跑 `r4` 时顺带验(会弹有界面窗口)。 --- @@ -179,3 +180,106 @@ V1 通过后再跑,分三段,每段跑完把回传文件发我: | 后训练与评测 | `FROM=apg TO=accept bash ml/pipeline.sh all` | 约 10–20 小时 | `ml/runs/acceptance.json`、`policy_bench.json`、`r9_browser.json` | 任何一段中途出错:把出错信息发我,我修完后用 `FROM=<出错阶段>` 接着跑,前面的不重做。 + +--- + +## 七、本机复核第六/七轮处置(2026-10-18,含显存预算) + +**先说约束(用户定的,之前所有估算都按整卡 24 GB 写了)**:本机 Windows + 4K 桌面**空闲就占 4.4 GB**(`nvidia-smi` 空载 23604→4394 MiB 实测),所以 **ML 只能用 19–20 GB**。 + +### 7.1 APG 会直接把第 8 步打崩(实测,非推算) + +用 d=192(与正式训练同结构)、`--steps 1`(峰值在第一次反向就到位)、每进程硬帽 19.2 GB,逐档实测 `max_memory_allocated`(GiB): + +| batch | plans=60 | 90 | 120 | 150 | +|---|---|---|---|---| +| 32 | 6.14 | | | 9.53 | +| **48(已定为新值)** | 7.67 | | | **13.08** | +| 56 | | | 13.61 | | +| 64 | 10.81 | 15.41 | | 17.66 | +| **128(原值)** | **OOM** | | | **OOM**(崩前已分配 17.85 GiB) | + +- **原 `batch=128` 无论 `plans` 取 60 还是 150 都 OOM**(d=32 的小模型也一样,崩前 17.54 GiB)→ 显存是被展开图吃掉的,与模型大小、数据量无关。 +- **已把 `apg.json` 的 batch 定为 48,`plans 60→150` 不动**:最坏 13.08 GiB,加桌面 4.4 GB ≈ 17.5 GB,在 19–20 GB 预算内。没选"降 plans"是因为降展开长度会把长任务的绕出段截掉(就是口径-3 那个问题)。`batch=64 + plans=150` 要 17.66 GiB,加桌面就到 22 GB 了,不取。 +- **加了每进程显存硬帽**:`ml/common/vram.py`,`VRAM_FRAC` 默认 0.78 ≈ 18.7 GiB,训练与 APG 启动时打印上限。没帽时它会把整卡顶到 23.6 GB、连桌面一起挤掉(就是这次报显存的原因)。 +- **显存用量现在看得见**:`train.py` / `apg_finetune.py` 每条日志带 `vram`(峰值 GiB);监控页指标卡加「显存峰值」(>14 黄、>17 红),GPU 卡的显存条改成按 **20 GB 预算**算而不是整卡 24。实测:预训练 `batch=1024` 记到 `vram: 2.71`,APG 自检档 `vram: 0.33`。 +- 对照:预训练 `batch=1024` 峰值 **2.76 GiB**(够);`check_r2` 每样本常驻 **776 B** → 1.7 M 样本 **1.32 GB** + 分块 0.05(4× 池自检 0.2)≈ **1.6 GB**,脚本里"GPU 峰值 <0.5 GB"错了约 3 倍,但不会崩。 +- 顺带:N-6 的"按空闲显存 1/4 自动定块"、出题分块等估算都要按 19 GB 重算。 +- **`ml/configs/smoke.json` 原来没有 `apg` 段**,而 `selftest.sh` 调 APG 时不带 `--config` → 自检第 6 项实际按正式 `batch=128` 跑,在本机必然 OOM、自检永远跑不完。已补 `apg` 段(`batch=8`、实测 `vram` 0.33 GiB)并让自检带 `--config`。 + +### 7.2 生命周期与断点续跑:本机整轮自检 **7/8 通过**,用时 3.5 分钟 + +| 项 | 结果 | +|---|---| +| `gen_resume` | ✅ true(判据修正后)——「续写:已有 2/6 条,从第 3 条继续」,与一口气生成**逐条一致** | +| `prep_rerun` | ✅ true——二次运行「待打包 0 个」 | +| `train_stop` | ✅ true——`--stop-at 20` → rc 143、`last.pt` 第 20 步、续训到 40 步 rc 0 | +| `train_stopfile` | ✅ true——写 `ml/runs/STOP` 后 **2 s** 退出并存档(第 351 步)。控制台「停止」走的就是这条路 | +| `train_term` | ❌ **false(Windows 的真实限制)**——`timeout` 的 TERM 送不到原生 python,rc=124 是硬杀。结论:阶段超时不能指望体面存档,靠 STOP 文件 + 每 10 分钟定时存档 | +| `apg_resume` | ✅ true(自检档 `batch=8`,`vram` 0.33 GiB) | +| `tree_killed` | ✅ true(实测)——`taskkill /T /F` 把 bash→python+node 整树杀净,没留孤儿 | +| `survive_parent_exit` | ✅ true(实测)——启动器退出后子树仍活着 = **关掉控制台黑窗口不影响训练** | + +用时 3.5 分钟(APG 用自检档;上一轮 APG 走正式 `batch=128` 时是 13.2 分钟,而且 OOM)。 +之前测不到的原因:① `monitor.find_bash()` 在本机返回 `None`(Qoder 自带 git 在 `mingw64\bin\git.exe`,"上上级 + bin/bash.exe"推出一个不存在的路径);② `selftest_proc.alive()` 用 `text=True` 读 `tasklist`,中文 Windows 的表头是 GBK → 读线程 `UnicodeDecodeError`、`stdout` 变 `None` → `TypeError`,异常又被 `wait_pids` 的 `except Exception` 吞掉。 + +### 7.3 两条判据自身的问题(会让"通过/失败"失真) + +1. **`selftest.sh:27` 用 `cmp -s` 逐字节比生成结果**,但记录里含 `plan_ms` 与 `cert.ms` 两个**墙钟耗时字段** → 两次运行不可能相同,**这项永远是 false**。我在副本里重跑基线、只把这两个字段剔掉后逐行比:**6 条完全一致** → 续写逻辑正确,是判据错。 +2. **`rec()` 用 `json.loads` 解析传入值**,而 `str(None).lower()`=`none` 不是合法 JSON → 抛异常、整项**静默不进结果文件**,`all_pass` 照样算出来。所以"第七轮说自检已修完"的两项进程测试,实际上是没跑过。 +3. **自检副本只复制 `git ls-files` 里的文件**(`selftest.sh:16`)→ 任何**未跟踪的新文件**在副本里不存在,调用它的项会以 `ModuleNotFoundError` 变成 rc=1,看起来像功能坏了。本轮新加 `ml/common/vram.py` 就踩了:`train_stop`/`train_stopfile`/`apg_resume` 三项同时 rc=1,`git add` 之后才恢复。判据建议:自检开头先 `git ls-files --others --exclude-standard` 非空就报出来(或改用 `git ls-files -o --exclude-standard -t` 一起复制)。 + +### 7.4 本机已改的(都按实测落地,不改判据含义) + +| 位置 | 改动 | 验证 | +|---|---|---| +| `ml/monitor.py` `find_bash()` | 便携版 git 的 `mingw64` 布局要再上一层;再兜 `which('bash')`(排除 System32 的 WSL bash) | 返回 `...\.qoder\bin\git\bin\bash.exe`(bash 5.2.37);网页「开始训练」在本机的拦路石解除 | +| `ml/selftest_proc.py` `alive()` / `wait_pids()` | `tasklist` 用 `text=True, errors='replace'` 并防 `None`;起不来时把最后一次异常带进结果 | `tree_killed`、`survive_parent_exit` 两项实测 true | +| `ml/selftest.sh` | `rec` 解析失败记 false 不再丢项;`gen_resume` 判据剔除 `plan_ms`/`cert.ms`;APG 两项带 `--config` | 重跑第 1 项已变 **true**(「续写:已有 2/6 条 → 逐条一致」) | +| `ml/configs/apg.json` | `batch 128 → 48`,加 `_vram` 记录实测表 | 最坏 13.08 GiB(实测),在 19–20 GB 预算内 | +| `ml/configs/smoke.json` | 补 `apg` 段(`batch=8`) | 自检 APG 档 `vram: 0.33` | +| `ml/common/vram.py`(新)+ `train.py` + `apg/apg_finetune.py` | 每进程硬帽 `VRAM_FRAC=0.78`(启动打印上限);日志每条记录加 `vram` 峰值 | `显存上限:本进程 18.7 GiB(整卡 24.0 × 0.78)`;train `vram: 2.71` | +| `ml/monitor.py` + `ml/monitor.html` | 状态带 `vram_budget_gb`(默认 20,`VRAM_BUDGET_GB` 可改);GPU 显存条按预算算并超线变色;指标卡加「显存峰值」 | headless 加载渲染出的 DOM 里有「显存峰值 / 预算 19–20 GB(整卡 24,桌面占 4–5)」 | + +### 7.5 仍需实现方处理的(我没动) + +- **APG 的 `steps` 与机时**:`batch` 从 128 降到 48,每步看到的任务少 2.7 倍。`steps=3000` 要不要加、加多少是收敛判断,我只有显存实测、没有这个依据。`R2/R4/N-6` 各处按整卡写的显存估算文案也要按 19–20 GB 重写。 +- `R4 复跑`:`feat_dyn` 要重导模型,探针现在会**弹有界面窗口**跑 `viewer_ref`(30 题×≤10 s)与 WebGPU——本机跑之前请知悉会有窗口,别动它。 +- B-2 的几何查询搬 Worker(若本机 R9 仍不过);R9 用真模型才有意义。 + +--- + +## 八、R4 复跑(2026-10-18,`feat_dyn` 新模型 + 本地 ort + WebGPU) + +`bash ml/pipeline.sh r4` 端到端 **rc=0**。这次先把 `webapp/vendor/ort/`(onnxruntime-web 1.20.1,33 MB)**入库**(`.gitignore` 同步移除),不再依赖 npm/CDN——上一轮 `vendor_ort.sh` 的 `npm pack` 失败后探针静默退回 CDN,而 jsDelivr 本机完全不通,四种配置全变成「加载 ort 失败」,一条数字都没有。**这是个真问题**:下载失败时流程不该"看起来跑完了"。 + +### 8.1 R4 判定口径:通过 + +| 配置 | P50 | P95 | P99 | 备注 | +|---|---|---|---|---| +| **wasm·16 线程·proxy(部署口径)** | 7.06 | 8.88 | **12.67** | 本地 ort,`enc_once` + `feat_dyn` 新模型 | +| wasm·4 线程·proxy | 10.25 | 12.48 | 13.22 | | +| wasm·16 线程·无 proxy | 6.83 | 8.28 | 9.53 | | +| **WebGPU** | 18.26 | 22.76 | **27.09** | 首次拿到真数:`hasGpu:true`、adapter `nvidia ampere`、有界面窗口;比 wasm 慢,不用 | + +结论:纯推理 P99 12.67 < 15,**R4 通过**;与修复前(10.22–11.25)同量级,`feat_dyn` 没让主推理变慢(主推理仍只送 1 行)。 + +### 8.2 `viewer_ref`:随机模型的整周期数字(只作参考,但暴露了三件事) + +30 道贴柱题、随机权重、真查看器(有界面窗口)、16 线程 + proxy: + +| 指标 | 值 | 说明 | +|---|---|---| +| `planP50` / `planP99` | 68.5 / **131.2 ms** | R9 线是 45;随机策略几乎每次都认证多条候选 | +| `starve_rate` | **32.7%** | 执行时钟到点队列为空 = 实际丢步 | +| `inferP99` | 58.3 ms | 纯推理探针只有 12.7 → 差额是主线程排队 | +| `lookaheadP99` | **170 ms** | 前瞻(8 行特征逐行编码 + 8 次 gapMatrix) | +| `certFailWhy` | **time 948** / box 346 / cert 19 | 3902 次规划里 70% 的失败是"墙钟到点判不过" | +| `tickGapMax` | **736 ms** | 主线程仍会被一次性占住(安全层在主线程) | + +**三条判读(给实现方,不是结论)**: +1. 前瞻是现在最贵的一环(170 ms P99)。虽然已改成只在冷启动做,但 30 题就是 30 次冷启动,每次都落在 P99 上——R9 的 `planP99` 会被冷启动那几次主导。建议 `r4_probe --viewer` 同时报 `planP99`(含冷启动)与 `planP99Warm`(剔除每题第一次规划),否则这条线测的是"题数"而不是稳态性能。 +2. **`time` 截止占失败原因 70%**:45 ms 预算在随机模型上基本等于"到点判不过 → 刹停"(1019 次刹停 / 3902 次规划)。R9 若"通过",必须同时报 `certFailWhy.time` 占比与 `brakeRate`,否则可能是靠拒绝候选换来的。 +3. `tickGapMax 736 ms` 说明安全层(几何查询)仍在主线程;B-2 的"搬进 Worker"没有做,这条是真模型跑之前要盯的。 + +这些全是随机权重的数,R9 判据本来就规定用训练好的模型测,所以**不据此判 R9 过不过**;`r4.json` 里 `viewer_ref` 已标注"仅参考"。 diff --git a/ml/apg/apg_finetune.py b/ml/apg/apg_finetune.py index 421a3b8..ae20075 100644 --- a/ml/apg/apg_finetune.py +++ b/ml/apg/apg_finetune.py @@ -17,6 +17,7 @@ from ml.model.policy import build, wta_loss from ml.model.meanflow import meanflow_loss from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state +from ml.common import vram def load_pool(paths): @@ -38,6 +39,7 @@ def main(): cfg = json.load(open(a.config)); C = cfg['apg'] if a.steps: C['steps'] = a.steps dev = a.device; torch.manual_seed(a.seed); random.seed(a.seed) + vram.limit() os.makedirs(a.out, exist_ok=True) ck = torch.load(a.init, map_location=dev) mcfg = ck['cfg'] @@ -147,7 +149,7 @@ def main(): with torch.no_grad(): e, oe = goal_err(kin, q, T, R) rec = {'it': it, 'total': C['steps'], 't': time.time(), 'plans': n_plans, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(), - 'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60} + 'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60, 'vram': vram.peak_gib()} if it % 10 == 0: print(json.dumps(rec), flush=True) log.write(json.dumps(rec) + '\n'); log.flush() if saver.stop() or (a.stop_at and it + 1 >= a.stop_at): diff --git a/ml/common/vram.py b/ml/common/vram.py new file mode 100644 index 0000000..2009601 --- /dev/null +++ b/ml/common/vram.py @@ -0,0 +1,21 @@ +"""显存预算(2026-10-18 本机实测后加)。 +用户定的预算:这台 3090 只有 19–20 GB 能给训练用——Windows + 4K 桌面常驻就占掉 4–5 GB(空载 nvidia-smi 4.4 GB)。 + limit() 给本进程设硬帽(VRAM_FRAC,默认 0.78 ≈ 18.7 GiB)。超限时在自己的反向里早失败, + 而不是把桌面挤到没有显存(整机卡顿、别的应用崩),这也是 APG batch=128 原先 OOM 时把显存顶到 23.6 GB 的原因。 + peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对预算,不用再去猜。 +实测参考(d=192 同结构、1 步即到位):预训练 batch=1024 → 2.76;APG batch=48/plans=150 → 13.08、batch=128 → OOM。""" +import os +import torch + + +def limit(): + if not torch.cuda.is_available(): return False + f = float(os.environ.get('VRAM_FRAC', '0.78')) + tot = torch.cuda.get_device_properties(0).total_memory / 2**30 + torch.cuda.set_per_process_memory_fraction(f) + print('显存上限:本进程 %.1f GiB(整卡 %.1f GiB × %.2f,其余留给桌面)' % (f * tot, tot, f), flush=True) + return True + + +def peak_gib(): + return round(torch.cuda.max_memory_allocated() / 2**30, 2) if torch.cuda.is_available() else None diff --git a/ml/configs/apg.json b/ml/configs/apg.json index 209510d..474f3e3 100644 --- a/ml/configs/apg.json +++ b/ml/configs/apg.json @@ -1,7 +1,8 @@ { "apg": { - "batch": 128, "lr": 1e-4, "steps": 3000, "grad_clip": 1.0, "amp": "bf16", + "batch": 48, "lr": 1e-4, "steps": 3000, "grad_clip": 1.0, "amp": "bf16", "plans": 60, "plans_max": 150, "seg_plans": 10, + "_vram": "batch 由 128 降到 48:2026-10-18 本机实测(d=192 同结构、每进程 19.2 GB 硬帽)batch=128 无论 plans 取 60 还是 150 都 OOM(崩前已分配 17.85 GiB);batch=48/plans=150 峰值 13.08 GiB、plans=60 为 7.67 GiB,加 Windows+4K 桌面常驻 4.4 GB 仍在 19–20 GB 预算内。batch=64/plans=150 要 17.66 GiB(不取)。展开长度 plans 没动——降 plans 会把长任务的绕出段截掉。", "margin_train_mm": 3.0, "w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5, "cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6}, diff --git a/ml/configs/smoke.json b/ml/configs/smoke.json index 5cc09f2..7fea377 100644 --- a/ml/configs/smoke.json +++ b/ml/configs/smoke.json @@ -31,5 +31,13 @@ "log_every": 10, "patience_evals": 5 }, - "_note": "冒烟专用(bash ml/smoke.sh):只验证流程能跑通,数值无意义" + "apg": { + "batch": 8, "lr": 0.0001, "steps": 10, "grad_clip": 1.0, "amp": "bf16", + "plans": 20, "plans_max": 30, "seg_plans": 5, + "margin_train_mm": 3.0, + "w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5, + "cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6}, + "ckpt_min": 10, "eval_every": 1000, "eval_tasks": 4 + }, + "_note": "冒烟与自检专用(bash ml/pipeline.sh selftest / smoke):只验证流程能跑通,数值无意义。apg 段必须有:selftest.sh 第 6 项原先不带 --config,走的是正式 apg.json 的 batch=128,本机实测第一次反向就 OOM;改成正式值 48 也要约 13 GiB,自检不必吃这么多。" } \ No newline at end of file diff --git a/ml/monitor.html b/ml/monitor.html index 250221b..04f88bd 100644 --- a/ml/monitor.html +++ b/ml/monitor.html @@ -436,6 +436,7 @@ function renderKpis() { tile('展开内到达比例', pct(r.reach_frac), '误差 < 1 mm 且姿态 < 0.5°', '', last('reach_frac'), COLORS[2]); tile('闭环到达率', pct(e.success) + dlt('success'), `最好 ${pct(best)}`, '', evs('success'), COLORS[2]); tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]); + tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}GiB` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]); } else { const sps = r.samples_per_s; tile('总损失', fmt(r.loss, 4), `MF ${fmt(r.mf)} · FK ${fmt(r.fk_cm)} cm`, '', ema(last('loss'), 0.9), COLORS[0]); @@ -443,6 +444,7 @@ function renderKpis() { tile('闭环到达率', pct(e.success) + dlt('success'), evals.length ? `最好 ${pct(best)} · @${e.x.toLocaleString()} 步` : '尚未评估', '', evs('success'), COLORS[2]); tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层,部署另有护栏', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]); tile('梯度范数', fmt(r.gnorm), '裁剪阈值 1.0', r.gnorm > 5 ? 'warn' : '', ema(last('gnorm'), 0.9), COLORS[4]); + tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}GiB` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]); } $('kpis').innerHTML = tiles.map(t => t.html).join(''); [...$('kpis').querySelectorAll('canvas')].forEach((cv, i) => spark(cv, tiles[i].sp, tiles[i].color)); @@ -477,7 +479,7 @@ async function poll() { render(); } const P = lastStatus.pipeline, busy = (job && job.running) || (P && P.current); - if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').insertAdjacentHTML('beforebegin', `
`); } + if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').closest('.hero').insertAdjacentHTML('beforebegin', ``); } else if (busy) setPill('live', `运行中 · ${P && P.current ? SM[P.current]?.n || P.current : '启动中'}`); else if (cur && Date.now() / 1000 - lastMtime < 120) setPill('live', `运行中 · ${ago(lastMtime)}更新`); else setPill('', '空闲'); @@ -531,15 +533,16 @@ function renderStatus(s) { // GPU const G = s.gpu || []; + const BUD = (s.vram_budget_gb || 24) * 1024; // 显存预算(MB):桌面常驻占 4–5 GB,不能按整卡 24 GB 看 $('gputime').textContent = G.length ? '实时' : ''; $('gpu').innerHTML = G.length ? G.map(g => { const u = g.util ?? 0, C = 2 * Math.PI * 50, col = u > 85 ? css('--ok') : u > 50 ? css('--accent') : css('--warn'); - const memP = 100 * g.mem_used / g.mem_total, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6'); + const memP = 100 * g.mem_used / BUD, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6'); const row = (k, v, p, c) => `I@2nO)E5vFFn~9tse#n|6$d97^Egh{U3j`ez-zFp8GMgz)v42JRUHa{(7Ai5o=eeWl>5AWup`KvZr-hhliRa+4_j7bZ!7heQ>*^dR+S zSjQGrb7I&l0#@{^0ixh%z%fIyRTRqL@6@5;k{E^2VawzbN^S>-5?YfulZB%oVt+Hh zmS`OT3wsq(u$04sqTow|Fh5v+ gxx gfwj`VJpV)$k 8v0??ff72v0upGBXk1dea+qXjC{Am1c2kfe z5ya5HD!#yOACd;H5F{9TKh|>-9FuWG!7PhmVYr*XCIQb65wHtl=op?t3dXTUA$Z|s zaVEt10SYEJiUGiDLw4#@o{jKuGz@P6BgSzg1iK;mf>8A*DjbU7-f& Il@lc!Zhj+IsvcEli`rNVpDibzvIhOky)s_?MSKmU7l1&&avN^zT5g>5E8 zSuKXe(tf2>HlsxoqKJmF`8fq@lY=T0hiEVZcR_@H0Kh5H1mdb`rVNKl>0qT0Lli32 zcJW)BmLU3D&DF{s;!bgw_&puMQ!&Fhu?FMtOY5aG*1Ad^0{0RkNFl*c4osz`28U$a z&0sBKB5>S>8z#*|z2VKU`{8_#LDQ5eo6xpRI}ojZ+8V-x!lT2Zp(XV{U^M>@VSPa* zHq$g~)=W^h)HET#p^<{fmq1%s_|3%KV(1<)P=$uW{9r+t9UM|LINXz#rO=?a-=_Nx z%(xP_SAR ZhN(zL{|1@U^g4oTf41bZ!{#*GC`9%(;}{?(O@O&fJM!g zCS4@qL)*V7)d$6v?LX9@3~}jTge_5-fCUrYvMsoRWq1(IAE<5y-vUi46^AfC&EPSb zNa!vqObR+I9ub>0J4&a8=8ERf+%d5W&P*z_3o^wY7Zr(;J;)v!8icDVNKaBIPr$aI z7kC5QH)( fu##IvxU+Kp;Ylg;=@R&Hy24Ug^+>ZXA|078MhLK~G zP#7ldl|Y(%Es#ohAe+UUVc=x J{zYbJWUW{F}*<1tvl`^;j9r O^?vW>6%KR)U5$Ke#@HDpW{QC_V7;iWX5~ zRH*8wI#JW&CpbcE?xHqG-2|FAhZ{MS@Uco2Oimj&`#95LePD} _b zO3WB;>~Stv1(6TOdZinEI0zhJX=B7`PBaSHIP!`(#%Ul9Jq%H+3shmjQi!OIg`o;e z5e$!tqb>9+s8cZ5;}ndGWvClrTCR@MPx(6N3++B@_s-UR78a9(>RNwwVTdw&sHP zPI%BotQcB{Dn%aYkefZp1K*9knx4QZa7xE+Mu8skP{44p3*pKE%p%MYMLdAvWK@Z5 z1#?JV9Lokv3C0IDN<5oGfT{4Uigr%!=XoaY<=DD4bmpf3TSb5c<4Rk%jHgD>fhcMT zApYC|K%sPs!~_CKL;BwY3HQX9e3-5g%rQ>vQb9Of %_j`vv@%^z)WxDvMjL*5WY-hqq_RAXC2lm#dGxg7om#G-+XuU9&0`^1!a0gtu& zI;w%K%>36^SzTOZxcm>19xykKwEq3<8lV7*GMCq*)U#wX8vUs6%R@M1vqj!6o=q zMs`7wEV Qw1zM_h@2^>CTB;63k#Qx3|xW0CP@}e7qS{8ER?tDRK)w0 zJC~I2`X}%mQEDbi38M5a85@WZ{{&j0r6_geb$~HGQ0NFv3?TlW!kzC(;Z2$}`BO9L zXH7zrmN>~lY#cpg^Wf%4Ck>xP5kPlca<`lY1uSi@BYD7@a9Sp4DeKR71z#B@D~0%Q z#rlgp#{r1z2aIGz;S2$PA>#mYJAt3@Hwpl#3%~&U1!_rRkT&2Ds0h&r@LQ$a&Nm>w zG>8_lA@f1Mfr4$lLgsHK=dWD@Cg%g-qm^)nd~E)PaurJdI~n}V i}^8?zNw6$lS89c4bC>GOS@*E7Sr O z0X%^MmP>1dTQG@3CWxP$1wh^iXe>>+K0pFuf5+6BS_q@&@-INk5wyXQA*0eiR?7Lb z;0m-{q7lnHsWTMJNP!<5fXo6Xm_SF3lowzPDvEIQBsMkj#V`pElIVROovC|ad-I&EIU9hB;FwKTW<2XjY2@sMuYH;o$DbmCO vQTwzT=nhYi{UgDUxJ_f=bl~Ib0(1d^l^VS3Km?8+pUacV zue|q0Wx0;b08%#?4PRv>Bcb`2sSijH;6`;gCBOyZ6*LtDdVH^;3y7%mxHz=Ijj{$P zGJIUn@&7LwZslZd=#>^tqo03SaTE&JcpKG{;;5 c(gDp| ^)*6&^g}&wKo6VSv}uDY4N0!VYehVe#0`=t{m~cxAWER(;mwACO0@U{Nx)M6 zSMX>KCjK_a|DT5NPyfV)@?YehaChSe{%^UzA?5!!+Slt}Z_63*OLG|%gyxND8erli z_t(&dc44^(>}edNlPaC?e-*GT_( +R%b@Di`l z2l(OnQiH6G1wc a z*lHB8z Gjb7{`D{F*Qf@Jd?e-Nu!goF0RkE4(}2JK;HZEA@-Z0X zzJM{|gA+b7`FT@dkTTMD_T|(vRiLS#1?7BFd~A3bp8@&U%0(*iFIxypyCrAVP{%h= zA2v`$)vpAuw^uL?1=P{th_PS2fM6Z;4TGHHfvg{*hZeHXTv8t!pA=MTWETw}4T=X6 z(a+PL2YT@%_@h %&TFHgRwT&u$XjwE!OZzA_r`|zE85RJpKEVjahfTf; zOu=h)0cw9M*%n`?^0g;4-IPkL{Xg^*h(HpWA3J~Tr(_I#6n7l-MjfsPFn2rjQ4cvB zbl|(dyW_`Q3?1L_(3n2fq9^hw@d~Qr^=QYB)nF$C<6}sAGeT{NA f8zoBn(q&RwvjTcIIodhZARWAmN zr@;6y4WBIF>)NO&RM7Kger87BAb}|K{)b)yAz`$5zU_OsEZ}Pc42o`xsgmAg(AWaM z5LHv5DV7S2F9mNrNH=r<5B%`@i9Q)a_jpXqpRJ=RS_lcj=f(K=FVy#{3$Lc=6$`#E zqQg7|C{_+==Lh{SI8gpA9HARizg7xxX$(Nic)m|6qz^if)FA?X6fgz!h#Rk_@aBPF zdZ;c&1ET^CpBMl|m3XBm*M-Q8`BQQEBEQFj;`z(EprEjxB7V`)^odk-bW c%K$dpHC|BIVHV<2@8hl zQZ<<>OcqbGQU$zhq;DAsAu6-!>ngr6&=&`R2p|B<*hWW*Mj%%?caL 0`mb6@}eJ3cb;|GJ4!JQ^8-ocV|OYEHGk3UD)!k!%G{ z1oubu4jPJ~dBryXDkZ-D3_+wEUxMQ0GlvNkf;VTuA)4S2NrN}~$nm*_TqGFu^tnYa z!DtKsC|=@y1qX+rIenDPzk%a#Ht^yDo*9zh%`v)%3?V`EN}8N4nGxQkA;~|y^@q)H zB7E(HuL|j844(@DDfr$Fa=785QiTQH@HSw73&OiXr4&YeKyEqEXLESXhYx5V2EFN{ zfREhhot)poQ4bt_T81~y`2HQAlaQqoj#y!fz%K<~$U(s7Xrxx)k04?QeOu-ejP?S$ zheM0z;13l^;j~YnTK?>k)(OGj+ngX;eF#uoqmqo19U&*dM||9U)BP{;rz^cIMl<@p z4LdY3z_y{*V5_7*4$$`%G(r-Zv!)9v8C94MDk$(-4$l;d$ar2&?gN_yjU|#eQW7JH zF))}MLSgIzy`ohl#w#=MFN_W!6BABBJU|I)RPp$)0cf!oz(v4uP>t&2kCjqak^#2V zBwPp&PwcD-ry%hwF7#;v8Y%I5Lw+LzYa-K@FAcDa30!$apcDXn4Fl!h1pIA@xFH5e z{nfngw=QS{3&xMmBLR?8Qd?r_FZc+! EKiC?%WQ19SG8U5(!>W2YRZ}bbGNb~Y9fNh{BhWy95 z$OT#dfhPXe9N}Zeaj#ZjH^(yPKOFM{V&KzWG;D|g6}-a2Cy)>4ui`!W$0M`wapE;3 zc*Ect2JE9DE`j06<|jAf?8)Zk;_E}A8x3?yg9un+gZw_(IKn?0D9HT%GU7%UUY1{w z@!Mi@u+*X4kalcTz!6~k#{L2iO9gU>IGgZW5nSn8ABcbp-4*eG&wWDi$x(q=IgEbl z)X-nVPtOc^C{Zr_f4c;2ls=X+dI efT3OqJg1AslKi^~e`Sy(i`83%B2s{yATG`ib;KzA&h>^ku# oRhEw`SzEI_m{xx5?V3+ 0khHYH+-IMTj2ajIpEWtQp5=g)NA`eY2r7@ILE zqbz1ZTCKKTqByscod5pydh$-qL8R44B}ikC`XF6Q+Ns%&RP@QSmUljW)^cx)rHOl6 z9Q|q^TlvS8e|+lgw!vH1dcF5->vi5Ut=D=_w_f?jvY2|Q+j*%pd -om$4bkhP*G0dcuZ><4y*heT^y~SI_~Q6!@l)fc#7~Z&6hAS3 zLj3sn-@=WJ9}_=1epLL(_y%Z1{P1{6!{UeXG$ej-Jf%VLMLZ2_D#row{o^V1i|@ A?GxWSez$W)bT5>LHeDWF7{4sKXZ(uj712wai(kVoj$RzSFnR$` z&*GlO5ls2~=y}nU9>+c6Fy%v<4sJTAX;IUGP5) AZpEI8e*=^os-f7-pt_xXdUSVEtUS_@!az5l-NL`3O zKO8Z}JlZ_UJkoq9;<4$G>7nU?siE#r#P7koO*>87P1U9<)BcFXrbVVw(?Zj}h}otR z(@fJ0;PyrM)82?NrqQNRrje$-5ft}C3^4UK^)vM`?E$ ;}hd!<0IqgkUHa8 z;~C>=V{OO|<91_}ahvf}$Wr4H<6`3? ^w-#^J_c#$6Fl437s2@8#E{u0>smx*YX-UKvplQ65ni z@q78DsQRdjQ5T}lN1coMHGCd5=c4MO&P1J#s*S3NI{6!L4QftC-O}CE-OydvUDI9F zUC~|EUDDO-F6u7m&g;(U>U3vyXLP4^wYpQf8r@0V3EgqsYTYW`O5F 8sd)N#_@Q@t}uTr3-ZV15pt}^ zSQbHXT a=IIXSAoa(_^N^OpTcmb5eUkdtAFb`k3~p_K0>; z%t7q|?SAdD=zZF~+CAEdF*~(8wA-~e!m71Z+HKnLG3DAa?N;s3rdza|wVSl#V%BTd zYS(C2YgcJkYFB8B!EsW|#F+6h<6;(QSBI?%TN$<@?EjW84_g*CFlIo^(y;z9{bKsY z^oi*m(<_FsOT!vUR3^+)T$hIph#BakUlO)BtT3i$%*$5KTRm;{xYeUp4_gtg0j3hg zC1E{d3OQX-gdFd+y4UJS>fKhCT2Z=)cn crHssiRVdrw&Pd) 2P z*^yF}QjxN@)!J5@QqKK>;_Q^-l#waRTP<((H2Hq=&E%`emy$0epG~e!KAC(p`B3t{ z KI;8Zua9IKd-37E5BGdn@!{GJ7kxPO!^s~G{;= }M*HIS)7wvLfANEJADsT6_Je63O#Wcv2jf2&`@!%JdVg^H z{TuIJeZTJg!|(5Wzw-U^_cy zuHET&``hhpx4T_cyNY(3+AWJ;-EL{Sx$R2Y&1knQVsg8YQ4gBkZ{| 8C6GkKqPq_7Fi8wQOM)1ahx}Y@$s|r>YtSDGk zFuP#xyR+Xt^YyHPnSf>#6c>k-!>u0>pfxEgU4;!4Dq-B+|((dKFQ$K4-xzu*0C z_uJiXcE8^JYWK_C>$@*&v#iaz?q|CXe1l?5_Y>U*yg_kko26|IcE1!U$Nq0n?8jr@ zHz@Yzu|86c7bE4^>kW#F+AL~Q_y)z&Hl=O+ez+`Xd$>RLe1qbJNI6!A`{TB7e>@*4 z#}~~ho{N-YU8Ee(M#}L_q#RF2%JF$~iVNB-Xfq#iUYj{Q&2CfDhSIDyGkB_rJQ3MA z6}Oqz=6K|>$i|6qM z1w?yY7pW T& zVwWPRP`c2iM0HYCs~jW^!mZu;F11R+o&&Z>@?-0OJ=>*FI@9Ge+SGQDX;1lRYdD9& zDj9pS%V6*;lumFC$9=TNKzp>y{w}+CpF6u8Z+=o$qbQFYBpqwM11+e}?Z8e@`?1wN zY}G5+Z9Z&emkMx_`z-HL)}=;4eQxctrAw*iXmgTebC*qBHuBy!bXkwQMnTxJ$aSEl z-qv @kzb9%c=n1vdEP_+7++RE(h(rF3<8Ns0*d1c~A2Eyz8aMc{K{c zKFTXmm1<<(_0q$<1#g#X3Z(~m_d)B|)4e=-)OSI9Qsu|q0hVZQ16w5B%9C|^6MSiW zH#pYM_j=y7Ji=b(Sb5a-(iLzZ?B!Rmmw+Y7>N)nJkM9NWC0(3 Q(Cs|G2NnlTKzVqHb4lMO{3>+q?kNSE$l6M$1h0>wC0|5?Yk^6mm*q65# zSXmEck$Zf-?S4guUB0pHd
Hyz*smh1lmRFhAFc%e=i}Jj=TyJwYm)Vf6 zJhEOY$*WZ^Z@#Sg(&n@BX6DJzjJyeN7w3(AdwSk9A7*Oa_`Gp>WAn!3jmR6CHzaRx z-k`jqyxw`e@@9t*%)9i~fV}#*`sek_>znuT^Jkww{e0c$BR}gl__MuT4*t_JagU|K zQufzrf1CKtjISQE2kbW6pSU}5Z{nWBU5PsrcO-64tWK;-+?H68Se{sxxHWN0;w10O z=;zTry=T}_wx8`~)ocq}&6cpmY 3Fi`CBs@=emhd#8ubZCp9M9R8vn^*y!lHzIiQ~OX6ILWF2j}St zV_Qs1n3}LDXMRq ZP0Z-TAlL+-g&oa5mw*eOtj{`(AsceVzSIo2vY6`7__>w%EQlVNJqKTVdyy-oxJA z-kshZ-tFEs-WA^23DdkK39}N;+nyvGx9zuWv#qx+u}$bYzU#QIb8Ta6mH8F<<@qz- z=(a0=P2%jXC0%EAo!NCp*TwnMyVk>4%ksD8Z^_@B->o=f- KbpBKvl|;64e)$sjURlPdEXDtwG>)zn6aYGC(L`z8_d(pPfa^a6HLQQ zeNDHG7mYQ>YUAyw<;KCrDe4=B^M;)fdkyOh^9&{8c;zG|*bX)HMS*qec12l;;UCs@ zL)fp|8@63{Mfo)5Sxn8R`#+ucY4N8AHP2$^r4LH4R^Cs$mi91cT*R8RdgZ*dVh;C9 zt5V+8p4Sq7owiW1R6C!;LsG^o9|q0U&eEcj9UpA_pc~utLA7#pM744h(nzEcNW+na zAq_ &&!e*@qB*rG@g%7 z9>()Q$&ZtW?pD$%o*zislvD*7s?bjr`maL&Rp`44GE_mnD)e0iIjYcq738dfoK=vc z3UX9Ijw;Ae1v#o9M-|3}@*ma(^=&o$lm4Gj{_Nw69~UYfwJcOTM0$WU808ztuOnSU zx{7oK=@Qaeq%%mTk!q1@kWL^SMB0V46KOlrgAWT8^+<=1%8?c$O+p%obiY07kPaeM zAT33jk2Di$B2p33gAWQ7bx5a>j(&hC9wa^LR4B3Yomf>Tw!9M?+=<=n$PRX7D?75{ zj%;v8c0ZL}PGyHv+16AxKb4J7WzRaWQytij4y?4pbaDFogWfNE|FKvkt^CWb_Z~GX zSH5g_MR_Ulk+|Xe`QK0b{tz~>(cjmG1OMH<@6IW&es}b{z2BX|=DF>=!*7%;SAIAD zyMym6{p+;9j{fU`cZ#Hk-|l^ ^@x!?v%9SU6IP$}d(CvTP{HN7_dX;uv zv*iWz3Q7uI t$b;E9ZPY`RkRMvTqiBQ}WH>@FHo#H#?y9zTZ6gde>K5zd9Jc>Z`e56@NAU z%lTgxz0>=hL0|OzqDVRxTKVP5FU!7o^u_frj(@THiwFN)D8a6Vd|CX(>Mv%zGx43V z?=1Y{Xz184fL!zW%Fny8C7+jwMO_C64-KBBV*5U?2CD4y`mWo%ZtA+QYjM{xUHf+( z8r-Yv=&qx>4)0nSIl9Zn=Cyf~yUc%kbKZ(Pa4G$K4w@8$3!DF0A;6N)0B-nf-DluU z;O@`L0dD?`o&M|?fTN$W>z};@aOtzlO?!6U)w!(mn$D%2XLg>{`H}ab_ks7m_n!Bz z_m20r_m=mjmrisyyw|;8bjn-fEeZ#4&3o0`ja?z!Id2`nvtD-5djY_CFT3o$1fbr_ z&UjA)s3ocw-sb?Hc?V;RHBEsy;yvU&=sn=w@7?F!>)qpJyS%htZ1!#f-9|4f^KJ#O z#mg$a6#&Y;tlC=zV4HWuSN* qY|8Tb4%a(cBeD6H(0`Fq) zBI &x*E`2M+gswD<(=uB;T`5>6TK6> D!QjVxv@yF$VeD>9N zWlbmwFW7U2ak7W(5u4X~UTdK4vODY+yUA{_`|KXO&aSbm> MxR6Qqv|>^%6LV;9-D)_32kXJcERZe7> 3t8o=(zx*^B#8Da|u8_F+1m}19;YR z#&g zUE|l!w)LP6Bl}{)Fea=a}cH=ZNR9=k8ng!wz~5d3Ht|jy~Yo@3|kg&$HLF z$3uPZ_Ur=vPR~3gtMY6ESm|LqJlg?OQx6rMa)1Y;aUx=YwAr%>$i4Bb%(E5177ttJ zSu0~UdNu%9?^)$p=~?bs=2_|~l&V`S!F92RPB@F2ZjV14eK@)ht8#n%qNYVsRSP=z zoC>Bpl|`OX&!VPdo6hx|&OM%cFn3Sx_S{9eb8~0rPRJdVJ2 vZDxl7%1-4oqo+{4{P?q@mob8h6+=bXtoma{)+d(PIJ zwK*?b4_wz==UvBL2VB*zt*$va({m=|jLaF7v)c9C`M`P2dBJ(odCYm(dBC~UxyZT4 zxx;zacFT6zR%aXS9OiuNxa&COIO;gy*zKrxJhb1l*VvBP4%-gc_SkmXs%?j|tFl*S z_s#B={W9xu){yLh*;BG6Wev$1l=UL>QRbb@YnhibFJ{(eF3X&mIU{p==77xpnf)>! zSo>R_TY6a^S?VpPEOnM+mTi{xmMxZ5mRXhwmT8tzmR^?C=F8@N=B1`_rn#mf({tkr z<9XvQ<66UF!wSQE!+66u!x+O@!yrRHLr=p~{T2NQ{Q-TMzD~bGzeqn-KUUvYKU+Uk ze^577H%vD`*Gu;z<6g$?j58T0GLB~K&)AVsnXx%zL&lnnr5Ot{mSoS(n2~Yw(> 6Pi5(pRT1w9QT*pFTAGW!kf}ducZu*B#d!7ac{; zfzAQWUd}sdH`A`CT}eBbwlD30ZFky^w5qg ?(;M4w}_WN|9c0$&;tfiUrY%{gR+G*M;+DGmyovwAd)M;495gq$??Ah@}>a*0R zsSi@Gq?Tpe$*4&^o_Zv8U+VVMDV{B N@=z z{c8P6{aXDh{d3(D-DBND-BaBoU7z$7*?qDLv-@Wc%3hZ-DeY$F^R%A22N_#4R%R^9 zD9M XOXaX=5^ {bjz}Gpx+!~O_VEt4?N{t| z_R|@2JIv@%++j@CsH_oL!?Jd!Ri{;^m8GprTb8ytZA04Hw8d!)( q $>Zt z>x65qYmKWccWdqw_ha`>=XK{PXN~ivb4$jGj9D3_8Kv3tGVW)r>996qW5$TInhrNJ zE~V9{T}->8y{NsUt=C@G&hAi~ano|$a>H`Xve&Z9vfHxLQfgUXS!kJWDY6W(47BvO zoH3m;oi^2)YD_0hlTCw7lT0s-7Yt_&=L~g*GltWK>4vF>X@)6=JNn!DTl$;&;f7&` z!G$yC{22>gv>0sVh^r 9jRwhhNqqEP^w+5U8G&6U7}sCz3VQuO-y@ge`0@Ze`Fu)80{G4 z80n}^DNEUzvL~f7Wpm1gl=Ug=QbuV sVI zbfeQ6$7;ta$4bWv$8yKDjN$2p=`|VSI*#u+w&R$NqdSi3cras_cBpoUcCdDQ+U|@e zX?skEO#4j-O$SW-OnXfS495(I4Mz<}42KK{4O>! Em$oaTD&u r(vC*cf8v1XveaSr5(pMy_S4A`C@Wi z^6BK7+R_UpwPqmdHMqA<6#3m0|4~*u)&@|0E#?pm}e+}As#l; zGXlVH4;$kd4PcaqjrWWLFxJB+c_spw;GzCb<(|(yms^*Me( z*qTt zsLZWEO*yYwm%9Nq>wPtAbJw6|HLqEgy8<=KeKkvSm!M`buUVM;IQLQRL*C1R-1)%I z ckI21) zo^Nyfu-u`*58?Phx%Ys-%kf3I1A!mF@qKdp0pFM7d*}87zL4WzxX%KAhU1^RpSioS zr|xUEtF|k)i?$24bGFmATH7hxov_ ptT??XGqA6KmWj-6!0~0qu3~aqo8T0<^=u z-Cgaj0#xa)aF@Hw0Bvz^c5iZT1hm{eJH8}-7Pu^RFL5t+F9L3%dx3ktdmf-U?%D1V z_bfmY+~eKj++zWac8_w8bdLZu)IG#K*gXi)K=%N5e|JAOS Bb)95dKciJ>c(h{Oz1uz~AKft2x(!zsB)baxMdZiQ~`bTm=3?j%=Ifa_UfXme-uh zIgOfH-l8VwB=9FV{&3Dw;E(uP9LhO}nghIMcg|ka?D5s?%GrsU9lWM8ry4a?zM6`h za@3UZnvFSIP_x-rvms|aYS!_ZRXJ-=v)Wg)GG_&9mh+lfIkQny!X=xTGXwZyj-Qe< z4fv@XKRIU-@Dn+HY|eP#$8r3aoYBCK;`m`XBY+>y@k4Wl06&=H2jmn1Kak`5=kx`> z563@oJ$D^fJav)IAG;p8y0M2Y()nH2ec mu+MIR32b z9Po7 KxVd3ddh^)&qZ$ juf;k#j&ua+kkh|*_B;J+XD9HBkw!U3I!8E% z0~+ca;vDQ81gM|0ud|P{H=sgiPv=X=3r9ckk>jD`f#W`)JC56qTaKH6t~f3`E;;G} zU2vRtoO9Fxs&SljoNycmbi{GkamaBH(0<20$6m)CK)W0}9XlM`0c~?sIw~CHfR;EG zI~F-g0WEOMcg%Cl0aW6c<(TQ10cg5os$+^{GN6f$36AlOae#(9hB<~hh5#yZ40H@| z^as?}(Z|u-Q3&Xz{e}Ix{h7U=cpvF5(hd7{`!)MjK$q )Y+iUHo0G+U3 zjk^+eIS#(xekra#?joRr_6u?6 BYminVtw35}>*MO}Ds=UC^>g)g6}bkw2Dpa0 z2D=8iM!1H%hPg(&M!811#<|A2#<(WACb}lLrn;uMCcBDV(_PbCC9YYn8LqjmIj-5R z1+Mw7d9KB-MXrUeWv->JC9ajO6|Uv33C{7(an8xkNzRGRY0jz6DbAVBV&`<{Y-fpc zmUEtSu5*rap>u(AzH^mxrE`UIt#gfYwR3}Wy>p#&vvZSkqqEGp)w#u4>8x;;JFA^l z&TY;c+1InLW#7)em3=e&UiRJWJJ}DjA7tOpev }T0evkPr6vtMNQvGum~ zvh}z1v-PzV*#_DM*oN2!+XmT&+lJYO+D6$%+D6#M*~Z#N+a}s3*v8wY*e2U1*{0j3 z*{0fN+Gf~_ZLHpX5nJK~H@o7#4B(QRU3XstaMjIjx^DoejAmD|FK44Fo7HDu1W*#a zDQ;ujxooVgY=TY$I)-#Ods}uzc6oMnd=;LuWN!uOnmD#SyIKa;Wv?XwgsZa`XD`ZL z3TQCW0HjA*{lt4ncaTmZoj|HW+J;n#RDo2Ev?PnwW}V7{FU(?Rv(5lGoyE>)odZyp z#p<&z0=SUHu4G*Xa4Cyj&$ >a8m&wj%p254j)0r1F7c|HD^#s3`X0q9tC8(N}$>wFw1u!R*EzDd1U_K`} z$* t`P#l=ahGq@{FgTNq$Q%w}SSDMU zxdOoQOtvO-HGox_Y<=cB0BbYZrp%21He|A`nOgvC&SVvt aJhZR}mis8&L*b%@U9g--;T#HQE$octGzzsS)L7U_%Lx>Yqj1#1j#v((a0rD; z3oEyjp|BN&%@(%FvJr(1D6F%vwU#v~tVUs_g{`nGM`0NXGc9a}r5J_jC`_@i$(Bhd zOhjS4g^ja}MPW1wBQ0!%WjG4MQ0Qr4*UVSVc;0Vjx6C&ITsO13<~smxn{QaJTd!HK zS`P&8558i>K7HSO4;80l0X{N61o*(ro|>Nkcx*0}E}7Xu^8pn0o7oZbVE~8B?6~ {;I+!kW}0W9P;6$i%_RV4 znb|z^TmW;-YpkoStE?+IuNA~=fq6bEP7<$0=2CzQ&1|W834q1mHP*~tnqHW?vF9e% z%UlR-Pc!Rl?gOB=d8m1ad9Znqne=rx3Jm(2`=R0(@hUP81USIVMwo{K7zSQXOzfPg z4!q8q*hSL?0Ow8Yvgs0ldea@#ZPP8&O;do^Rnrwz9E38>Z_3ubJ3=(>(xpO{LN< z6I)?gj>0k%TWwkeV5NzzGpz-%##C-9Gi^0(Y2dZNv>p|l*JjfufE!J0o2e2&1$Zqn zu?432D9l4)j)@hUrlT+og|Q|!!ZaL(p(qSAvEHU$C={ab%*ZYo>y7Y=Mt0SB1;Aw^ zyJ5Tz;F^)$GTsDmI+oot-UV>S$Q~LW0Jv`~mChO2UgI7Vb{p9N<9-19jO?)S5P*Y5 zcGP$TKy55LVLT4tn32^QPXRcImOG7XwQ&^+D~)WOaV>x~Mz+zo0l<1A+ictfpeB}; z8Mgx1Vq}%Z3IOG3xy;C>8K dzF2m}a2dcQ1G{dx2H>iJ-7?$+a4>@1HQWJk+rS KGV2lecj{wRPW z!0y+x-TGZ9?9{V;`n>@50J~k!D)kj8l y16!hJ#ro+eOw+Sj`k4S`06Rs`Cg{hbFiy`V>n8!22<#X=8=)VL z!Z1A>tsezoB(OvDY@mJs3jOtLuznDLB4GRISx@~-9h|?8_15 8m#tmmwC*0a_#)+-U0BTie{Me7Bi&s*7L>m>m7R(9KZ3&2e)+iKkcV6&A~Sjz#F zS+`rOtyR`-ylJJC?X>Oy8eiV*vF--2%gT;e4+A)4WhK^G0A^a*T k0|5-MvLV*N00vn{T1QxiTZi$cL#=F# zbu`eUtZck>9DuP_Hr+Z6z*K8>>;c_A-Co@u9h>$4()Q+YQ6<^FFC?RaifC6^V%15( zD$JC56oiu0Pj%n(y8ZOI@1A>BXJ$dp2_%!%r$2qp`{UgLWR^ie1Y~dkR6s!yaX_3$ zR8T-f5gc(qR0I@6k@t(eL9OAO?mN7uWAASaJ7VpK6)RT6Mrvg#`eED3hOdfO7OgB? zdGISLSecKcyp^ mvka2efJzUWXc(8(LT_?(PAY5Eg@>TIyMPDTuA4RNT z#eo&;zN$x{b*r-A3udmOoK@L?^{Z&(s$9T^RaCGlACR|-idGc@Hm#z~t4aXHt7zM* zt$;17sC-o!pmY`OT(twReHB%$ssvQ5Vy*8%Ta}y~+gH@g>a FkE}{4pamb2YK}jnT0c%!J z>WUOV@(NZhrLS0vNE#vuU$ORLzoK5r13Y?PLXRbn01qYfRMH1{BBAG!XMlbQwMwo4 zS|oH$(hg{oP^aWNphH4klAC}V61pwv0dz~~p5!jzj)c|!=F3awySVo`E;%MSDruDL zwcKNwJ&jIDPNL8W37wUk0i2f51<859ISE~oTm&>pXt!h+ph`k}BsG9)3GI{A0`^L% zUa}uhC!s@ k7^bt@-ZEs+$XK#_#DNwxqsOQ>8@ z1}K%#PRS0yb_u0R)&kNblqJaoWJoAmvL3KbLb;L+fE)?sOY#64B~&Qc1SpUUONJza zl9!SJ$*AOwWJK~>@=B6qoFJh%Nh}KNm`iIU@qnFklZ@X5(VL*rAehRz^fqV=upOzd zf@m=4B_gGBX*g&IP=?g!LG(1J50R~N=~+-eU>i~&1<`|``-q_4he5pnR_~o4>JGYv z2 Vwjh;9f<2}%x13QA;!(}L2Glp2&| z{C;_o@!RF2%SV>K1`QxQM|g_x1mO-sH^NPX>j>=#tq9Esmk=%>oJBZ=a2%l#;V?oy z!ajsO2)mb8F5kI)$MT&)J0MNXmqp7 C4wH-_0Hx`S$g<=-Mx7?Ateh(Qj$|+joGs z-%{-I3BdbrDSmkzszI5= <)AQa)5G=vm{B!mQnc!XGl z_kmDTgfWB>gjWbXfz%v$IS|W6AYBc-0%!@O_P{nkYam?@>;PN~q?>^^0G)WCDUgl? zHXw31kd6f&1vCcI$-onU OiUstOe{1qyvHVfc+W6dO1ZfIbhPHG%O+jSHmIz!X4oAY}xu1*D 0705-t^bTi-vBAo%$9dHZK6+m|aZUcG(=zhRGz}*0P7|;uN5I|1? z9s?fX$&LUz6L1=lQvq~7;2hv=09_1d0$d27=77t9O96B>;0mB6fUX6!16uLqi2&LY zP=iQy0PPE?1?&x=0|E7bx&S&Ha0qZPfEoji02%`5c)&5hQ9QXTfC>XPAyN=PB>}~N zq5#?!um!L=fXV~P0Hp!6Ghhc`djM4hR01jjC@C-z9g`49uL6dVGK7~6yoHpmK)Mik z9&k1=DWKXqF(4seO+cK0{mOi`_4u+h!JB2H%SM(BE*M@mv}|zM%VqRzS^qM4t;=X& z*$cq)Wsep-UiN6&!)3k8= g@9xS^LxVH=kEeM?m?aQcbSt}B+BGR&qnwMQh z HNh;7_mohXF(WH0nPBc &lR z9B|B^PWzt%ob;!2{$~Ma{OO|q1;BZKYWBYjxa3b){jUI8{Hfi)4bbXO*Zn&H*Zg7P z{ON#yJtF)4sm^~Npw^$N{3`(!{#4^%4cLWpWeA)7i~I}yH~AO%!+gE<+lZ_&Kj4Jl zdp}HxnZS7eIHbn10*U?!z%>k${gZ%PfmHvLDO~HHHihGU*}!*x4`=qy9ObUBx$7`@ z9rQ~w?niip(2H>2kGlMBqRAV6)Z^C;xP{bfestCE3L-6j)b7^?XhrHJgbN5~5l-Qk zvoTLVjed0tdS)K-JLpH1eibOX(~qkCb^~_#(O$njfEqul^V {sGf>{rCa3X#9wFU>F2FU2pJOG)xu=a=P|>6gyM z*81I=(={i-k7E5MmSXUi(i*>b)E?*eX6fkCk)^P*ONW;ZE`7O_m3xkOAHpMq`v`Xs zx)E+7bS%9wr)ELx(#|>8=d^IwOWgJR(sJuE>oeT-1b02kT^p7jTw1?$KlfDa(p?KG z7wleIvvl`@>IEHhuFa`fx^qFrf*lLCFDPHIeQA^BiD}2DZChHlpmf2u1tm+jF4(f5 zaB2IT);U+_6ffAkpl!~TIW2QGEG=A6w1AZ?SinC~u;A&eCd-ed`1+bG+4D(Dlb7Z% zVC}}C)%d0CxpCk3zO1GZ-!b391*5)9k|E!n3x<7JyU%@_7QW!pp7=iX?ek?%Jn-%H z-ME0YaNGB;?;Y;x8@^q>H@Vyn vyYzMHK}tatg+PTw8AaB6--+kMLcWxiD9TM4M(3T*YIV&5V}3w>#e?`A*=m!9uS z8+>yR&Gw}{-;ID=E t>677uwP6me_sIgBo E)CGW7^ zdheHZ>%6nPGriY(r+L!>uX- ~F7;s_^9rbDi9PyUdI4?=Fmy6 z6M(&Q=#1BCz$q^}?{yAv){C0FE&x!^C9jJB)YI&B8E|F}UG=&GXz`*puT}u+x#ral zKs}va*8v?~)a7*(aKnpwyt)Cmyy&ji9l&iby6<%laDEOw^y&pX@S-PPj{%RosNd@; zpwEk5cs&O^^P)komw*8;dgV0?81kY~uMxm&FB s=amt z@Lqep_5f u_nbEw_34bbXI*F8G`*F5QlXD0ylba~zcpq?JjTLAR` zUC%p!+n#ja^Bw^8^m;x3pq|H`j{pxn>8WQQ;E5+a_k0HE_oSDe1ArHvG~_u5K>xq? zd<7Wxq&J?UfDuo6=lK>e=1CKt?*ZeU6z>&>_l@ 0GVEt sK6^9kmp7FJZn)i-fO>S9RTljz_T8J{y*$_ z2yoDoj(9cz@Loqf8v%H)6Q0Le8BaRxc?xjSll6ax-8H+^1*1MAKCgXfo6lBEILy~F zpHcwk>vo@V0Q$YcXD48X4^{b80x(~9`|JW>zV7j<0aW`?olh-buMgGx><6HpgFXiU zsOPZHAprWl(dP)D!H16d90j1B6F$cQsOPlLDZoh|I_Gm1aK?w4d@caa`_N^dOMr_$ z)Z)|3Qsz*r&sD$`AG+q#4rueCPM_<54j=0Bxe2)8Lp?s-fLlIv*XIu4whuk f0<5bLk^c1b>oJ_$Zl z;k^?R0(Pj%yAl99RPDVRu*;kFdhY?$c+)=bS^(_Oe(yQ}?9f5)1AuyOYVbY`IOI)@ z-bWbBp<~`h0jTGs_X)spZ#v_B8gR;+&Uv2&pq>lf=K-kalJ`YGlQ*?^Hv=wvQ>*t? zz!h)0=G_ix^QKPk>wpe#>hit`xZzDb-ray(-gMXd4&b&oJ@CE{xaUnzydML4y{X^( zDWK1rUU)wTJoBa@@0Wl9Z+h+h3NY+VZ@fnVBi=Ob{T49hO|d=`fcM_C#wQ;A73Wjw zS>d_UbEC};&pgje&k2vK9*+Va2KENt54;z6C-81yW Dbr3f$JaCAjsw4!XW@z3 mUCa4B)gby@2Y z=km_EZ^eLfzw;C4`_6ZqTb;+3UU6=AKJR?cxz@SDxzxGDx!5`1`Hb^1=R?lho%5V? zopYQwI&W~!c3$tC b;p3fSgJ zg`S%L1)fymSqv!h%<;_jT?k66$BtI?pT)ke2Sb7T9D*sh%kuu*E*nlaf7? z__A@Hu^h1Plj9!L=F#ebsbfdiJvso_Jm{v!4M3*{b$i?bba~JnkK2GA54!Jh4{+Cm z9(wcw9(d3bkH-K!-tX}g(C0xfJe~uddC;K8OTd5!z490a40+HSk5Ryg2fg!n3mEgb z=yBGr$pdG7mpv{ap=KtX^Ek`H1&{N9>Y2wq&e$DeMNWF0;B$_6H1KfLqme6e)~?=z z4tg9wWcN(k=TXbUevdjX`;1+c2krLQ#i#D{*ug`kM+KM7T*vJmRPIrRXsHKn^Vka5 z;(^~7@mS}P<&o(@IUd 4U z-?_haA9Wvbf9*c(KIC3)-S3`;t>9B!`ClBP^DEEc4EO9xy0r2lplRh1ySwfW+ ?0(4op!)&$dUx9IUWdZ_-0SW3+tt~XxbL^?b=x^}!tIP@g SO {#>-}!&cDvlF+$!BF+;+Of z33j+`cPn>Gn6t;N#;w|Iw;O+&TIaUUt=4VtWGuzH%q_*b6k!{}RyQ_8C2r%tAN^|0 zA4b1wTXpy!T320Nbp`iCy9B#DH~x33D90@u@}K$5WHi@p0~f^}-Hp=RQV~gUqfEDS zz*;vPP`S}t*D+UYrd{d1>p0+@E5*6RB74FW=Ya^%TxryG1Uau=>6Pm+U DC9_>fJKz= I2@ TYF>3|)x}k8sG3%>^gXT( zNPnQZt-7XaR`El4*!7U>LDvJWd~CPtF4rno+Tpq#y;AN ~NT+Pdd*7qDR_J#e`X zKwA%8dI4zbvCAVqtIy>L0ByCqv;ojohs!lStJCE=0BzlLxdA|1w_LjTtR9zc0NQGG zxeB=ALM<-MfXgm)(WMD+!6nJ~ybJAesY0aEg=$=?0lQtO)@3hXj|=U0sRQhDp@S|5 z0QD}^;Bpvn$c2u&Gy;ye& tOH~rHPwX@T@nyk<3cGe$$%uJ#=Fpa z=W%CvLi5=gI_6A6&Vz`)bf(wNuK>f&^u~D 9g(jz`Y)(76|d9-z=&gdT)$XS(Iwg~XfAbj`UP(B@2?&es7Q$i9Ja$(b%XHzDBy zB4?f1Z+o6{ev9AA9^-y%`lRy- 8jAzs7G_zv9wg z|MRPVhQ$AjPXNxe-?b%8yvorqzvCw&wbAfZuO!iYXZTs{r=V4pQbk4v7SLet7A?^>cf0+0^KjBz* znscf%r8p-e`^tQlkig2XagIkqoHI*!?= SRrp_t%H~XBZ)`!uXftyROM9Rw9^R|E6phd1(Th&JC!@_onP?% zhVQe#ubRE?`_j*mS?aV6nOmKx$f*!e;6%Aj8vr>@l;xBONM}{7cghB?W4PI=1X%1u z`A&I&jZU=xPu#zQL|&3pB9aoE);N`6?mNXHa^N4>KM+1MJJxAp3B6eId XIu B^G!=u6fEhnEnKo`N%s5 6B7QA?B_0+J zi3i0f`BFR}ej$D??hs!S_lUd2x5UrH{o<$MKJgRrWAP*LLvgS8fw)V2Q+!{1Pkfg> zaz}hy+$nAsw~1TDSHvyiX7Oe5CGkaZllX%8y!f2>s`!TZtoXF}g!qWKQG8T P`> z&0;DM7mML@i}S=A#kt}Q;v8|_lHB=_qur^^snzM!{A*5BC@w&md@&oE_ODOR&K9p1 zuM=mAhpdvs>EgBGL~)upMI0~A5I36lnKqh#{OO*SEbg9GGA+l9rHzUj&4&?=f58IU z7!ctSE1nQRF{abF=p7HQMXz{xEPBL2rA6_yhoWB51JQlaJ<(lJg1C6v9Z~T#_H$d* zBkC4)iEfGptr~4_h#J1=6kQi}h^~p+MQx&1lxkp2-2&9EV#+aOh3FMgi>O(2S#*_2 zz@=OiT@q1~=)CBhXvm7rh)#=6iB5{>sO?!13iO#)I`x@Wh) KIv zZx`*ry;M{#D#QJt?N-q??!H;Hg}WDvN^svKDiRgqo+m00<>S6Vv{96c`+8B1C>!^^ zU#t^liSkW5Ofp53AxcLF*4rkD5=Ci(wW1VJvM5cIDq X%$-{8W5D%|oUhyy*Gs45$m@yv4W8U#F5%ZphxYe-`9P+GL9nVA3>O>w= zRwwh2wmOxE^wn#5$XuPl!}`_hc-Xmm2M<-ND|y(xdKV8htE+j~yLt}?=!P3HooN4h z3|)(92eiddYs^)^l^AM{xeT}zLl $6g_v`I+{JV@<_uuNVmcjj3UD%pPQ)As z9F3vIm?MCO7&;tt2yifl4#eyS)Wy)gm|DQz7}^t41ISrS)iJwysEVlsRK(EEm>q!5 zd9*#I9B^j_mBwrXY>lBUF`EG;F;pB=1jt@Yn_>z8`7x9ivk`E89_7Yt;2|d_n}_u= z>i}6Xlo^u&NROelF=>F*7)psr0wl&zLd+UKd<@0K!~!Ow>3#HgG!A#8sXO`>pevg0 zMBfJVMAQA~dw{#q^f0;?@F1EVM?V5&EvCNcCp IwZjou4LUQBzUYXC_YvFKfVN>y|vAaOBOMDGOLwxS); z+j%IDE(0VirqbwbfVO$GHF^u6B$|q&ivWeuv?;m(kRMGOqH_S*(d%sM>?t974I=T; z6c-%}n24hHQR7kAM@7-ws4>8sC>o6#0aPuf*HOcOaYq`88sy Jh~Zm1JD^o zFQWzkFQVvq)HA@ x*gR6=#HZ6Q5}G5 zQPdvQ26*d8tx;D2Em71Qbs2CeiY`Vq0WL(*`KWV%vr%+9>J;E)6rG4V4mcJ?N23}6 z4N-JB>JZ>y6dj1F2keicx~P4CJX@-b+6#E?NPD7c0M${nJ8Bo8DvBzicJf&}qP7Fd zqo^#Z6tFFdwnl9MltfW+R1u&siZ(?R0P>?KFKQzoH;QtivH|O(XkAnmATx?GqS66t zqbMyZ6_64|$x%sw#3)LLS_42Q#7D&eVlf1f-yP4Dj5M zjz%`}DMun30M8uhaO5Gt!ALp~Sr6!Uq`Js`fZ9mf8@UI78Bh~h4Zxh(9k~ktLs=DB z!DsD^+yQv%NZTXJc_@o4<+HX$ZUt OKSIkE(hKAj38^ZAs*$W44oZsZ0) z^E}Fn+z9A%q;-*5d`eDaHeh`uWkzNIP)~Z~TEJsRN{dVdT%JcMk;#CEj+7Lc$U}T& z9N>W?B}A?P^g7b}i17$6B{p&bfRuL;ZvknxG#2p&aNm(eBSrxC9O-q$E5L9B4MhwB zUPjPB#0$V(M|vLd4A37zPb2yOJr?vN;xP}8A|CS48}WdL`w{nexEpbYhwg}5fHRnl z5j}vj^QbH0CZOAqu19nLZaLD8h)w`H>RLoQpe=%~M6>|9Fi|6}@>$IhmjRa|=wd_@ z;3hO7;ymEwJUSO~7I4Fn&P1FBbUM ;&wHpzRUmfTQ!METWW$ zZ4p}mTOz0=q8LyVL4^^U0O-1ch mn#CA`_4iLFo}| z0cjDG8j%9PFeOJM0TLr9Az}?6K7!&RVgXkjX~OW{z{xyrcn5%zyfKUd8s^bk!x-Sm zJQ_9(@hPtjulSS!!wUdZV$kpsaCjc|8=mqh&kfJG6f9$p3=e_52I@0B0X#O`H{3Ja zHQX`WHuM;}4Rp(J6UjFW)M2>BU t^+Wlt%j=rD0;i04FE-NF*E}p-$g?c0P
)y=sizI604EJ}%y1OYXrM!egMb4DI$~%595ztBVLt#W zTW8n@KzG*~_VQ3;sOF){PzgZ4?>6iLpx-MDI{{F~GD9f<>bS$O9RT6C7&Zf-h1(2U z`IHhvF#yU}WGDn+AU7Eb08qYsLmmKnu+fkU*kGU>LpA{OcD*4BkZGU{Lporsfzk}A zfD{8I8 yhu;I-4W~Qd zxB0A|@NOP%g?I6AGyDbv3~u;!zy(L@2)_n^bJrf;251eZtKnAwE#cH0ei?8chC93o za1PTo{5+t cjT~>cVMXcrD ^payy-l87xor# zaNe7+(Xf%Q*I}>1hQp4T(oonSl3#|=K-dew^DufA)(?0ZMtxyV0FT4yQP@L3{XBXQ zcAtlPVRr!)i|KY)4`8z`-3sdh6xq^^uui_r^{@`WwJ>TAYXcmGu?@SzLrYjQpbk?r z>=F+b! VFv-|wqs#O0gYjFAgms+ zKaA?a_VHPJ!}b7b!l*iIH=nXAtO{^{233Yt0CtAamaxr$d|N6BD+a)CD-GKQ*cwLL z!^#0=VU!=12iO=!g<+cj1!0sMwgCX$$qCB_zz13%whjPI$qLH^WQ0+A*jfPg9%*5z zfRr#w4od<+VH3j=0Bgc1J}eFZ&5aG42*vl^P#O2iRy!Z$rlbZ$fD_bOi7^lwO4n z1BOCrF!Ut=?>-Rv0`NSPo`v=Uo`zCi=o7%>P Il6CXb+{f&{hEIxf*%}&=N|`p_c)dLg`{? z6W~H9oew<+I2%f5LQexuh0@8;6M*BPbS(5JpfQw=gf;*Uhti?YgMb5}lo!1b9(*o# z6`^%33yWiDEnsgb?Fp>`^vt8`(A@y&`>xO`KxHUZgzg0F2&L_z<$$tKDh=HR*cwV( zLN^0SLa8{k2v8VGV}E+X{S`ji6k33kd^8%m5dcptH*^CaCzP^7*8|ptQdVdtAS0C0 zL)QY*LMb&g1&|y{Nuh~=giu-&8V{(01r3b_Oz7#oeq0az(X*ZMpE}W6k12Ki=_U_j z`ZvfN)zgUnH2}}O(hmcM^z4rS(xCn&A_IDQp??l|rtjB3)z>=ib?nnW(Vw3GSpP`> zP~WS6puewwIrpCauKtexw!TN-t-q!3(%;nI(0A%P^w;$5`Zj&5{;K|pzD3`xzpTHg zZ_;1TpVyz$pVgnypVpt!pVS}MAJZSzH|me*8}x_uhx7;a2lV^(dmZca`}Eb0wfepK zE~`EI8hw&&HA0uwZpYpFUHU40qU|on-Hw&|3VoGhr6UUowiS*$9oN|I&`;5JeYw6& zU#j1t->fgu7we1kh58+ioAd?xe0`pNyW>WEylt+2gFZ)}t&g+4X~n`ieU?5`pP^6J zuhpmNQ}rqOWPPGOLBB>HuaDEm>L+yXbvLZW5h~}EJ60f+ImX())s5-i=vrr#I&O6w z)s5)3IBs? #*2UVk&S;&n#&%HmQa7M`p{tyC!|I0BbKNstzpe;!Kh+gF z#-r^%-4oqoU4di1<0IX6$A`$vbL`bU(B0GB)!otE*7fMRb+>fv&AW6rbvJaKy6d_Q z-8Egiu1(jfyQ-U*o$I&(Kh3(!x=Xr?x+dKP-Fe+P-PsTFB-ozTl{ywU7DCQbx_rlz zkeNw#LYL#1 6?3ix7<9f%Vx<*~ayoz~8bPc+7j)xHr=??0$91kGW>*{p- zbhWy@x;?rYUA1nvZkMh~m+4rc+o{{3%dpt4E7z6jN_E?GTXkD>n{_2fD@J~tZLDpE zW3^+tW09^ -9}xmZi6mImyMM5x^=oNU8XKww^o;?OVy?5l66VCL|uYz zjV@jnr;F81Xy0qcwePfVwPV_e+3~h-v?JQr+E?0PZT75nv$AGo%*vd#VOHv_^jXQX za%Qca6+dguto5^EXN}K%J9BL2`#IxthP1Ekp4&aMdurEjH)?m+uFvj{-H6>|yB@pS zcDL+q+O^qTvb$(^!ER9dQahl1p?$9H*FM$uX`g5xYaeMJYJ0U0wD-05w0E_4w70cA z+HUPFZI?FHG1c*=HpMZ;u~U0po9vkE*r82wOme)YO>|6jY}Y0@COEcf;~nE2TeV5H zSG8BPap(!Acd?Ew+KF$PwRzB|OWH20i`pjb1?_q5Iqg~P8SQE9DeVdEaqThfQEj94 zh_*p{SbIo&P`hSPmsOl&y>`F0PP @_tF)Ec3hhqq4()brxwcGO zs%1UDO}j #Y^_u;fI?X=K4#y |vMSfi=d?AGkkRB0+T z6`Gxz9h&W$a!r}0RI^R9RkKC2SyQ4Z))Z+9HJdaAntV;3W}_xovq6)i$=0mbtkYy^ zGBp{Rbj?~#nkH3~qDj^yX%aOFnl+kuO`IlHGogO39{FbEn{o9!^;`9r`i**2J)(ZC zex)8(52**$FVzF;7wYHgXX<|SQ+1#ErqvVmWA!8TLv^qEf%?8W*YTdZ%j&NBj{3H` zN8PQyrS4MSRNqi{s;{d%)YsJQYAXK<{}Dc8dN;d8-K=J|nv1;pW*GkkL8LNhQ@5(Q z$W`^wH$&fCQR5GK9#Ge-;fttCrq!wYzo9rWy%*725siuHjfh4>G$^8%B6=aBei1zt zQJ;vOi|Cn%o{09TYt?(zd(^wtyVTX{DmDA`(EClLx Krrt z@g#MsIXjfkSLdmzP`wGQ7O1IMT?FVerLF2MfX(XSC2zhcQ5Vmv{1>YD7oswCDU!D_ zvAd_GtJkV?)Y dJST`Yc6bwkyux~|Il@~|~^sM=L+Dr!|-Rb5e0 zi|VrKl8P>>npBiI?}F+)tM{DhtcuR4PODC-PO9jHijJ#}sg9}|RSzub2ooW5UW4ke ziVmp`st&LR>Q(zyRL5%Ar>a#oElm0B%xw0FiuEP_bd`$ssA^QZR8^{K)o#_XZ`jv} z3Kbg)bXlbee Fic}3Zxyr;aQyshj}b}PG-H q z>&g!0HD$Z9S$SD`NqJG(q`aU!uRNzbt30DTtvsbXsXU=Pt~{nZs%%sqQ8p+KD-S6T zDi0{@mHU--%6-aO u8tWs`MmMY7Y#mXXOp>mV5K)F@9 zMY&m7qI{)ztr%70EAx~amAT3d${c03a=mh$GE14M%uuE)*DBMLsmc^(vNB1Ts7z3< zQN}Cdl(EVQ#e2oL;+^8HVodQyF{~I;3@TnIo+_Rx`W4R=y^2SQhl)N$x8km%NAW<> zu4q+UQ8X(qDcTfQ6)lR(ii?U{ikpf~MVI1+;<} Q4k+ps`xSc?HHzJeDn*52r(%a ==WGmJy)+w?S8H#kpT1A>7Rgt1dRwOFcDB=_oA@4%ohKz;0 z2^kG}9r7w IXZ6hWmj(>%kxjJq=Wd%}n!MetHFfNP52iK2V eJ9-)ndiUPSwqLpW?FB#~=RPwkozLHY;)z8^7834N7zM^EvNB z#zUIEyzu2nNM6u|FVBC;r=|$HLT-dy54jf77IHPDCFF8QQ^>iHGa;u!PJ~ 9RvWT6WKT# CLdK42cs|gzOC2 z5kigs7crBO8PfN8cF6jW42!IgN1tbeq=&2xNexK}Neam~O$ dU?u^6-Ff8tnjY%ooHPw&5{@$ZSQvp;CQQI