From 585d0749bd93b0d3708b7d037ec7e1562ed02435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=8E=E5=9F=8E?= Date: Mon, 15 Jun 2026 10:52:43 +0800 Subject: [PATCH 01/36] =?UTF-8?q?docs:=20add=20community=20section=20with?= =?UTF-8?q?=20WeChat=20QR=20code=20to=20README=20=F0=9F=93=A2=20(#1409)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add WeChat group QR code image to assets/figures - include community engagement section with links to GitHub discussions - reference Community Repository for meeting materials and details Signed-off-by: mingcheng --- README.md | 12 ++++++++++++ assets/figures/wechat_qrcode.png | Bin 4901 -> 5534 bytes 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 453d2a8391..deec52423e 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,18 @@ monthly. We warmly welcome community engagement and contributions. We are also **actively hiring interns and full-time employees** with open positions in both the US and China. +## 📢 Community + +WeChat QR Code + +We warmly welcome everyone to join our community! We have a _**WeChat group**_ for +real-time discussions and a +[GitHub discussion board](https://github.com/areal-project/AReaL/discussions) for +asynchronous conversations. + +Check out our [Community Repository](https://github.com/areal-project/community) for +more details and meeting materials. + ## 🙏 Acknowledgments We gratefully acknowledge that major contributors are from the AReaL Team at the diff --git a/assets/figures/wechat_qrcode.png b/assets/figures/wechat_qrcode.png index 885cc31dd93144c9c1c3be20ed1461fc14dbe910..01246f26dcb6753b22bcf3c3b2efb09dab6152fd 100644 GIT binary patch delta 696 zcmZ`%yH3ME5L{jogpP*xWKarFe2zg#IXOlVQ3Mi(@F+}&F&8)yKV%<@p&%)cD1nXw z(IIMDK7+47h%Z1C+&Li_0d_N0E0T)wz+Ms5;yQvXFu4ZYT|`L2w)^cK#y_48gBpP zGZx^prw85kH`QX@Rme0>?TfNTy1#`a7G79fieh@~dB>#;~1 M=AYU8ooTrc0N2G20{{R3 From 13353cc6767b7e47d45eed5fd87a0a6e669a246c Mon Sep 17 00:00:00 2001 From: guojiapub <89919061+guojiapub@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:45:41 +0800 Subject: [PATCH 02/36] Supporting features for IcePop and KPop (#1405) * docs(cli): sync cli reference for icepop and kpop params * feat(examples): add icepop and kpop configs for gsm8k * style: fix end-of-file newline in icepop/kpop configs * fix(icepop/kpop): detach imp_ratio and KL inputs to prevent gradient leak * fix(kpop): use <= for KL threshold to include boundary * refactor: unify icepop/kpop into rejection_sampling with binary_kl metric - Add binary_kl metric to RejectionSamplingConfig for KPop - Remove enable_icepop/icepop_alpha/icepop_beta/enable_kpop/kpop_phi - Update gsm8k_icepop.yaml to use rejection_sampling with ratio+lower+upper - Update gsm8k_kpop.yaml to use rejection_sampling with binary_kl+upper - Sync CLI docs --- areal/api/cli_args.py | 6 +- areal/utils/functional/functional.py | 24 ++++ docs/en/cli_reference.md | 16 +-- docs/zh/cli_reference.md | 16 +-- examples/math/gsm8k_icepop.yaml | 190 +++++++++++++++++++++++++++ examples/math/gsm8k_kpop.yaml | 189 ++++++++++++++++++++++++++ 6 files changed, 423 insertions(+), 18 deletions(-) create mode 100644 examples/math/gsm8k_icepop.yaml create mode 100644 examples/math/gsm8k_kpop.yaml diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index d5ae9c2cad..82d3a485f0 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -1348,8 +1348,10 @@ class RejectionSamplingConfig: "'ratio': direct importance ratio π_proximal/π_behave. " "'kl_k1': KL estimator k1 = log(r), forward KL unbiased estimator (can be negative). " "'kl_k2': KL estimator k2 = 0.5 * (log r)^2, non-negative quadratic approximation. " - "'kl_k3': KL estimator k3 = r - log(r) - 1, non-negative exact forward KL estimator.", - "choices": ["ratio", "kl_k1", "kl_k2", "kl_k3"], + "'kl_k3': KL estimator k3 = r - log(r) - 1, non-negative exact forward KL estimator. " + "'binary_kl': KPop (symmetric binary KL divergence) — masks tokens where either " + "KL(proximal||behave) or KL(behave||proximal) exceeds the upper bound.", + "choices": ["ratio", "kl_k1", "kl_k2", "kl_k3", "binary_kl"], }, ) agg: str = field( diff --git a/areal/utils/functional/functional.py b/areal/utils/functional/functional.py index 848edd23d3..d1583e1523 100644 --- a/areal/utils/functional/functional.py +++ b/areal/utils/functional/functional.py @@ -245,6 +245,17 @@ def apply_rejection_sampling( kl_estimator=estimator_name, apply_clamp=False, # Don't clamp; threshold check handles bounds ) + elif config.metric == "binary_kl": + # Bidirectional binary KL divergence (KPop): mask tokens where + # KL(proximal || behave) > upper OR KL(behave || proximal) > upper. + kl_fwd = compute_binary_kl_divergence( + proximal_logprobs.detach(), old_logprobs.detach() + ) + kl_rev = compute_binary_kl_divergence( + old_logprobs.detach(), proximal_logprobs.detach() + ) + # Use max of forward and reverse as the metric; upper bound applies to both. + metric = torch.maximum(kl_fwd, kl_rev) else: raise ValueError(f"Unknown metric: {config.metric}") @@ -426,6 +437,18 @@ def apply_rejection_sampling( ) +def compute_binary_kl_divergence( + log_p: torch.Tensor, log_q: torch.Tensor, eps: float = 1e-8 +) -> torch.Tensor: + """KL(P||Q) for Bernoulli distributions parameterized by log-probabilities. + Treats each element as a Bernoulli: P = [p, 1-p], Q = [q, 1-q]. + KL(P||Q) = p*log(p/q) + (1-p)*log((1-p)/(1-q)) + """ + p = torch.clamp(torch.exp(log_p), eps, 1.0 - eps) + q = torch.clamp(torch.exp(log_q), eps, 1.0 - eps) + return p * torch.log(p / q) + (1 - p) * torch.log((1 - p) / (1 - q)) + + def ppo_actor_loss_fn( logprobs: torch.Tensor, proximal_logprobs: torch.Tensor, @@ -549,6 +572,7 @@ def ppo_actor_loss_fn( clip_mask=clip_mask, dual_clip_mask=dual_clip_mask, ) + if rejection_sampling is not None: stat.update( behave_approx_kl=behave_approx_kl.detach(), diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index ec1272c6f2..ded31439f1 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -1163,14 +1163,14 @@ Attributes: lower: Lower bound for filtering (optional). ``` -| Parameter | Type | Default | Description | -| --------- | ------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `level` | string | `"token"` | Filtering granularity. 'token': per-token filtering (each token judged independently). 'sequence': per-sequence filtering (all tokens in a sequence share the same fate). When metric='ratio', both the filtering decision and the correction weight (behave_imp_weight) operate at sequence level using the geometric mean. **Choices:** `token`, `sequence` | -| `action` | string | `"mask"` | Action to take when metric exceeds threshold. 'mask': zero out loss_mask for filtered tokens/sequences (rejection, completely excludes from gradient computation). 'clamp': clamp importance weight to \[lower, upper\] bounds (truncation, tokens still participate in gradient but with bounded weight). **Choices:** `mask`, `clamp` | -| `metric` | string | `"ratio"` | Divergence metric for filtering. 'ratio': direct importance ratio π_proximal/π_behave. 'kl_k1': KL estimator k1 = log(r), forward KL unbiased estimator (can be negative). 'kl_k2': KL estimator k2 = 0.5 * (log r)^2, non-negative quadratic approximation. 'kl_k3': KL estimator k3 = r - log(r) - 1, non-negative exact forward KL estimator. **Choices:** `ratio`, `kl_k1`, `kl_k2`, `kl_k3` | -| `agg` | string | `"mean"` | Aggregation method for sequence-level filtering. Only used when level='sequence'. For 'ratio' metric, aggregation is in log space: 'sum' = exp(sum(log(r_i))), 'mean' = exp(mean(log(r_i))) = geometric mean (length-invariant, consistent with GSPO). For KL metrics, aggregation is arithmetic: 'sum' = sum(kl_i), 'mean' = mean(kl_i). 'max': max of per-token metric values (most conservative). **Choices:** `sum`, `mean`, `max` | -| `upper` | float | `5.0` | Upper bound for filtering. Tokens/sequences with metric > upper are filtered out (loss_mask zeroed). For 'ratio' metric: must be > 1.0, typical values are 2.0 or 5.0. For 'kl_k2'/'kl_k3' metrics: typical values are 0.5-2.0. | -| `lower` | float \| None | `None` | Lower bound for filtering (optional). None means no lower bound. For 'ratio' metric: typical value is 0.5 (filter out tokens where policy probability dropped significantly). Must be > 0. For 'kl_k1' metric: can be used to filter negative KL estimates. | +| Parameter | Type | Default | Description | +| --------- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `level` | string | `"token"` | Filtering granularity. 'token': per-token filtering (each token judged independently). 'sequence': per-sequence filtering (all tokens in a sequence share the same fate). When metric='ratio', both the filtering decision and the correction weight (behave_imp_weight) operate at sequence level using the geometric mean. **Choices:** `token`, `sequence` | +| `action` | string | `"mask"` | Action to take when metric exceeds threshold. 'mask': zero out loss_mask for filtered tokens/sequences (rejection, completely excludes from gradient computation). 'clamp': clamp importance weight to \[lower, upper\] bounds (truncation, tokens still participate in gradient but with bounded weight). **Choices:** `mask`, `clamp` | +| `metric` | string | `"ratio"` | Divergence metric for filtering. 'ratio': direct importance ratio π_proximal/π_behave. 'kl_k1': KL estimator k1 = log(r), forward KL unbiased estimator (can be negative). 'kl_k2': KL estimator k2 = 0.5 * (log r)^2, non-negative quadratic approximation. 'kl_k3': KL estimator k3 = r - log(r) - 1, non-negative exact forward KL estimator. 'binary_kl': KPop (symmetric binary KL divergence) — masks tokens where either KL(proximal | +| `agg` | string | `"mean"` | Aggregation method for sequence-level filtering. Only used when level='sequence'. For 'ratio' metric, aggregation is in log space: 'sum' = exp(sum(log(r_i))), 'mean' = exp(mean(log(r_i))) = geometric mean (length-invariant, consistent with GSPO). For KL metrics, aggregation is arithmetic: 'sum' = sum(kl_i), 'mean' = mean(kl_i). 'max': max of per-token metric values (most conservative). **Choices:** `sum`, `mean`, `max` | +| `upper` | float | `5.0` | Upper bound for filtering. Tokens/sequences with metric > upper are filtered out (loss_mask zeroed). For 'ratio' metric: must be > 1.0, typical values are 2.0 or 5.0. For 'kl_k2'/'kl_k3' metrics: typical values are 0.5-2.0. | +| `lower` | float \| None | `None` | Lower bound for filtering (optional). None means no lower bound. For 'ratio' metric: typical value is 0.5 (filter out tokens where policy probability dropped significantly). Must be > 0. For 'kl_k1' metric: can be used to filter negative KL estimates. | (section-scheduler)= diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index d8cb5c8f80..81db1197cf 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -1161,14 +1161,14 @@ Attributes: lower: Lower bound for filtering (optional). ``` -| Parameter | Type | Default | Description | -| --------- | ------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `level` | string | `"token"` | Filtering granularity. 'token': per-token filtering (each token judged independently). 'sequence': per-sequence filtering (all tokens in a sequence share the same fate). When metric='ratio', both the filtering decision and the correction weight (behave_imp_weight) operate at sequence level using the geometric mean. **Choices:** `token`, `sequence` | -| `action` | string | `"mask"` | Action to take when metric exceeds threshold. 'mask': zero out loss_mask for filtered tokens/sequences (rejection, completely excludes from gradient computation). 'clamp': clamp importance weight to \[lower, upper\] bounds (truncation, tokens still participate in gradient but with bounded weight). **Choices:** `mask`, `clamp` | -| `metric` | string | `"ratio"` | Divergence metric for filtering. 'ratio': direct importance ratio π_proximal/π_behave. 'kl_k1': KL estimator k1 = log(r), forward KL unbiased estimator (can be negative). 'kl_k2': KL estimator k2 = 0.5 * (log r)^2, non-negative quadratic approximation. 'kl_k3': KL estimator k3 = r - log(r) - 1, non-negative exact forward KL estimator. **Choices:** `ratio`, `kl_k1`, `kl_k2`, `kl_k3` | -| `agg` | string | `"mean"` | Aggregation method for sequence-level filtering. Only used when level='sequence'. For 'ratio' metric, aggregation is in log space: 'sum' = exp(sum(log(r_i))), 'mean' = exp(mean(log(r_i))) = geometric mean (length-invariant, consistent with GSPO). For KL metrics, aggregation is arithmetic: 'sum' = sum(kl_i), 'mean' = mean(kl_i). 'max': max of per-token metric values (most conservative). **Choices:** `sum`, `mean`, `max` | -| `upper` | float | `5.0` | Upper bound for filtering. Tokens/sequences with metric > upper are filtered out (loss_mask zeroed). For 'ratio' metric: must be > 1.0, typical values are 2.0 or 5.0. For 'kl_k2'/'kl_k3' metrics: typical values are 0.5-2.0. | -| `lower` | float \| None | `None` | Lower bound for filtering (optional). None means no lower bound. For 'ratio' metric: typical value is 0.5 (filter out tokens where policy probability dropped significantly). Must be > 0. For 'kl_k1' metric: can be used to filter negative KL estimates. | +| Parameter | Type | Default | Description | +| --------- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `level` | string | `"token"` | Filtering granularity. 'token': per-token filtering (each token judged independently). 'sequence': per-sequence filtering (all tokens in a sequence share the same fate). When metric='ratio', both the filtering decision and the correction weight (behave_imp_weight) operate at sequence level using the geometric mean. **Choices:** `token`, `sequence` | +| `action` | string | `"mask"` | Action to take when metric exceeds threshold. 'mask': zero out loss_mask for filtered tokens/sequences (rejection, completely excludes from gradient computation). 'clamp': clamp importance weight to \[lower, upper\] bounds (truncation, tokens still participate in gradient but with bounded weight). **Choices:** `mask`, `clamp` | +| `metric` | string | `"ratio"` | Divergence metric for filtering. 'ratio': direct importance ratio π_proximal/π_behave. 'kl_k1': KL estimator k1 = log(r), forward KL unbiased estimator (can be negative). 'kl_k2': KL estimator k2 = 0.5 * (log r)^2, non-negative quadratic approximation. 'kl_k3': KL estimator k3 = r - log(r) - 1, non-negative exact forward KL estimator. 'binary_kl': KPop (symmetric binary KL divergence) — masks tokens where either KL(proximal | +| `agg` | string | `"mean"` | Aggregation method for sequence-level filtering. Only used when level='sequence'. For 'ratio' metric, aggregation is in log space: 'sum' = exp(sum(log(r_i))), 'mean' = exp(mean(log(r_i))) = geometric mean (length-invariant, consistent with GSPO). For KL metrics, aggregation is arithmetic: 'sum' = sum(kl_i), 'mean' = mean(kl_i). 'max': max of per-token metric values (most conservative). **Choices:** `sum`, `mean`, `max` | +| `upper` | float | `5.0` | Upper bound for filtering. Tokens/sequences with metric > upper are filtered out (loss_mask zeroed). For 'ratio' metric: must be > 1.0, typical values are 2.0 or 5.0. For 'kl_k2'/'kl_k3' metrics: typical values are 0.5-2.0. | +| `lower` | float \| None | `None` | Lower bound for filtering (optional). None means no lower bound. For 'ratio' metric: typical value is 0.5 (filter out tokens where policy probability dropped significantly). Must be > 0. For 'kl_k1' metric: can be used to filter negative KL estimates. | (section-scheduler)= diff --git a/examples/math/gsm8k_icepop.yaml b/examples/math/gsm8k_icepop.yaml new file mode 100644 index 0000000000..6209530d1c --- /dev/null +++ b/examples/math/gsm8k_icepop.yaml @@ -0,0 +1,190 @@ +experiment_name: gsm8k-icepop +trial_name: trial0 + +seed: 1 +enable_offload: false +total_train_epochs: 10 +tokenizer_path: ${actor.path} + +cluster: + n_nodes: 1 + n_gpus_per_node: 8 + fileroot: /tmp/areal/experiments + name_resolve: + type: nfs + nfs_record_root: /tmp/areal/name_resolve + + +scheduler: + type: null + +rollout: + backend: "sglang:d4p1t1" + experiment_name: ${experiment_name} + trial_name: ${trial_name} + max_concurrent_rollouts: 256 + queue_size: null + consumer_batch_size: ${train_dataset.batch_size} + max_head_offpolicyness: 2 + enable_rollout_tracing: false + scheduling_spec: ${actor.scheduling_spec} + fileroot: ${cluster.fileroot} + tokenizer_path: ${tokenizer_path} + dump_to_file: false + agent: + mode: inline + export_style: individual + turn_discount: 1.0 + +gconfig: + n_samples: 4 + min_new_tokens: 0 + max_new_tokens: 1024 + max_tokens: 2048 + greedy: false + temperature: 1.0 + +actor: + backend: "fsdp:d4p1t1" + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: Qwen/Qwen2.5-1.5B-Instruct + init_from_scratch: false + disable_dropout: true + gradient_checkpointing: true + dtype: bfloat16 + mb_spec: + max_tokens_per_mb: 10240 + packing_algorithm: ffd + optimizer: + type: adam + lr: 1.70e-5 + weight_decay: 0.017 + beta1: 0.9 + beta2: 0.999 + eps: 1e-8 + lr_scheduler_type: constant + gradient_clipping: 1.0 + warmup_steps_proportion: 0.001 + eps_clip: 0.4 + temperature: ${gconfig.temperature} + reward_scaling: 10.0 + reward_bias: -0.5 + kl_ctl: 0.0 + ppo_n_minibatches: 1 + use_decoupled_loss: true + rejection_sampling: + level: token + action: mask + metric: ratio + lower: 0.5 + upper: 5.0 + reward_norm: + mean_level: group + std_level: group + group_size: ${gconfig.n_samples} + adv_norm: + mean_level: batch + std_level: batch + weight_update_mode: xccl + max_new_tokens: ${gconfig.max_new_tokens} + scheduling_spec: + - task_type: worker + port_count: 2 + gpu: 1 + mem: 32 + cmd: python3 -m areal.infra.rpc.rpc_server + env_vars: {} + +ref: + backend: ${actor.backend} + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: ${actor.path} + init_from_scratch: false + disable_dropout: true + dtype: ${actor.dtype} + mb_spec: + max_tokens_per_mb: 10240 + packing_algorithm: ffd + optimizer: null + scheduling_strategy: + type: colocation + target: actor + scheduling_spec: ${actor.scheduling_spec} + +# SGLang +sglang: + model_path: ${actor.path} + random_seed: ${seed} + skip_tokenizer_init: true + dtype: ${actor.dtype} + max_running_requests: null + context_length: 32768 + mem_fraction_static: 0.8 + +vllm: + model: ${actor.path} + seed: ${seed} + skip_tokenizer_init: false + dtype: ${actor.dtype} + max_model_len: 32768 + gpu_memory_utilization: 0.8 + +# datasets +train_dataset: + batch_size: 256 + shuffle: true + pin_memory: true + num_workers: 4 + path: openai/gsm8k + type: rl + max_length: 1024 + +valid_dataset: + batch_size: 256 + pin_memory: true + num_workers: 4 + path: openai/gsm8k + type: rl + +# Utilities +saver: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +recover: + mode: disabled + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: 3600 + +evaluator: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +stats_logger: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + wandb: + mode: disabled + +perf_tracer: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + enabled: false + session_tracer: + enabled: false diff --git a/examples/math/gsm8k_kpop.yaml b/examples/math/gsm8k_kpop.yaml new file mode 100644 index 0000000000..2a0397fe26 --- /dev/null +++ b/examples/math/gsm8k_kpop.yaml @@ -0,0 +1,189 @@ +experiment_name: gsm8k-kpop +trial_name: trial0 + +seed: 1 +enable_offload: false +total_train_epochs: 10 +tokenizer_path: ${actor.path} + +cluster: + n_nodes: 1 + n_gpus_per_node: 8 + fileroot: /tmp/areal/experiments + name_resolve: + type: nfs + nfs_record_root: /tmp/areal/name_resolve + + +scheduler: + type: null + +rollout: + backend: "sglang:d4p1t1" + experiment_name: ${experiment_name} + trial_name: ${trial_name} + max_concurrent_rollouts: 256 + queue_size: null + consumer_batch_size: ${train_dataset.batch_size} + max_head_offpolicyness: 2 + enable_rollout_tracing: false + scheduling_spec: ${actor.scheduling_spec} + fileroot: ${cluster.fileroot} + tokenizer_path: ${tokenizer_path} + dump_to_file: false + agent: + mode: inline + export_style: individual + turn_discount: 1.0 + +gconfig: + n_samples: 4 + min_new_tokens: 0 + max_new_tokens: 1024 + max_tokens: 2048 + greedy: false + temperature: 1.0 + +actor: + backend: "fsdp:d4p1t1" + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: Qwen/Qwen2.5-1.5B-Instruct + init_from_scratch: false + disable_dropout: true + gradient_checkpointing: true + dtype: bfloat16 + mb_spec: + max_tokens_per_mb: 10240 + packing_algorithm: ffd + optimizer: + type: adam + lr: 1.70e-5 + weight_decay: 0.017 + beta1: 0.9 + beta2: 0.999 + eps: 1e-8 + lr_scheduler_type: constant + gradient_clipping: 1.0 + warmup_steps_proportion: 0.001 + eps_clip: 0.4 + temperature: ${gconfig.temperature} + reward_scaling: 10.0 + reward_bias: -0.5 + kl_ctl: 0.0 + ppo_n_minibatches: 1 + use_decoupled_loss: true + rejection_sampling: + level: token + action: mask + metric: binary_kl + upper: 2.0 + reward_norm: + mean_level: group + std_level: group + group_size: ${gconfig.n_samples} + adv_norm: + mean_level: batch + std_level: batch + weight_update_mode: xccl + max_new_tokens: ${gconfig.max_new_tokens} + scheduling_spec: + - task_type: worker + port_count: 2 + gpu: 1 + mem: 32 + cmd: python3 -m areal.infra.rpc.rpc_server + env_vars: {} + +ref: + backend: ${actor.backend} + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: ${actor.path} + init_from_scratch: false + disable_dropout: true + dtype: ${actor.dtype} + mb_spec: + max_tokens_per_mb: 10240 + packing_algorithm: ffd + optimizer: null + scheduling_strategy: + type: colocation + target: actor + scheduling_spec: ${actor.scheduling_spec} + +# SGLang +sglang: + model_path: ${actor.path} + random_seed: ${seed} + skip_tokenizer_init: true + dtype: ${actor.dtype} + max_running_requests: null + context_length: 32768 + mem_fraction_static: 0.8 + +vllm: + model: ${actor.path} + seed: ${seed} + skip_tokenizer_init: false + dtype: ${actor.dtype} + max_model_len: 32768 + gpu_memory_utilization: 0.8 + +# datasets +train_dataset: + batch_size: 256 + shuffle: true + pin_memory: true + num_workers: 4 + path: openai/gsm8k + type: rl + max_length: 1024 + +valid_dataset: + batch_size: 256 + pin_memory: true + num_workers: 4 + path: openai/gsm8k + type: rl + +# Utilities +saver: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +recover: + mode: disabled + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: 3600 + +evaluator: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +stats_logger: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + wandb: + mode: disabled + +perf_tracer: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + enabled: false + session_tracer: + enabled: false From 8b2d2210e4aece147cc7f91af937b02a5fc49d86 Mon Sep 17 00:00:00 2001 From: pyq623 <12421170@zju.edu.cn> Date: Fri, 19 Jun 2026 17:38:08 +0800 Subject: [PATCH 03/36] =?UTF-8?q?fix:=20per-sample=20version=20tracking=20?= =?UTF-8?q?with=20loss=5Fmask=20filter=20and=20multi-turn=E2=80=A6=20(#140?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - head_version/tail_version now per-sample, filtered by loss_mask==1 - fixes head_version always being -1 due to input token version placeholders - adds version_rle field (run-length encoded per-token version sequence) - adds _split_trajectory_for_dump helper for correct multi-turn prompt_end - adds segments field for multi-turn agent trajectory analysis ## Context The previous `_dump_trajectory` had three issues: 1. `head_version = min(versions)` always returned -1 because input tokens use -1 as placeholder 2. `prompt_end = seqlen - sum(mask)` is incorrect for multi-turn agent rollouts where loss_mask is interleaved 3. No per-token version granularity was persisted for cross-version analysis --------- Co-authored-by: Claude Opus 4.6 --- areal/infra/workflow_executor.py | 239 ++++++++++++++++-------- docs/en/reference/rollout_workflow.md | 25 ++- docs/zh/reference/rollout_workflow.md | 24 ++- tests/test_split_trajectory_for_dump.py | 119 ++++++++++++ 4 files changed, 327 insertions(+), 80 deletions(-) create mode 100644 tests/test_split_trajectory_for_dump.py diff --git a/areal/infra/workflow_executor.py b/areal/infra/workflow_executor.py index dcf1eabd12..8d00e68877 100644 --- a/areal/infra/workflow_executor.py +++ b/areal/infra/workflow_executor.py @@ -832,6 +832,80 @@ def _get_dump_dir(self, is_eval: bool) -> str | None: subdir = "eval-rollout" if is_eval else "rollout" return os.path.join(log_path, subdir) + @staticmethod + def _compute_output_versions( + sample_versions: list[int], mask: list[int] + ) -> tuple[int, int, list[list[int]]]: + """Filter versions by loss_mask and compute head, tail, and RLE.""" + output_versions = [v for v, m in zip(sample_versions, mask) if m == 1] + head = min(output_versions) if output_versions else -1 + tail = max(output_versions) if output_versions else -1 + rle: list[list[int]] = [] + for v in output_versions: + if rle and rle[-1][0] == v: + rle[-1][1] += 1 + else: + rle.append([v, 1]) + return head, tail, rle + + @staticmethod + def _split_trajectory_for_dump( + ids: list[int], mask: list[int], tokenizer + ) -> dict[str, Any]: + if len(ids) != len(mask): + raise ValueError(f"ids length {len(ids)} != mask length {len(mask)}") + prompt_end = mask.index(1) if 1 in mask else len(ids) + prompt_text = tokenizer.decode(ids[:prompt_end], skip_special_tokens=False) + completion_text = tokenizer.decode(ids[prompt_end:], skip_special_tokens=False) + + # Count generation runs without decoding to avoid unnecessary work + # in the common single-turn case. + n = len(mask) + gen_count = 0 + idx = 0 + while idx < n: + j = idx + 1 + while j < n and mask[j] == mask[idx]: + j += 1 + if mask[idx] == 1: + gen_count += 1 + idx = j + + # Only decode segments for multi-turn trajectories so single-turn + # keeps the same 2-decode cost as before. + segments: list[dict[str, Any]] | None = None + if gen_count > 1: + raw_segments: list[dict[str, Any]] = [] + idx = 0 + seen_prompt = False + while idx < n: + j = idx + 1 + while j < n and mask[j] == mask[idx]: + j += 1 + if mask[idx] == 1: + role = "gen" + elif not seen_prompt: + role = "prompt" + seen_prompt = True + else: + role = "context" + raw_segments.append( + { + "role": role, + "len": j - idx, + "text": tokenizer.decode(ids[idx:j], skip_special_tokens=False), + } + ) + idx = j + segments = raw_segments + + return { + "prompt_end": prompt_end, + "prompt_text": prompt_text, + "completion_text": completion_text, + "segments": segments, + } + async def _dump_trajectory( self, traj: dict[str, Any] | None, @@ -841,87 +915,100 @@ async def _dump_trajectory( if traj is None: return False, "trajectory is None" - traj = RTensor.localize(traj) - - dump_dir = self._get_dump_dir(is_eval) - if dump_dir is None: - return False, "dump dir is empty" - - tokenizer = self._get_tokenizer() - if tokenizer is None: - return False, "tokenizer not configured" - - # Extract tensors - input_ids = traj.get("input_ids") - rewards = traj.get("rewards") - loss_mask = traj.get("loss_mask") - attention_mask = traj.get("attention_mask") - - if ( - input_ids is None - or rewards is None - or loss_mask is None - or attention_mask is None - ): - return ( - False, - "missing required tensor fields: input_ids, rewards, attention_mask, or loss_mask", - ) + try: + traj = RTensor.localize(traj) - if "versions" not in traj: - self.logger.warning( - "Trajectory missing 'versions' field, defaulting to current inference engine version." - ) - versions = [self.inference_engine.get_version()] - else: - versions = traj["versions"].flatten().tolist() - - tail_version = max(versions) - head_version = min(versions) - # Create versioned directory - version_dir = os.path.join(dump_dir, str(tail_version)) - await aiofiles.os.makedirs(version_dir, exist_ok=True) - - # Handle batched trajectories - batch_size = input_ids.shape[0] - - file_path = os.path.join(version_dir, f"{task_id}.jsonl") - async with aiofiles.open(file_path, "a") as f: - for i in range(batch_size): - seqlen = attention_mask[i].sum().item() - if seqlen == 0: - continue - ids = input_ids[i, :seqlen].tolist() - mask = loss_mask[i, :seqlen].tolist() - # Skip samples with empty completions (all prompt, no completion tokens) - if mask[-1] != 1: - continue + dump_dir = self._get_dump_dir(is_eval) + if dump_dir is None: + return False, "dump dir is empty" - prompt_end = seqlen - sum(mask) - prompt_ids = ids[:prompt_end] - completion_ids = ids[prompt_end:] + tokenizer = self._get_tokenizer() + if tokenizer is None: + return False, "tokenizer not configured" - # Decode to text - prompt_text = tokenizer.decode(prompt_ids, skip_special_tokens=False) - completion_text = tokenizer.decode( - completion_ids, skip_special_tokens=False + # Extract tensors + input_ids = traj.get("input_ids") + rewards = traj.get("rewards") + loss_mask = traj.get("loss_mask") + attention_mask = traj.get("attention_mask") + + if ( + input_ids is None + or rewards is None + or loss_mask is None + or attention_mask is None + ): + return ( + False, + "missing required tensor fields: input_ids, rewards, attention_mask, or loss_mask", + ) + + if "versions" not in traj: + self.logger.warning( + "Trajectory missing 'versions' field, defaulting to current inference engine version." ) + all_versions = None + default_version = self.inference_engine.get_version() + else: + all_versions = traj["versions"] - reward = rewards[i].item() - - record = { - "task_id": task_id, - "sample_idx": i, - "seqlen": seqlen, - "prompt_len": prompt_end, - "head_version": head_version, - "tail_version": tail_version, - "reward": reward, - "prompt": prompt_text, - "completion": completion_text, - } - await f.write(json.dumps(record) + "\n") - return True, "" + global_tail = ( + all_versions.max().item() + if all_versions is not None + else default_version + ) + # Directory is named by batch-global max version (global_tail). + # Individual records may have tail_version <= global_tail. + version_dir = os.path.join(dump_dir, str(global_tail)) + await aiofiles.os.makedirs(version_dir, exist_ok=True) + + # Handle batched trajectories + batch_size = input_ids.shape[0] + + file_path = os.path.join(version_dir, f"{task_id}.jsonl") + async with aiofiles.open(file_path, "a") as f: + for i in range(batch_size): + seqlen = attention_mask[i].sum().item() + if seqlen == 0: + continue + ids = input_ids[i, :seqlen].tolist() + mask = loss_mask[i, :seqlen].tolist() + # Skip samples with empty completions + if mask[-1] != 1: + continue + + if all_versions is not None: + sample_versions = all_versions[i, :seqlen].tolist() + head_version, tail_version, version_rle = ( + self._compute_output_versions(sample_versions, mask) + ) + else: + head_version = tail_version = default_version + version_rle = [[default_version, sum(mask)]] + + split = self._split_trajectory_for_dump(ids, mask, tokenizer) + + reward = rewards[i].item() + + record = { + "task_id": task_id, + "sample_idx": i, + "seqlen": seqlen, + "prompt_len": split["prompt_end"], + "head_version": head_version, + "tail_version": tail_version, + "version_rle": version_rle, + "reward": reward, + "prompt": split["prompt_text"], + "completion": split["completion_text"], + } + if split["segments"] is not None: + record["segments"] = split["segments"] + + await f.write(json.dumps(record) + "\n") + return True, "" + except Exception as e: + return False, f"dump failed: {e}" def initialize(self, logger=None, train_data_parallel_size: int | None = None): """Initialize the workflow executor and start background threads. diff --git a/docs/en/reference/rollout_workflow.md b/docs/en/reference/rollout_workflow.md index b1ed9549d6..9b0720d595 100644 --- a/docs/en/reference/rollout_workflow.md +++ b/docs/en/reference/rollout_workflow.md @@ -140,16 +140,37 @@ Each line in the JSONL file contains: { "task_id": 42, "sample_idx": 0, - "seqlen": 256, + "seqlen": 512, "prompt_len": 128, "head_version": 5, - "tail_version": 5, + "tail_version": 6, + "version_rle": [[5, 100], [6, 200]], "reward": 1.0, "prompt": "<|im_start|>user\nWhat is 2+2?<|im_end|>\n<|im_start|>assistant\n", "completion": "The answer is 4.<|im_end|>" } ``` +**Field descriptions:** + +| Field | Description | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `task_id` | Batch task identifier | +| `sample_idx` | Index of the sample within the batch | +| `seqlen` | Effective sequence length | +| `prompt_len` | Index of the first generated token (`mask.index(1)`). For multi-turn agent rollouts this is the position of the first assistant generation, not `seqlen - sum(mask)` | +| `head_version` | Per-sample minimum model version among `loss_mask==1` tokens | +| `tail_version` | Per-sample maximum model version among `loss_mask==1` tokens | +| `version_rle` | Run-length encoded per-token version sequence (output tokens only), e.g. `[[5, 100], [6, 200]]` | +| `reward` | Reward value for this sample | +| `prompt` | Decoded prompt text | +| `completion` | Decoded completion text | +| `segments` | *(multi-turn only)* List of `{"role": "prompt"\|"gen"\|"context", "len": N, "text": "..."}` | + +**Directory naming:** The `{version}` directory is named by the batch-global maximum +version (`global_tail`). Individual records within the same directory may have +`tail_version <= global_tail`. + ## Grouped Rollout Grouped rollout runs the same workflow multiple times per input prompt, producing diff --git a/docs/zh/reference/rollout_workflow.md b/docs/zh/reference/rollout_workflow.md index f999b31875..da3a791189 100644 --- a/docs/zh/reference/rollout_workflow.md +++ b/docs/zh/reference/rollout_workflow.md @@ -134,16 +134,36 @@ JSONL 文件的每一行包含: { "task_id": 42, "sample_idx": 0, - "seqlen": 256, + "seqlen": 512, "prompt_len": 128, "head_version": 5, - "tail_version": 5, + "tail_version": 6, + "version_rle": [[5, 100], [6, 200]], "reward": 1.0, "prompt": "<|im_start|>user\nWhat is 2+2?<|im_end|>\n<|im_start|>assistant\n", "completion": "The answer is 4.<|im_end|>" } ``` +**字段说明:** + +| 字段 | 说明 | +| -------------- | --------------------------------------------------------------------------------------------------------------------- | +| `task_id` | Batch 任务标识符 | +| `sample_idx` | Batch 内的样本索引 | +| `seqlen` | 有效序列长度 | +| `prompt_len` | 第一个生成 token 的位置(`mask.index(1)`)。多轮 agent rollout 中为首次 assistant 生成位置,而非 `seqlen - sum(mask)` | +| `head_version` | Per-sample 最小模型版本,仅统计 `loss_mask==1` 的 token | +| `tail_version` | Per-sample 最大模型版本,仅统计 `loss_mask==1` 的 token | +| `version_rle` | RLE 编码的 per-token 版本序列(仅输出 token),例如 `[[5, 100], [6, 200]]` | +| `reward` | 该样本的 reward 值 | +| `prompt` | 解码后的 prompt 文本 | +| `completion` | 解码后的 completion 文本 | +| `segments` | *(仅多轮)* 分段列表 `[{"role": "prompt"\|"gen"\|"context", "len": N, "text": "..."}]` | + +**目录命名:** `{version}` 目录以 batch 内的全局最大版本(`global_tail`)命名。 同一目录内的单条 record 的 +`tail_version` 可能 \<= 目录名。 + ## 分组 Rollout 分组 Rollout 对每个输入提示多次运行相同工作流,生成多样化回复用于训练。这对于像 GRPO 这样受益于每个提示多个样本的算法很有用。 diff --git a/tests/test_split_trajectory_for_dump.py b/tests/test_split_trajectory_for_dump.py new file mode 100644 index 0000000000..8b1907edb2 --- /dev/null +++ b/tests/test_split_trajectory_for_dump.py @@ -0,0 +1,119 @@ +"""Tests for WorkflowExecutor._split_trajectory_for_dump and _compute_output_versions.""" + +import pytest + +from areal.infra.workflow_executor import WorkflowExecutor + + +class _FakeTokenizer: + """Minimal tokenizer that returns a deterministic string for testing.""" + + def decode(self, ids: list[int], **kwargs) -> str: + return f"[{len(ids)} tokens]" + + +class TestSplitTrajectoryForDump: + @pytest.fixture + def tokenizer(self): + return _FakeTokenizer() + + def test_single_turn_no_segments(self, tokenizer): + ids = [1, 2, 3, 4, 5, 6] + mask = [0, 0, 0, 1, 1, 1] + result = WorkflowExecutor._split_trajectory_for_dump(ids, mask, tokenizer) + assert result["prompt_end"] == 3 + assert result["prompt_text"] == "[3 tokens]" + assert result["completion_text"] == "[3 tokens]" + assert result["segments"] is None + + def test_multi_turn_has_segments(self, tokenizer): + ids = list(range(8)) + mask = [0, 0, 1, 1, 0, 0, 1, 1] + result = WorkflowExecutor._split_trajectory_for_dump(ids, mask, tokenizer) + assert result["prompt_end"] == 2 + assert result["segments"] is not None + assert len(result["segments"]) == 4 + roles = [s["role"] for s in result["segments"]] + assert roles == ["prompt", "gen", "context", "gen"] + lengths = [s["len"] for s in result["segments"]] + assert lengths == [2, 2, 2, 2] + + def test_all_zeros_prompt_only(self, tokenizer): + ids = [1, 2, 3] + mask = [0, 0, 0] + result = WorkflowExecutor._split_trajectory_for_dump(ids, mask, tokenizer) + assert result["prompt_end"] == 3 + assert result["prompt_text"] == "[3 tokens]" + assert result["completion_text"] == "[0 tokens]" + assert result["segments"] is None + + def test_all_ones_gen_only(self, tokenizer): + ids = [10, 20, 30] + mask = [1, 1, 1] + result = WorkflowExecutor._split_trajectory_for_dump(ids, mask, tokenizer) + assert result["prompt_end"] == 0 + assert result["prompt_text"] == "[0 tokens]" + assert result["completion_text"] == "[3 tokens]" + assert result["segments"] is None + + def test_three_gen_runs(self, tokenizer): + ids = list(range(12)) + mask = [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1] + result = WorkflowExecutor._split_trajectory_for_dump(ids, mask, tokenizer) + assert result["prompt_end"] == 2 + assert result["segments"] is not None + roles = [s["role"] for s in result["segments"]] + assert roles == ["prompt", "gen", "context", "gen", "context", "gen"] + + def test_length_mismatch_raises_valueerror(self, tokenizer): + with pytest.raises(ValueError, match="ids length 2 != mask length 1"): + WorkflowExecutor._split_trajectory_for_dump([1, 2], [0], tokenizer) + + def test_single_token_gen(self, tokenizer): + ids = [42] + mask = [1] + result = WorkflowExecutor._split_trajectory_for_dump(ids, mask, tokenizer) + assert result["prompt_end"] == 0 + assert result["segments"] is None + + +class TestComputeOutputVersions: + def test_filters_negative_one_placeholders(self): + versions = [-1, -1, 5, 5, 6] + mask = [0, 0, 1, 1, 1] + head, tail, rle = WorkflowExecutor._compute_output_versions(versions, mask) + assert head == 5 + assert tail == 6 + assert rle == [[5, 2], [6, 1]] + + def test_single_version(self): + versions = [-1, 3, 3, 3] + mask = [0, 1, 1, 1] + head, tail, rle = WorkflowExecutor._compute_output_versions(versions, mask) + assert head == 3 + assert tail == 3 + assert rle == [[3, 3]] + + def test_multiple_version_transitions(self): + versions = [-1, 2, 2, 3, 3, 4] + mask = [0, 1, 1, 1, 1, 1] + head, tail, rle = WorkflowExecutor._compute_output_versions(versions, mask) + assert head == 2 + assert tail == 4 + assert rle == [[2, 2], [3, 2], [4, 1]] + + def test_all_masked_out(self): + versions = [1, 2, 3] + mask = [0, 0, 0] + head, tail, rle = WorkflowExecutor._compute_output_versions(versions, mask) + assert head == -1 + assert tail == -1 + assert rle == [] + + def test_interleaved_multi_turn(self): + versions = [-1, -1, 5, 5, -1, -1, 6, 6] + mask = [0, 0, 1, 1, 0, 0, 1, 1] + head, tail, rle = WorkflowExecutor._compute_output_versions(versions, mask) + assert head == 5 + assert tail == 6 + assert rle == [[5, 2], [6, 2]] From 1f24e1e566f180e601fe0b398ad1fb17c34fe185 Mon Sep 17 00:00:00 2001 From: guojiapub <89919061+guojiapub@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:20:38 +0800 Subject: [PATCH 04/36] docs: add IcePop/KPop feature introduction (#1424) * docs: add IcePop/KPop feature introduction * style: fix mdformat * docs: remove IcePop/KPop from Examples table --- README.md | 8 ++++ docs/en/algorithms/grpo_series.md | 69 +++++++++++++++++++++++++++++-- docs/zh/algorithms/grpo_series.md | 68 ++++++++++++++++++++++++++++-- 3 files changed, 139 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index deec52423e..9b20e92d01 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ cost-effective** for a broad community of developers and researchers. ## 📰 News +**\[2026/06/17\]** 🔬 Introducing **KPop** — bidirectional binary KL divergence token +masking. Configured via `rejection_sampling.metric=binary_kl`. Also adding an +[IcePop config](examples/math/gsm8k_icepop.yaml) (importance-ratio-based token masking). +Check out [gsm8k_kpop.yaml](examples/math/gsm8k_kpop.yaml) and +[gsm8k_icepop.yaml](examples/math/gsm8k_icepop.yaml) to get started! + **\[2026/04/23\]** 🚀 We’re excited to release our integration with [Scaffoldings](https://github.com/NVIDIA/TensorRT-LLM/tree/main/tensorrt_llm/scaffolding) for agentic RL training - now live in our @@ -207,6 +213,8 @@ All RL algorithms support both asynchronous and synchronous versions by setting | **REINFORCE++** | - | [📄 Paper](https://arxiv.org/pdf/2501.03262) | [🔗 GSM8K Example](examples/math/gsm8k_reinforce.yaml) | | **RLOO** | [📖 Docs](docs/en/algorithms/grpo_series.md) | [📄 Paper](https://arxiv.org/pdf/2402.14740v1) | [🔗 GSM8K Example](examples/math/gsm8k_rloo.yaml) | | **SAPO** | [📖 Docs](docs/en/algorithms/grpo_series.md) | [📄 Paper](https://arxiv.org/abs/2511.20347) | [🔗 GSM8K Example](examples/math/gsm8k_sapo.yaml) | +| **IcePop** | [📖 Docs](docs/en/algorithms/grpo_series.md) | [📄 Blog](https://ringtech.notion.site/icepop) | [🔗 GSM8K Example](examples/math/gsm8k_icepop.yaml) | +| **KPop** | [📖 Docs](docs/en/algorithms/grpo_series.md) | [📄 Blog](https://ringtech.notion.site/kpop) | [🔗 GSM8K Example](examples/math/gsm8k_kpop.yaml) | | **M2PO** | [📖 Docs](docs/algorithms/m2po.md) | [📄 Paper](https://arxiv.org/abs/2510.01161) | [🔗 GSM8K Example](examples/math/gsm8k_m2po.yaml) | | **DPO** | [📖 Docs](docs/en/algorithms/dpo.md) | [📄 Paper](https://arxiv.org/abs/2305.18290) | [🔗 HH-RLHF Example](examples/alignment/hhrlhf_dpo.yaml) | | **RLHF Reward Modeling** | - | - | [🔗 RLHF Example](examples/alignment/hhrlhf_rw.yaml) | diff --git a/docs/en/algorithms/grpo_series.md b/docs/en/algorithms/grpo_series.md index 0eb3dc1f8d..4943b530c3 100644 --- a/docs/en/algorithms/grpo_series.md +++ b/docs/en/algorithms/grpo_series.md @@ -18,6 +18,11 @@ training, including: - **SAPO**: [Paper](https://arxiv.org/abs/2511.20347) - **GSPO** (Qwen3): [Paper](https://arxiv.org/abs/2507.18071), [Blog](https://qwenlm.github.io/blog/gspo/) +- **IcePop**: [Blog](https://ringtech.notion.site/icepop) — Importance-ratio-based token masking (composable with other RL algorithms) +- **KPop**: [Blog](https://ringtech.notion.site/kpop) — Bidirectional binary KL divergence token masking (composable with other RL algorithms) + +IcePop and KPop are token masking strategies that can be composed with any RL algorithm +listed above. These algorithms share the same base objective but differ in their normalization strategies, clipping mechanisms, importance sampling levels, etc. By adjusting a few @@ -35,7 +40,7 @@ configuration YAML file. | **slurm** | `python3 examples/math/gsm8k_rl.py --config examples/math/gsm8k_.yaml scheduler.type=slurm` | Replace `` with: `ppo`, `grpo`, `drgrpo`, `liteppo`, `rloo`, `gspo`, -`dapo_dynamic_bs`, or `sapo`. +`dapo_dynamic_bs`, `sapo`, `icepop`, or `kpop`. ### Switching Algorithms via CLI Overrides @@ -148,8 +153,10 @@ parameters: | **LitePPO** | `group` | `batch` | `false` | `token` | - | | **RLOO** | `group` | `null` | `true` | `token` | - | | **GSPO** | `batch` | `batch` | `false` | `sequence` | - | -| **DAPO** | `batch` | `batch` | `false` | `token` | asymmetric clip, dynamic sampling | -| **SAPO** | `batch` | `batch` | `false` | `token` | `use_sapo_loss=true` | +| **DAPO** | `batch` | `batch` | `false` | `token` | asymmetric clip, dynamic sampling | +| **SAPO** | `batch` | `batch` | `false` | `token` | `use_sapo_loss=true` | +| **IcePop** | `batch` | `batch` | `false` | `token` | `rejection_sampling.metric=ratio` | +| **KPop** | `batch` | `batch` | `false` | `token` | `rejection_sampling.metric=binary_kl` | **Note**: The "GRPO" row reflects the original DeepSeekMath formulation. AReaL's default GRPO config uses these settings but with length normalization already removed (see AReaL @@ -298,6 +305,62 @@ and then filter them. The following option controls batch sizing behavior: | ------------ | ---- | ------- | --------------------------- | | `dynamic_bs` | bool | `false` | Enable dynamic batch sizing | +### IcePop + +IcePop masks tokens whose importance ratio $r_{i,t} = \frac{\pi_\theta(o_{i,t} \mid q, o_{i, \phi$ are masked (here $\phi$ corresponds to `actor.rejection_sampling.upper`). + +It is implemented via the `rejection_sampling` config with `metric=binary_kl`: + +```yaml +actor: + use_decoupled_loss: true + rejection_sampling: + level: token + action: mask + metric: binary_kl + upper: 2.0 +``` + +| Parameter | Type | Default | Description | +| ------------------------------------ | ----------- | ------- | ------------------------------------ | +| `actor.rejection_sampling.metric` | str | - | Set to `binary_kl` for KPop | +| `actor.rejection_sampling.upper` | float | `2.0` | KL divergence threshold ($\phi$) | + +**Note:** KPop only supports `action=mask` (not `clamp`), and `lower` is not used with `binary_kl`. KPop requires `actor.use_decoupled_loss=true`, otherwise `rejection_sampling` has no effect. + +See `examples/math/gsm8k_kpop.yaml` for a complete configuration example. + ## Core Concepts **Rewards**: AReaL assumes outcome-based rewards. Each trajectory, which may consist of diff --git a/docs/zh/algorithms/grpo_series.md b/docs/zh/algorithms/grpo_series.md index d882b8a6e8..a9cf12f5aa 100644 --- a/docs/zh/algorithms/grpo_series.md +++ b/docs/zh/algorithms/grpo_series.md @@ -17,6 +17,10 @@ - **SAPO**: [论文](https://arxiv.org/abs/2511.20347) - **GSPO** (Qwen3): [论文](https://arxiv.org/abs/2507.18071), [博客](https://qwenlm.github.io/blog/gspo/) +- **IcePop**:[博客](https://ringtech.notion.site/icepop) — 基于重要性比率的token掩码(可与其它RL算法组合使用) +- **KPop**:[博客](https://ringtech.notion.site/kpop) — 双向二元KL散度token掩码(可与其它RL算法组合使用) + +IcePop和KPop是token掩码策略,可以与上面列出的任意RL算法组合使用。 这些算法共享相同的基础目标,但在归一化策略、裁剪机制、重要性采样级别等方面有所不同。通过调整AReaL中的少量配置参数,你可以在不同算法之间切换。 @@ -30,7 +34,7 @@ | **ray** | `python3 examples/math/gsm8k_rl.py --config examples/math/gsm8k_.yaml scheduler.type=ray` | | **slurm** | `python3 examples/math/gsm8k_rl.py --config examples/math/gsm8k_.yaml scheduler.type=slurm` | -将 `` 替换为:`ppo`、`grpo`、`drgrpo`、`liteppo`、`rloo`、`gspo`、`dapo_dynamic_bs` 或 `sapo`。 +将 `` 替换为:`ppo`、`grpo`、`drgrpo`、`liteppo`、`rloo`、`gspo`、`dapo_dynamic_bs`、`sapo`、`icepop` 或 `kpop`。 ### 通过CLI覆盖切换算法 @@ -134,8 +138,10 @@ actor: | **LitePPO** | `group` | `batch` | `false` | `token` | - | | **RLOO** | `group` | `null` | `true` | `token` | - | | **GSPO** | `batch` | `batch` | `false` | `sequence` | - | -| **DAPO** | `batch` | `batch` | `false` | `token` | 非对称裁剪,动态采样 | -| **SAPO** | `batch` | `batch` | `false` | `token` | `use_sapo_loss=true` | +| **DAPO** | `batch` | `batch` | `false` | `token` | 非对称裁剪,动态采样 | +| **SAPO** | `batch` | `batch` | `false` | `token` | `use_sapo_loss=true` | +| **IcePop** | `batch` | `batch` | `false` | `token` | `rejection_sampling.metric=ratio` | +| **KPop** | `batch` | `batch` | `false` | `token` | `rejection_sampling.metric=binary_kl` | **注意**:"GRPO"行反映原始DeepSeekMath公式。AReaL的默认GRPO配置使用这些设置,但已移除长度归一化(见下文AReaL实现说明)。 @@ -271,6 +277,62 @@ trainer.train( | ------------ | ---- | ------- | ---------------- | | `dynamic_bs` | bool | `false` | 启用动态批量大小 | +### IcePop + +IcePop对重要性比率 $r_{i,t} = \frac{\pi_\theta(o_{i,t} \mid q, o_{i, \phi$ 的token被掩码(此处 $\phi$ 对应 `actor.rejection_sampling.upper`)。 + +通过 `rejection_sampling` 配置的 `metric=binary_kl` 实现: + +```yaml +actor: + use_decoupled_loss: true + rejection_sampling: + level: token + action: mask + metric: binary_kl + upper: 2.0 +``` + +| 参数 | 类型 | 默认值 | 描述 | +| ------------------------------------ | ----------- | ------ | ------------------------------ | +| `actor.rejection_sampling.metric` | str | - | 设置为 `binary_kl` 以启用KPop | +| `actor.rejection_sampling.upper` | float | `2.0` | KL散度阈值($\phi$) | + +**注意:** KPop仅支持 `action=mask`(不支持 `clamp`),且 `lower` 在 `binary_kl` 中不使用。KPop需要 `actor.use_decoupled_loss=true`,否则 `rejection_sampling` 不生效。 + +完整的配置示例见 `examples/math/gsm8k_kpop.yaml`。 + ## 核心概念 **奖励**:AReaL假设基于结果的奖励。每个可能由连接的LLM输入-输出对组成的轨迹,在序列级而非token级被分配一个标量奖励。 From 37645dd1077d7766c724baf218e6f171593ee0e5 Mon Sep 17 00:00:00 2001 From: Maxwill Lin <0312fs3@gmail.com> Date: Mon, 22 Jun 2026 20:05:34 -0700 Subject: [PATCH 05/36] fix(vllm): forward frequency_penalty and stop in generation requests (#1429) VLLMBackend.build_generation_request omitted gconfig.frequency_penalty and gconfig.stop, while the sibling SGLangBackend forwards both. Both are GenerationHyperparameters (and neither is in _OPENAI_UNSUPPORTED_ARGS), and vLLM's OpenAI-compatible /v1/completions and /v1/chat/completions endpoints accept them. As a result a user who set frequency_penalty (anti-repetition) or stop strings had them honored on SGLang but silently ignored on vLLM, changing the sampling distribution between backends. Co-authored-by: Claude Opus 4.8 --- areal/engine/vllm_remote.py | 3 +++ tests/test_vllm_generation_request.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/test_vllm_generation_request.py diff --git a/areal/engine/vllm_remote.py b/areal/engine/vllm_remote.py index c0c4a4a59e..6f70747150 100644 --- a/areal/engine/vllm_remote.py +++ b/areal/engine/vllm_remote.py @@ -58,11 +58,14 @@ def build_generation_request( "stop_token_ids": stop_token_ids, "ignore_eos": gconfig.ignore_eos, "skip_special_tokens": gconfig.skip_special_tokens, + "frequency_penalty": gconfig.frequency_penalty, "return_tokens_as_token_ids": True, "logprobs": 0, "use_beam_search": gconfig.use_beam_search, "stream": False, } + if gconfig.stop: + payload["stop"] = gconfig.stop if with_lora: lora_name = gconfig.lora_name diff --git a/tests/test_vllm_generation_request.py b/tests/test_vllm_generation_request.py new file mode 100644 index 0000000000..18a42d0ee8 --- /dev/null +++ b/tests/test_vllm_generation_request.py @@ -0,0 +1,20 @@ +from areal.api.cli_args import GenerationHyperparameters +from areal.api.io_struct import ModelRequest +from areal.engine.vllm_remote import VLLMBackend + + +def test_vllm_forwards_frequency_penalty_and_stop(): + """The vLLM backend must forward frequency_penalty and stop like the SGLang + backend does; both are GenerationHyperparameters and are accepted by vLLM's + OpenAI-compatible /v1/completions endpoint.""" + gconfig = GenerationHyperparameters( + max_new_tokens=8, frequency_penalty=0.5, stop=["STOP"] + ) + req = ModelRequest(input_ids=[11, 12], gconfig=gconfig) + + payload = ( + VLLMBackend().build_generation_request(req, with_lora=False, version=0).payload + ) + + assert payload["frequency_penalty"] == 0.5 + assert payload["stop"] == ["STOP"] From f19e00acae400cc4dfa122768df1c6cecfbc6f0c Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:39:39 +0800 Subject: [PATCH 06/36] fix(awex): allow disabling batch_send_recv use_group via AWEX_WU_USE_GROUP (#1414) On some hardware/driver combos (e.g. certain Ascend variants), torch.distributed.batch_isend_irecv hangs during weight update. Add AWEX_WU_USE_GROUP env var so callers can fall back to per-op send/recv. Usage: defaults to 1 (use_group=True). Set AWEX_WU_USE_GROUP=0 to bypass the group path. Applied at the three adapter call sites: fsdp/megatron/sglang execute_weight_update(). Co-authored-by: Claude Opus 4.7 --- areal/experimental/weight_update/awex/__init__.py | 11 +++++++++++ .../experimental/weight_update/awex/fsdp_adapter.py | 12 ++++++++++-- .../weight_update/awex/megatron_adapter.py | 12 ++++++++++-- .../weight_update/awex/sglang_adapter.py | 12 ++++++++++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/areal/experimental/weight_update/awex/__init__.py b/areal/experimental/weight_update/awex/__init__.py index 4c0a78b1b4..e48c3ee343 100644 --- a/areal/experimental/weight_update/awex/__init__.py +++ b/areal/experimental/weight_update/awex/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import os from typing import Any import aiohttp # pyright: ignore[reportMissingImports] @@ -13,6 +14,16 @@ logger = logging.getLogger("AwexHTTP") +def awex_wu_use_group() -> bool: + """Resolve whether ``batch_send_recv`` should use ``use_group=True``. + + Why: on some hardware/driver combinations, ``torch.distributed.batch_isend_irecv`` + can hang during weight update. ``AWEX_WU_USE_GROUP=0`` lets the caller fall back + to per-op send/recv to bypass the hang. Defaults to ``1`` (True). + """ + return bool(int(os.getenv("AWEX_WU_USE_GROUP", "1"))) + + async def _fetch_kv_metadata( kv_store_url: str, pair_name: str, diff --git a/areal/experimental/weight_update/awex/fsdp_adapter.py b/areal/experimental/weight_update/awex/fsdp_adapter.py index 9c061bacb8..c5a3a987b0 100644 --- a/areal/experimental/weight_update/awex/fsdp_adapter.py +++ b/areal/experimental/weight_update/awex/fsdp_adapter.py @@ -19,7 +19,10 @@ from torch.distributed.tensor.placement_types import Shard from areal.engine.core.model import is_qwen_vl_model -from areal.experimental.weight_update.awex import fetch_kv_metadata +from areal.experimental.weight_update.awex import ( + awex_wu_use_group, + fetch_kv_metadata, +) from areal.experimental.weight_update.nccl_group import ( init_weights_update_group, setup_batch_isend_irecv, @@ -174,7 +177,12 @@ def execute_weight_update(self, version: int) -> None: self._weights_update_group, copy_rank=self._transfer_rank, ) - batch_send_recv(send_ops=send_ops, recv_ops=[], blocking=True) + batch_send_recv( + send_ops=send_ops, + recv_ops=[], + blocking=True, + use_group=awex_wu_use_group(), + ) torch.distributed.barrier(group=self._weights_update_group) def batch_isend_irecv(self, **kwargs) -> None: diff --git a/areal/experimental/weight_update/awex/megatron_adapter.py b/areal/experimental/weight_update/awex/megatron_adapter.py index 6002a7fa8b..0fec179877 100644 --- a/areal/experimental/weight_update/awex/megatron_adapter.py +++ b/areal/experimental/weight_update/awex/megatron_adapter.py @@ -24,7 +24,10 @@ group_tensors_by_shape_and_dtype, ) -from areal.experimental.weight_update.awex import fetch_kv_metadata +from areal.experimental.weight_update.awex import ( + awex_wu_use_group, + fetch_kv_metadata, +) from areal.experimental.weight_update.nccl_group import ( init_weights_update_group, setup_batch_isend_irecv, @@ -198,7 +201,12 @@ def execute_weight_update(self, version: int) -> None: self._weights_update_group, copy_rank=self._transfer_rank, ) - batch_send_recv(send_ops=send_ops, recv_ops=[], blocking=True) + batch_send_recv( + send_ops=send_ops, + recv_ops=[], + blocking=True, + use_group=awex_wu_use_group(), + ) dist.barrier(group=self._weights_update_group) def batch_isend_irecv(self, **kwargs) -> None: diff --git a/areal/experimental/weight_update/awex/sglang_adapter.py b/areal/experimental/weight_update/awex/sglang_adapter.py index 6194031806..855fe9c5ef 100644 --- a/areal/experimental/weight_update/awex/sglang_adapter.py +++ b/areal/experimental/weight_update/awex/sglang_adapter.py @@ -30,7 +30,10 @@ reconstruct_tensors_from_groups, ) -from areal.experimental.weight_update.awex import fetch_kv_metadata +from areal.experimental.weight_update.awex import ( + awex_wu_use_group, + fetch_kv_metadata, +) from areal.experimental.weight_update.inference_adapter import ( AwexInferenceAdapter, ) @@ -409,7 +412,12 @@ def execute_weight_update(self, version: int) -> None: self._transfer_plan, self._weights_update_group, ) - batch_send_recv(send_ops=[], recv_ops=recv_ops, blocking=True) + batch_send_recv( + send_ops=[], + recv_ops=recv_ops, + blocking=True, + use_group=awex_wu_use_group(), + ) for original, contiguous in non_contiguous_pairs: original.copy_(contiguous) From e9756803b9597fbe45fe8647ac9935dee5b55daf Mon Sep 17 00:00:00 2001 From: HT-Yuan <70688525+HT-Yuan@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:13:51 +0800 Subject: [PATCH 07/36] feat: disable megatron grad buffers CPU backup to save host memory (#1393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: disable megatron grad buffers CPU backup to save host memory Add disable_grad_buffers_cpu_backup option to MegatronEngineConfig to skip CPU backup for gradient buffers during offload in colocated training. Gradient buffers are recomputed each training step, so they don't need to be backed up to CPU memory during offload. This saves host memory (∼5.9GB for typical configs) and reduces offload/onload overhead. - Add config flag disable_grad_buffers_cpu_backup (default: False) - Call DDP.offload_grad_buffers() before TMS pause to release grad storage - Call DDP.restore_grad_buffers() after TMS resume to reallocate and zero Tested: numerical equivalence confirmed, host memory saved ≈ grad buffer size * docs: add disable_grad_buffers_cpu_backup to cli_reference * fix: disable synchronize/empty_cache in offload/restore_grad_buffers --- areal/api/cli_args.py | 10 ++++++++++ areal/engine/megatron_engine.py | 18 ++++++++++++++++++ docs/en/cli_reference.md | 1 + docs/zh/cli_reference.md | 1 + 4 files changed, 30 insertions(+) diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 82d3a485f0..1713695962 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -951,6 +951,16 @@ class MegatronEngineConfig: }, ) + disable_grad_buffers_cpu_backup: bool = field( + default=False, + metadata={ + "help": ( + "When offloading with torch_memory_saver, skip CPU backup for " + "Megatron gradient buffers (they are recomputed each step). " + ) + }, + ) + class SchedulingStrategyType(str, Enum): separation = "separation" diff --git a/areal/engine/megatron_engine.py b/areal/engine/megatron_engine.py index 0713683dee..8bff38bd1a 100644 --- a/areal/engine/megatron_engine.py +++ b/areal/engine/megatron_engine.py @@ -1122,6 +1122,17 @@ def offload(self) -> None: ) self.get_device_stats().log("before offload model") + + # Discard gradient buffers via Megatron's native API *before* TMS pause. + # `DDP.offload_grad_buffers()` releases grad storage in-place + # (storage().resize_(0)); views like param.main_grad recover automatically + # on restore. Since grads are recomputed every step, they need no CPU + # backup. Doing this before pause() means the grad region has no physical + # memory left for TMS to back up. + if self.mcore_config.disable_grad_buffers_cpu_backup: + for m in self.model: + m.offload_grad_buffers(synchronize=False, empty_cache=False) + current_platform.clear_memory() torch_memory_saver.pause() @@ -1139,6 +1150,13 @@ def onload(self) -> None: """ torch_memory_saver.resume() + + # Reallocate gradient buffers released in offload(). resize_() restores + # storage and zeroes it; param.main_grad views become valid again. + if self.mcore_config.disable_grad_buffers_cpu_backup: + for m in self.model: + m.restore_grad_buffers(synchronize=False) + current_platform.clear_memory() # TODO: NCCL onload diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index ded31439f1..7ba51ea468 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -1100,6 +1100,7 @@ Refer to Megatron-LM documentation for implementation details. | `bridge_type` | string | `"mbridge"` | Bridge backend for MegatronEngine. Choices: 'mbridge' or 'megatron-bridge'. **Choices:** `mbridge`, `megatron-bridge` | | `use_mbridge_save` | boolean | `False` | Use mbridge's save method to save gpu memory when saving weights. | | `use_bridge_for_update_weights` | boolean | `False` | When True and bridge_type='megatron-bridge', delegate live weight sync to bridge.export_hf_weights instead of the hand-rolled convert_to_hf registry. Required for models without a registry entry (e.g. Qwen3.5). FP8 paths fall back to the registry automatically. | +| `disable_grad_buffers_cpu_backup` | boolean | `False` | When offloading with torch_memory_saver, skip CPU backup for Megatron gradient buffers (they are recomputed each step). | (section-memory-profiler)= diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index 81db1197cf..f351abb85d 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -1098,6 +1098,7 @@ Refer to Megatron-LM documentation for implementation details. | `bridge_type` | string | `"mbridge"` | Bridge backend for MegatronEngine. Choices: 'mbridge' or 'megatron-bridge'. **Choices:** `mbridge`, `megatron-bridge` | | `use_mbridge_save` | boolean | `False` | Use mbridge's save method to save gpu memory when saving weights. | | `use_bridge_for_update_weights` | boolean | `False` | When True and bridge_type='megatron-bridge', delegate live weight sync to bridge.export_hf_weights instead of the hand-rolled convert_to_hf registry. Required for models without a registry entry (e.g. Qwen3.5). FP8 paths fall back to the registry automatically. | +| `disable_grad_buffers_cpu_backup` | boolean | `False` | When offloading with torch_memory_saver, skip CPU backup for Megatron gradient buffers (they are recomputed each step). | (section-memory-profiler)= From b89271e6fa4ff1911ca6a863934cad3c33c295a2 Mon Sep 17 00:00:00 2001 From: Maxwill Lin <0312fs3@gmail.com> Date: Tue, 23 Jun 2026 00:20:50 -0700 Subject: [PATCH 08/36] fix: only subtract in-range excluded ports in find_free_ports (#1436) available_range subtracted the full exclude_ports count regardless of whether those ports lay within [min_port, max_port]. Excluded ports outside the range deflated the availability count and could raise a spurious ValueError even when every in-range port was free. Intersect exclude_ports with the range before subtracting. Co-authored-by: Claude Opus 4.8 --- areal/utils/network.py | 6 ++++-- tests/test_network_ipv6_utils.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/areal/utils/network.py b/areal/utils/network.py index 3481016039..8cf9f5955e 100644 --- a/areal/utils/network.py +++ b/areal/utils/network.py @@ -139,8 +139,10 @@ def find_free_ports( free_ports = [] attempted_ports = set() - # Calculate available port range - available_range = max_port - min_port + 1 - len(exclude_ports) + # Calculate available port range. Only excluded ports that fall within + # [min_port, max_port] reduce availability; out-of-range entries do not. + in_range_excluded = sum(min_port <= p <= max_port for p in exclude_ports) + available_range = max_port - min_port + 1 - in_range_excluded if count > available_range: raise ValueError( diff --git a/tests/test_network_ipv6_utils.py b/tests/test_network_ipv6_utils.py index 273105d957..235ceba311 100644 --- a/tests/test_network_ipv6_utils.py +++ b/tests/test_network_ipv6_utils.py @@ -86,3 +86,13 @@ def test_get_loopback_ip_returns_bindable_address(): network = _load_network_module() ip = network.get_loopback_ip() assert ip in {"127.0.0.1", "::1"} + + +def test_find_free_ports_ignores_out_of_range_excludes(): + # Excluded ports outside [min_port, max_port] must not deflate the + # availability check; all in-range ports here are free. + network = _load_network_module() + ports = network.find_free_ports( + count=6, port_range=(10000, 10005), exclude_ports={50000} + ) + assert len(ports) == 6 From bd333e6a4002bc5a8f0639d3104ebd3705405e6e Mon Sep 17 00:00:00 2001 From: Maxwill Lin <0312fs3@gmail.com> Date: Tue, 23 Jun 2026 00:21:18 -0700 Subject: [PATCH 09/36] fix(reward): guard clevr_count_70k_reward_fn against scoring failures (#1430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(reward): score non-string answers in clevr_count_70k_reward_fn clevr_count_70k_reward_fn did not str()-coerce its inputs or guard against errors, unlike the sibling reward fns (gsm8k, geometry3k). A non-string answer (e.g. an int) made ans.strip() raise AttributeError, which WorkflowExecutor catches and uses to reject the whole trajectory instead of scoring the sample — and even a matching completion was lost rather than scored 1.0. Coerce completions and answer to str and wrap the body in try/except, matching the sibling reward fns. Co-Authored-By: Claude Opus 4.8 * fix(example): apply the same guard to the clevr GRPO example reward fn The example defines its own clevr_count_70k_reward_fn (referenced via workflow_kwargs) that still had the old logic. Mirror the built-in fix: str-coerce completions/answer and guard, so a non-string answer is scored rather than raising and dropping the trajectory. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- areal/reward/clevr_count_70k.py | 24 +++++++++++++----------- examples/vlm/clevr_count_70k_grpo.py | 19 ++++++++++++------- tests/test_clevr_reward.py | 15 +++++++++++++++ 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 tests/test_clevr_reward.py diff --git a/areal/reward/clevr_count_70k.py b/areal/reward/clevr_count_70k.py index 12f63419c6..1d6ba7bfe1 100644 --- a/areal/reward/clevr_count_70k.py +++ b/areal/reward/clevr_count_70k.py @@ -18,15 +18,17 @@ def extract_answer(pred_str, data_name, use_last_number=True): def clevr_count_70k_reward_fn( prompt, completions, prompt_ids, completion_ids, answer, **kwargs ) -> float: - sol = extract_answer(completions, data_name="") # str number - ans = answer - - if sol is None: - return 0.0 - if ans is None: + try: + sol = extract_answer(str(completions), data_name="") # str number + ans = str(answer) + + if not sol or not ans: + return 0.0 + + is_correct = sol.strip() == ans.strip() + if is_correct: + logger.info(f"completions: {completions}, answer: {answer}") + return float(is_correct) + except Exception: + logger.warning("Exception in clevr_count_70k_reward_fn", exc_info=True) return 0.0 - - is_correct = sol.strip() == ans.strip() - if is_correct: - logger.info(f"completions: {completions}, answer: {answer}") - return float(is_correct) diff --git a/examples/vlm/clevr_count_70k_grpo.py b/examples/vlm/clevr_count_70k_grpo.py index 8cab44e8ff..2838dc3da4 100644 --- a/examples/vlm/clevr_count_70k_grpo.py +++ b/examples/vlm/clevr_count_70k_grpo.py @@ -4,8 +4,11 @@ from areal import PPOTrainer from areal.api.cli_args import GRPOConfig, load_expr_config from areal.dataset import get_custom_dataset +from areal.utils import logging from areal.utils.hf_utils import load_hf_processor_and_tokenizer +logger = logging.getLogger("clevr_count_70k_grpo") + def extract_answer(pred_str, data_name, use_last_number=True): match = re.findall(r"\[([0-9\.]+)\]", pred_str) @@ -18,15 +21,17 @@ def extract_answer(pred_str, data_name, use_last_number=True): def clevr_count_70k_reward_fn( prompt, completions, prompt_ids, completion_ids, answer, **kwargs ): - sol = extract_answer(completions, data_name="") # str number - ans = answer + try: + sol = extract_answer(str(completions), data_name="") # str number + ans = str(answer) - if sol is None: - return 0 - if ans is None: - return 0 + if not sol or not ans: + return 0.0 - return float(sol.strip() == ans.strip()) + return float(sol.strip() == ans.strip()) + except Exception: + logger.warning("Exception in clevr_count_70k_reward_fn", exc_info=True) + return 0.0 def main(args): diff --git a/tests/test_clevr_reward.py b/tests/test_clevr_reward.py new file mode 100644 index 0000000000..9d9c0b4d8d --- /dev/null +++ b/tests/test_clevr_reward.py @@ -0,0 +1,15 @@ +from areal.reward import clevr_count_70k_reward_fn + + +def test_clevr_non_string_answer_scored_correctly(): + """A non-string answer (e.g. an int) must be coerced and scored, not crash. + The sibling reward fns str()-coerce both inputs; clevr did not, so a matching + completion scored as a dropped trajectory instead of 1.0.""" + reward = clevr_count_70k_reward_fn( + prompt="", + completions="[3]", + prompt_ids=[], + completion_ids=[], + answer=3, + ) + assert reward == 1.0 From 4be0c6416f2ea5e83e7e2bc01b1ff39e3b37c598 Mon Sep 17 00:00:00 2001 From: Rongzhi Gu <51925155+Adiactive@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:41:32 -0700 Subject: [PATCH 10/36] feat(megatron): make MTP head opt-in to support Qwen3.6 MoE RL (#1403) * feat(megatron): make MTP head opt-in to support Qwen3.6 MoE RL Qwen3.6 MoE checkpoints store their MTP (multi-token-prediction) experts in a fused layout that megatron-bridge 0.4.x cannot export, so update_weights crashes with "Object must exist on at least one PP rank" at step 1. MTP is an inference-only head, not part of the RL objective and unused by the rollout, so it should not be built for RL training. Add MegatronEngineConfig.enable_mtp (default False). In make_mcore_model (megatron-bridge path), drop MTP (provider.mtp_num_layers=None) when the model has one and enable_mtp is False; raise if enable_mtp is True but the model has no MTP head. Set True for SFT or bridge builds that support the model's MTP format. Refs: #1398 * docs(examples): add Qwen3.6-35B-A3B megatron geometry3k GRPO config GRPO recipe for Qwen3.6-35B-A3B on geometry3k with the megatron actor (megatron-bridge) + vLLM rollout, demonstrating the new megatron.enable_mtp=False switch (MTP dropped for RL). Refs: #1398 * docs(megatron): refine enable_mtp help + Qwen3.6 example (review) Address PR #1403 review: - Scope the enable_mtp help to bridge_type=megatron-bridge (the flag is a no-op on the mbridge/registry paths) and drop the unwired SFT claim. - Add PYTORCH_ALLOC_CONF=expandable_segments to the Qwen3.6 example to avoid 35B-MoE OOM. Refs: #1398 * fix(engine): keep non-MTP weights in HF export when MTP head is dropped With enable_mtp=False the bridge export yields no mtp.* tensors, and megatron-bridge's save_generator with strict=True silently skips every source shard containing an MTP key -- discarding the non-MTP weights packed in those shards (Qwen3.6-35B lost lm_head + 2 layers; 27B lost ~16 layers) while rebuilding a consistent-looking index. Pass strict=False to save_hf_pretrained when the MTP head was dropped so incomplete shards are written with all present keys (only mtp.* is absent, as intended), zero the MTP layer counts in the exported config.json so external loaders do not fabricate an MTP head over missing weights, and rebuild the safetensors index from the shard files' actual contents (the bridge's strict=False path leaves ghost mtp.* entries and a stale metadata.total_size). Refs: #1398 --- areal/api/cli_args.py | 8 + areal/engine/megatron_engine.py | 95 +++++++++ areal/models/mcore/registry.py | 14 ++ docs/en/cli_reference.md | 1 + docs/zh/cli_reference.md | 1 + ...n3_6_35b_a3b_megatron_geometry3k_grpo.yaml | 186 ++++++++++++++++++ 6 files changed, 305 insertions(+) create mode 100644 examples/vlm/qwen3_6_35b_a3b_megatron_geometry3k_grpo.yaml diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 1713695962..9a3c689521 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -961,6 +961,14 @@ class MegatronEngineConfig: }, ) + enable_mtp: bool = field( + default=False, + metadata={ + "help": "Keep the model's Multi-Token-Prediction (MTP) head " + "(bridge_type=megatron-bridge only). Default False drops it.", + }, + ) + class SchedulingStrategyType(str, Enum): separation = "separation" diff --git a/areal/engine/megatron_engine.py b/areal/engine/megatron_engine.py index 8bff38bd1a..9b6619e392 100644 --- a/areal/engine/megatron_engine.py +++ b/areal/engine/megatron_engine.py @@ -5,9 +5,11 @@ import dataclasses import functools import gc +import json import math import os import re +import struct from collections.abc import Callable, Iterator from concurrent.futures import Future from contextlib import contextmanager @@ -1972,10 +1974,17 @@ def _save_model_to_hf( base_model_name_or_path=base_model_path or self.config.path, ) else: + # When the MTP head was dropped (enable_mtp=False), the export + # yields no mtp.* tensors and strict=True would silently skip + # every source shard containing an MTP key -- discarding the + # non-MTP weights packed in those shards (e.g. lm_head). + # strict=False writes such shards with all present keys, so the + # export loses only the intentionally dropped MTP weights. self.bridge.save_hf_pretrained( self.model, path, source_path=base_model_path, + strict=not self._mtp_head_dropped, ) else: if self.mcore_config.use_mbridge_save: @@ -2007,10 +2016,96 @@ def _save_model_to_hf( tokenizer.save_pretrained(path) if processor is not None: processor.save_pretrained(path) + if self._mtp_head_dropped: + self._scrub_mtp_from_saved_config(path) + self._rebuild_index_from_saved_shards(path) current_platform.synchronize() dist.barrier(group=self.cpu_group) + @property + def _mtp_head_dropped(self) -> bool: + """True when the model declares an MTP head but enable_mtp left it unbuilt.""" + if self.bridge_cls != "megatron-bridge" or self.mcore_config.enable_mtp: + return False + text_config = getattr(self.hf_config, "text_config", self.hf_config) + return any( + bool(getattr(text_config, key, 0)) + for key in ("mtp_num_hidden_layers", "num_nextn_predict_layers") + ) + + def _scrub_mtp_from_saved_config(self, path: str) -> None: + """Zero MTP layer counts in an exported config.json so it matches the + MTP-stripped weights (the head is dropped when enable_mtp=False).""" + cfg_path = os.path.join(path, "config.json") + if not os.path.exists(cfg_path): + return + with open(cfg_path) as f: + cfg = json.load(f) + + def _walk(node: Any) -> bool: + changed = False + if isinstance(node, dict): + for key, value in node.items(): + if ( + key in ("mtp_num_hidden_layers", "num_nextn_predict_layers") + and value + ): + node[key] = 0 + changed = True + else: + changed |= _walk(value) + elif isinstance(node, list): + for item in node: + changed |= _walk(item) + return changed + + if _walk(cfg): + with open(cfg_path, "w") as f: + json.dump(cfg, f, indent=2, sort_keys=True) + self.logger.info( + "Exported checkpoint is MTP-stripped (enable_mtp=False); " + "zeroed MTP layer counts in %s.", + cfg_path, + ) + + def _rebuild_index_from_saved_shards(self, path: str) -> None: + """Rewrite model.safetensors.index.json from the shard files' actual + contents. megatron-bridge's strict=False save marks each shard's full + expected key set as saved, leaving ghost entries for the dropped mtp.* + tensors and a stale metadata.total_size.""" + index_path = os.path.join(path, "model.safetensors.index.json") + if not os.path.exists(index_path): + return + weight_map: dict[str, str] = {} + total_size = 0 + for filename in sorted(os.listdir(path)): + if not filename.endswith(".safetensors"): + continue + with open(os.path.join(path, filename), "rb") as f: + header_len = struct.unpack(" None: assert self.model is not None, "Model is not initialized." diff --git a/areal/models/mcore/registry.py b/areal/models/mcore/registry.py index 08cd0a11b2..aada02407d 100644 --- a/areal/models/mcore/registry.py +++ b/areal/models/mcore/registry.py @@ -226,6 +226,20 @@ def make_mcore_model( provider.account_for_embedding_in_pipeline_split = False provider.account_for_loss_in_pipeline_split = False + has_mtp = bool(getattr(provider, "mtp_num_layers", None)) + if mcore_config.enable_mtp: + if not has_mtp: + raise ValueError( + "megatron.enable_mtp=True but the model has no MTP layers." + ) + elif has_mtp: + logger.warning( + "Dropping MTP head (mtp_num_layers=%s -> None); not used in RL and not " + "exportable for Qwen3.6. Set megatron.enable_mtp=True to keep it.", + provider.mtp_num_layers, + ) + provider.mtp_num_layers = None + # LoRA params are injected after model materialization and do not carry # Megatron main_grad buffers required by fused grad accumulation kernels. if use_lora: diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index 7ba51ea468..a137c15c10 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -1101,6 +1101,7 @@ Refer to Megatron-LM documentation for implementation details. | `use_mbridge_save` | boolean | `False` | Use mbridge's save method to save gpu memory when saving weights. | | `use_bridge_for_update_weights` | boolean | `False` | When True and bridge_type='megatron-bridge', delegate live weight sync to bridge.export_hf_weights instead of the hand-rolled convert_to_hf registry. Required for models without a registry entry (e.g. Qwen3.5). FP8 paths fall back to the registry automatically. | | `disable_grad_buffers_cpu_backup` | boolean | `False` | When offloading with torch_memory_saver, skip CPU backup for Megatron gradient buffers (they are recomputed each step). | +| `enable_mtp` | boolean | `False` | Keep the model's Multi-Token-Prediction (MTP) head (bridge_type=megatron-bridge only). Default False drops it. | (section-memory-profiler)= diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index f351abb85d..29c6a5745f 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -1099,6 +1099,7 @@ Refer to Megatron-LM documentation for implementation details. | `use_mbridge_save` | boolean | `False` | Use mbridge's save method to save gpu memory when saving weights. | | `use_bridge_for_update_weights` | boolean | `False` | When True and bridge_type='megatron-bridge', delegate live weight sync to bridge.export_hf_weights instead of the hand-rolled convert_to_hf registry. Required for models without a registry entry (e.g. Qwen3.5). FP8 paths fall back to the registry automatically. | | `disable_grad_buffers_cpu_backup` | boolean | `False` | When offloading with torch_memory_saver, skip CPU backup for Megatron gradient buffers (they are recomputed each step). | +| `enable_mtp` | boolean | `False` | Keep the model's Multi-Token-Prediction (MTP) head (bridge_type=megatron-bridge only). Default False drops it. | (section-memory-profiler)= diff --git a/examples/vlm/qwen3_6_35b_a3b_megatron_geometry3k_grpo.yaml b/examples/vlm/qwen3_6_35b_a3b_megatron_geometry3k_grpo.yaml new file mode 100644 index 0000000000..f6ceca39b3 --- /dev/null +++ b/examples/vlm/qwen3_6_35b_a3b_megatron_geometry3k_grpo.yaml @@ -0,0 +1,186 @@ +experiment_name: geometry3k-grpo +trial_name: trial1 + +seed: 1 +enable_offload: false +total_train_epochs: 10 +tokenizer_path: ${actor.path} + +cluster: + n_nodes: 6 + n_gpus_per_node: 8 + fileroot: /tmp/areal/experiments + name_resolve: + type: nfs + nfs_record_root: /tmp/areal/name_resolve + +scheduler: + type: slurm + +rollout: + backend: "vllm:d8p1t2" + experiment_name: ${experiment_name} + trial_name: ${trial_name} + max_concurrent_rollouts: 256 + queue_size: null + consumer_batch_size: ${train_dataset.batch_size} + max_head_offpolicyness: 4 + enable_rollout_tracing: false + scheduling_spec: ${actor.scheduling_spec} + fileroot: ${cluster.fileroot} + tokenizer_path: ${tokenizer_path} + dump_to_file: true + +gconfig: + n_samples: 4 + min_new_tokens: 0 + max_new_tokens: 2048 + greedy: false + temperature: 1.0 + +actor: + backend: "megatron:(attn:d8p2t2|ffn:d4p2t1e4)" + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: Qwen/Qwen3.6-35B-A3B + init_from_scratch: false + disable_dropout: true + gradient_checkpointing: true + dtype: bfloat16 + mb_spec: + max_tokens_per_mb: 8192 + optimizer: + type: adam + lr: 1e-6 + weight_decay: 0.01 + beta1: 0.9 + beta2: 0.999 + eps: 1e-8 + lr_scheduler_type: constant + gradient_clipping: 1.0 + warmup_steps_proportion: 0.001 + eps_clip: 0.4 + temperature: ${gconfig.temperature} + reward_scaling: 10.0 + reward_bias: -0.5 + kl_ctl: 0.0 + ppo_n_minibatches: 1 + recompute_logprob: true + use_decoupled_loss: true + rejection_sampling: + metric: ratio + upper: 5.0 + reward_norm: + mean_level: group + std_level: group + group_size: ${gconfig.n_samples} + adv_norm: + mean_level: batch + std_level: batch + megatron: + bridge_type: megatron-bridge + use_bridge_for_update_weights: true + enable_mtp: false + max_new_tokens: ${gconfig.max_new_tokens} + scheduling_spec: + - task_type: worker + port_count: 2 + gpu: 1 + mem: 32 + cmd: python3 -m areal.infra.rpc.rpc_server + env_vars: { PYTORCH_ALLOC_CONF: "expandable_segments:True" } + +ref: + backend: ${actor.backend} + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: ${actor.path} + init_from_scratch: false + disable_dropout: true + dtype: ${actor.dtype} + mb_spec: + max_tokens_per_mb: 4096 + optimizer: null + scheduling_strategy: + type: colocation + target: actor + scheduling_spec: ${actor.scheduling_spec} + +# SGLang +sglang: + model_path: ${actor.path} + random_seed: ${seed} + skip_tokenizer_init: true + dtype: ${actor.dtype} + max_running_requests: 64 + context_length: 32768 + mem_fraction_static: 0.8 + enable_multimodal: true + +vllm: + model: ${actor.path} + seed: ${seed} + skip_tokenizer_init: false + dtype: ${actor.dtype} + max_model_len: 32768 + gpu_memory_utilization: 0.9 + disable_sliding_window: false + enforce_eager: true + gdn_prefill_backend: triton + +# datasets +train_dataset: + batch_size: 128 + shuffle: true + pin_memory: true + num_workers: 4 + path: hiyouga/geometry3k + type: rl + +valid_dataset: + batch_size: 128 + pin_memory: true + num_workers: 4 + path: hiyouga/geometry3k + type: rl + +# Utilities +saver: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +recover: + mode: disabled + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: 3600 + +evaluator: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +stats_logger: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + wandb: + mode: disabled + +perf_tracer: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + enabled: false + session_tracer: + enabled: false From d97d3ae5959afd53bf942b8cd7119216feac2a89 Mon Sep 17 00:00:00 2001 From: Maxwill Lin <0312fs3@gmail.com> Date: Tue, 23 Jun 2026 05:49:14 -0700 Subject: [PATCH 11/36] feat(ppo): add CISPO loss surrogate (MiniMax-M1) (#1412) * feat(ppo): add CISPO advantage estimator (MiniMax-M1) PPO/GRPO clipping zeroes the gradient of any token whose importance-sampling ratio leaves the clip band: `min(r*A, clip(r)*A)` is constant in theta there. MiniMax-M1 (https://arxiv.org/abs/2506.13585, Eq. 4-5) observes those are disproportionately the low-probability "fork" tokens (`However`, `Wait`, ...) that steer reasoning -- exactly the tokens reasoning RL needs gradient on -- and instead clips the IS *weight* under stop-gradient while keeping the gradient on every token's `log pi_theta`. ScaleRL (arXiv:2510.13786 Eq. 4) adopts the same surrogate. AReaL had grpo/gspo/ppo/sapo but no CISPO. Per token, opt-in via `actor.use_cispo_loss=true`: ratio = exp(logprobs - proximal_logprobs) ratio_clipped = clip(ratio, 1 - eps_clip, 1 + eps_clip_higher) # stop-grad pg_loss = -sg(ratio_clipped) * advantages * logprobs Advantages are never clipped. The clip reuses the existing delta-from-1 `eps_clip` / `eps_clip_higher` plumbing (same convention as GSPO); CISPO is canonically single-sided, so the recommended setting is `eps_clip=1.0` (lower bound 0) with `eps_clip_higher=4.0` for the wide MiniMax-M1 range. Shape of the change -- additive, fits AReaL's existing estimator seam: - `functional.py`: `cispo_loss_fn` (+ export), reusing the token-mean reduction and the PPO-compatible stat schema so the existing clip-stat logging path applies unchanged; `clip_mask` reports band-exit on either side (under CISPO no clip zeroes the loss, so band-exit -- not loss-affecting clip -- is the meaningful metric). - `actor.py`: `grpo_loss_fn` dispatches to CISPO before SAPO/PPO. - `cli_args.py`: `PPOActorConfig.use_cispo_loss` with `__post_init__` validation (mutually exclusive with SAPO, requires `eps_clip_higher > 0`, token-level importance sampling only) + regenerated CLI docs (en/zh). Defaults unchanged (`use_cispo_loss=False`) -> byte-identical to before. `tests/test_cispo_loss.py` pins the two defining invariants, each across a PPO-like band (0.2/0.28) and the wide MiniMax band (1.0/4.0): the closed-form surrogate value + clip indices, and gradient routing (`logprobs.grad == -sg(clip(ratio)) * A / N`, zero gradient through the IS-ratio path -- mutation-verified to fail if the stop-gradient detach is dropped), plus config validation. Co-Authored-By: Claude Opus 4.8 * feat(ppo): support decoupled loss for CISPO via rejection sampling The CISPO branch ignored use_decoupled_loss / rejection_sampling, silently dropping the off-policy correction in exactly the async/stale regime CISPO targets. CISPO already anchors its clipped IS ratio at pi_proximal; thread the behavior logp + rejection_sampling through cispo_loss_fn and rescale each token's surrogate by the detached pi_proximal/pi_behave weight, mirroring ppo_actor_loss_fn. Both factors are stop-gradient, so the estimator stays -sg(behave_imp_weight * ratio_clipped) * A * grad(log pi_theta). Addresses the maintainer review on the dispatch bypassing the decoupled check. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: EazyReal <8047065+EazyReal@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 --- areal/api/cli_args.py | 31 +++++ areal/trainer/ppo/actor.py | 29 ++++- areal/utils/functional/__init__.py | 2 + areal/utils/functional/functional.py | 118 ++++++++++++++++++ docs/en/cli_reference.md | 1 + docs/zh/cli_reference.md | 1 + tests/test_cispo_loss.py | 173 +++++++++++++++++++++++++++ 7 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 tests/test_cispo_loss.py diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 9a3c689521..21e51e255b 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -1565,6 +1565,18 @@ class PPOActorConfig(TrainEngineConfig): metadata={"help": "SAPO temperature for negative advantages"}, ) + # CISPO (Clipped IS-weight Policy Optimization) - MiniMax-M1 https://arxiv.org/abs/2506.13585 + use_cispo_loss: bool = field( + default=False, + metadata={ + "help": "Use CISPO loss: clip the importance-sampling weight under " + "stop-gradient and keep gradient on every token's log pi (MiniMax-M1 " + "Eq. 4-5). Mutually exclusive with SAPO. Token-level only. Requires " + "eps_clip_higher > 0; recommended eps_clip=1.0 (single-sided, lower " + "bound 0) with eps_clip_higher=4.0." + }, + ) + # Asynchronous RL recompute_logprob: bool = field( default=False, @@ -1672,6 +1684,25 @@ def __post_init__(self): "Please set `actor.use_decoupled_loss=false` in your configuration." ) + # Validate CISPO configuration + if self.use_cispo_loss: + if self.use_sapo_loss: + raise ValueError( + "CISPO and SAPO are mutually exclusive surrogates. " + "Set at most one of use_cispo_loss / use_sapo_loss." + ) + if self.eps_clip_higher is None or self.eps_clip_higher <= 0: + raise ValueError( + "CISPO requires a positive eps_clip_higher (the asymmetric " + "upper clip is its defining knob, MiniMax-M1 Eq. 4-5). Got " + f"eps_clip_higher={self.eps_clip_higher}." + ) + if self.importance_sampling_level != "token": + raise ValueError( + "CISPO only supports importance_sampling_level='token'. " + "Sequence-level (GSPO-style) CISPO has no published surrogate." + ) + super().__post_init__() diff --git a/areal/trainer/ppo/actor.py b/areal/trainer/ppo/actor.py index 07944a31a5..2d7972a450 100644 --- a/areal/trainer/ppo/actor.py +++ b/areal/trainer/ppo/actor.py @@ -31,6 +31,7 @@ split_padded_tensor_dict_into_mb_list, ) from areal.utils.functional import ( + cispo_loss_fn, ppo_actor_loss_fn, reward_overlong_penalty, sapo_loss_fn, @@ -358,6 +359,7 @@ def _ppo_update(self, data: dict[str, Any]) -> None: use_sapo_loss=self.config.use_sapo_loss, sapo_tau_pos=self.config.sapo_tau_pos, sapo_tau_neg=self.config.sapo_tau_neg, + use_cispo_loss=self.config.use_cispo_loss, use_decoupled_loss=self.config.use_decoupled_loss, ), loss_weight_fn=lambda x: x["loss_mask"].count_nonzero(), @@ -420,6 +422,7 @@ def grpo_loss_fn( use_sapo_loss: bool = False, sapo_tau_pos: float = 1.0, sapo_tau_neg: float = 1.05, + use_cispo_loss: bool = False, use_decoupled_loss: bool = False, vocab_min_logits: torch.Tensor | None = None, vocab_max_logits: torch.Tensor | None = None, @@ -447,8 +450,30 @@ def grpo_loss_fn( if m2_threshold is not None: loss_mask = _apply_m2po_masking(old_logp, prox_logp, loss_mask, m2_threshold) - # Use SAPO or PPO loss - if use_sapo_loss: + # Use CISPO, SAPO, or PPO loss + if use_cispo_loss: + if use_sapo_loss: + raise ValueError( + "CISPO and SAPO are mutually exclusive surrogates. " + "Set at most one of use_cispo_loss / use_sapo_loss." + ) + if importance_sampling_level != "token": + raise ValueError( + "CISPO only supports importance_sampling_level='token'. " + "Sequence-level (GSPO-style) CISPO has no published surrogate." + ) + loss, stat = cispo_loss_fn( + logprobs=logprobs, + proximal_logprobs=prox_logp, + advantages=advantages, + eps_clip=eps_clip, + eps_clip_higher=eps_clip_higher, + loss_mask=loss_mask, + old_logprobs=old_logp, + rejection_sampling=rejection_sampling, + cu_seqlens=input_data.get("cu_seqlens"), + ) + elif use_sapo_loss: if use_decoupled_loss: raise ValueError( "SAPO is not compatible with `use_decoupled_loss=True`. " diff --git a/areal/utils/functional/__init__.py b/areal/utils/functional/__init__.py index c91c3ff2b6..b09c6af5f2 100644 --- a/areal/utils/functional/__init__.py +++ b/areal/utils/functional/__init__.py @@ -3,6 +3,7 @@ from areal.utils.functional.functional import ( RejectionSamplingResult, apply_rejection_sampling, + cispo_loss_fn, dpo_pair_logratios, dpo_preference_loss, masked_normalization, @@ -20,6 +21,7 @@ # functional.py "RejectionSamplingResult", "apply_rejection_sampling", + "cispo_loss_fn", "dpo_pair_logratios", "dpo_preference_loss", "masked_normalization", diff --git a/areal/utils/functional/functional.py b/areal/utils/functional/functional.py index d1583e1523..8aaed7730a 100644 --- a/areal/utils/functional/functional.py +++ b/areal/utils/functional/functional.py @@ -662,6 +662,124 @@ def sapo_loss_fn( return pg_loss, stat +def cispo_loss_fn( + logprobs: torch.Tensor, + proximal_logprobs: torch.Tensor, + advantages: torch.Tensor, + eps_clip: float, + loss_mask: torch.Tensor, + eps_clip_higher: float | None = None, + old_logprobs: torch.Tensor | None = None, + rejection_sampling: RejectionSamplingConfig | None = None, + cu_seqlens: torch.Tensor | None = None, +) -> tuple[torch.Tensor, dict]: + """CISPO (Clipped IS-weight Policy Optimization) loss from MiniMax-M1. + + PPO/GRPO-style clipping zeroes the gradient of any token whose + importance-sampling ratio leaves the clip band: ``min(r*A, clip(r)*A)`` is + constant in theta there. MiniMax-M1 (https://arxiv.org/abs/2506.13585, + Eq. 4-5) observes those are disproportionately low-probability "fork" tokens + (``However``, ``Wait``, ...) that steer reasoning, and instead clips the IS + *weight* under stop-gradient while keeping the gradient on every token's + ``log pi_theta`` (the choice ScaleRL, https://arxiv.org/abs/2510.13786 Eq. 4, + also adopts). Per token:: + + ratio = exp(logprobs - proximal_logprobs) + ratio_clipped = clip(ratio, 1 - eps_clip, 1 + eps_clip_higher) # stop-grad + pg_loss = -sg(ratio_clipped) * advantages * logprobs + + Advantages are never clipped. The clip bounds reuse the same delta-from-1 + convention as :func:`ppo_actor_loss_fn`. CISPO is canonically single-sided: + pass ``eps_clip=1.0`` (lower bound 0) with ``eps_clip_higher=4.0`` for the + wide MiniMax-M1 range. Token level only -- the geometric-mean sequence ratio + of GSPO is not part of the MiniMax-M1 surrogate. + + Decoupled loss: when ``rejection_sampling`` is set, the detached + ``pi_proximal/pi_behave`` weight (from ``old_logprobs``) rescales each token's + surrogate, mirroring :func:`ppo_actor_loss_fn`. + + Args: + logprobs: Current policy log-probabilities (pi_theta), with autograd. + proximal_logprobs: Proximal policy log-probabilities; enters only through + the detached ratio path, so it carries no gradient. + advantages: Per-token advantage estimates; detached on entry. + eps_clip: Lower clipping delta from 1 (ratio lower bound ``1 - eps_clip``). + loss_mask: Mask for valid tokens (1 = valid). + eps_clip_higher: Upper clipping delta from 1 (ratio upper bound + ``1 + eps_clip_higher``); must be positive -- the asymmetric upper + clip is the defining knob of CISPO, so ``None`` is rejected. + old_logprobs: Behavior policy log-probabilities (pi_behave) from inference. + Required only when ``rejection_sampling`` is set (decoupled loss). + rejection_sampling: Staleness filtering / off-policy correction config. + None disables it (pure on-policy CISPO). + cu_seqlens: Cumulative sequence lengths for 1D packed inputs; required when + ``rejection_sampling.level == "sequence"``. + + Returns: + ``(loss, stat)`` matching the PPO loss signature. ``stat['clip_mask']`` + flags tokens whose raw ratio left the band (CISPO never zeroes their + loss, so band-exit -- not loss-affecting clip -- is the meaningful + metric); ``stat['importance_weight']`` reports the unclipped ratio. When + rejection sampling is active the ``behave_*`` keys are reported too. + """ + if eps_clip_higher is None or eps_clip_higher <= 0: + raise ValueError( + "CISPO requires a positive eps_clip_higher; the asymmetric upper " + f"clip is the defining knob (MiniMax-M1 Eq. 4-5). Got {eps_clip_higher!r}." + ) + # Pre-rejection token count, so the denominator matches loss_weight_fn. + loss_mask_count = loss_mask.count_nonzero() or 1 + + # Decoupled off-policy correction: the pi_proximal/pi_behave weight. + if rejection_sampling is not None: + rs_result = apply_rejection_sampling( + proximal_logprobs=proximal_logprobs, + old_logprobs=old_logprobs, + loss_mask=loss_mask, + cu_seqlens=cu_seqlens, + config=rejection_sampling, + ) + loss_mask = rs_result.loss_mask + behave_imp_weight = rs_result.behave_imp_weight + filtered_fraction = rs_result.filtered_fraction + + advantages = advantages.detach() + + # Stop-gradient on the clipped IS weight; gradient flows through logprobs only. + log_ratio = (logprobs - proximal_logprobs).detach() + ratio = torch.exp(log_ratio) + ratio_clipped = torch.clamp(ratio, 1.0 - eps_clip, 1.0 + eps_clip_higher).detach() + pg_loss = -ratio_clipped * advantages * logprobs + + if rejection_sampling is not None: + # behave_imp_weight is detached at source -> still a valid policy gradient. + behave_approx_kl = proximal_logprobs.detach() - old_logprobs.detach() + behave_mask = (behave_imp_weight > 0).logical_and(loss_mask.bool()) + behave_approx_kl = torch.where(behave_mask, behave_approx_kl, 0.0) + pg_loss = pg_loss * behave_imp_weight + + logging_loss = pg_loss.detach() + pg_loss = torch.where(loss_mask, pg_loss, 0).sum() / loss_mask_count + + clip_mask = (ratio_clipped != ratio).logical_and(loss_mask) + stat = dict( + loss=logging_loss, + importance_weight=ratio.detach(), + approx_kl=log_ratio, + clip_mask=clip_mask, + # CISPO has no dual clip; zeros keep the stat schema stable. + dual_clip_mask=torch.zeros_like(loss_mask, dtype=torch.bool), + ) + if rejection_sampling is not None: + stat.update( + behave_approx_kl=behave_approx_kl.detach(), + behave_imp_weight=behave_imp_weight.detach(), + behave_mask=behave_mask, + filtered_fraction=filtered_fraction, + ) + return pg_loss, stat + + def dpo_pair_logratios( logprobs: torch.Tensor, ref_logprobs: torch.Tensor, diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index a137c15c10..78d9515cde 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -401,6 +401,7 @@ Configuration for PPO actor model, a subclass of a TrainEngine. | `use_sapo_loss` | boolean | `False` | Use SAPO loss (mutually exclusive with PPO clipping) | | `sapo_tau_pos` | float | `1.0` | SAPO temperature for positive advantages | | `sapo_tau_neg` | float | `1.05` | SAPO temperature for negative advantages | +| `use_cispo_loss` | boolean | `False` | Use CISPO loss: clip the importance-sampling weight under stop-gradient and keep gradient on every token's log pi (MiniMax-M1 Eq. 4-5). Mutually exclusive with SAPO. Token-level only. Requires eps_clip_higher > 0; recommended eps_clip=1.0 (single-sided, lower bound 0) with eps_clip_higher=4.0. | | `recompute_logprob` | boolean | `False` | Recompute log probability and replace the log probability returned by inference. | | `use_decoupled_loss` | boolean | `False` | Use the decoupled loss. Implicitly enables recompute_logprob. | | `rejection_sampling` | [`RejectionSamplingConfig`](section-rejection-sampling) \| None | `None` | Rejection sampling configuration for filtering stale samples. None disables filtering (equivalent to old behave_imp_weight_mode='disabled'). Only effective when use_decoupled_loss=True. | diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index 29c6a5745f..455339316f 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -399,6 +399,7 @@ Configuration for PPO actor model, a subclass of a TrainEngine. | `use_sapo_loss` | boolean | `False` | Use SAPO loss (mutually exclusive with PPO clipping) | | `sapo_tau_pos` | float | `1.0` | SAPO temperature for positive advantages | | `sapo_tau_neg` | float | `1.05` | SAPO temperature for negative advantages | +| `use_cispo_loss` | boolean | `False` | Use CISPO loss: clip the importance-sampling weight under stop-gradient and keep gradient on every token's log pi (MiniMax-M1 Eq. 4-5). Mutually exclusive with SAPO. Token-level only. Requires eps_clip_higher > 0; recommended eps_clip=1.0 (single-sided, lower bound 0) with eps_clip_higher=4.0. | | `recompute_logprob` | boolean | `False` | Recompute log probability and replace the log probability returned by inference. | | `use_decoupled_loss` | boolean | `False` | Use the decoupled loss. Implicitly enables recompute_logprob. | | `rejection_sampling` | [`RejectionSamplingConfig`](section-rejection-sampling) \| None | `None` | Rejection sampling configuration for filtering stale samples. None disables filtering (equivalent to old behave_imp_weight_mode='disabled'). Only effective when use_decoupled_loss=True. | diff --git a/tests/test_cispo_loss.py b/tests/test_cispo_loss.py new file mode 100644 index 0000000000..44745a5a31 --- /dev/null +++ b/tests/test_cispo_loss.py @@ -0,0 +1,173 @@ +# SPDX-License-Identifier: Apache-2.0 +"""CISPO surrogate (MiniMax-M1 Eq. 4-5) invariants. + +Two defining properties, each across a PPO-like band (0.2 / 0.28) and the wide +MiniMax-M1 band (1.0 / 4.0): + +1. closed-form surrogate value + which tokens the clip flags; +2. gradient routing -- ``logprobs.grad == -sg(clip(ratio)) * A / N`` exactly, + with zero gradient through the importance-ratio path (the test fails if the + stop-gradient detach is dropped). +""" + +import pytest +import torch + +from areal.api.cli_args import PPOActorConfig, RejectionSamplingConfig +from areal.utils.functional import cispo_loss_fn + +# (eps_clip, eps_clip_higher): a PPO-like band that clips most tokens, and the +# wide MiniMax-M1 band [0, 5] that clips none of the fixture ratios. +BANDS = [(0.2, 0.28), (1.0, 4.0)] + + +def _inputs(): + # log_ratio spans below / inside / above a tight band. The last token is + # masked out, so it must contribute neither loss nor gradient. + log_ratio = torch.tensor([-2.0, -0.5, 0.0, 0.3, 1.5, 0.1]) + logprobs = torch.tensor([-1.0, -2.0, -0.3, -1.2, -0.7, -2.5], requires_grad=True) + proximal = logprobs.detach() - log_ratio # logprobs - proximal == log_ratio + advantages = torch.tensor([1.0, -2.0, 0.5, 3.0, -1.0, 2.0]) + loss_mask = torch.tensor([1, 1, 1, 1, 1, 0], dtype=torch.bool) + return logprobs, proximal, advantages, loss_mask, log_ratio + + +@pytest.mark.parametrize("eps_clip,eps_clip_higher", BANDS) +def test_cispo_closed_form_value_and_clip_mask(eps_clip, eps_clip_higher): + logprobs, proximal, advantages, loss_mask, log_ratio = _inputs() + + loss, stat = cispo_loss_fn( + logprobs=logprobs, + proximal_logprobs=proximal, + advantages=advantages, + eps_clip=eps_clip, + eps_clip_higher=eps_clip_higher, + loss_mask=loss_mask, + ) + + ratio = torch.exp(log_ratio) + ratio_clipped = ratio.clamp(1.0 - eps_clip, 1.0 + eps_clip_higher) + per_token = -ratio_clipped * advantages * logprobs.detach() + expected = ( + torch.where(loss_mask, per_token, torch.zeros_like(per_token)).sum() + / loss_mask.count_nonzero() + ) + + torch.testing.assert_close(loss, expected) + # importance_weight logs the *unclipped* ratio for diagnostics. + torch.testing.assert_close(stat["importance_weight"], ratio) + # clip_mask flags band-exit on either side, intersected with the loss mask. + expected_clip = (ratio != ratio_clipped) & loss_mask + assert torch.equal(stat["clip_mask"], expected_clip) + assert not stat["dual_clip_mask"].any() + + +@pytest.mark.parametrize("eps_clip,eps_clip_higher", BANDS) +def test_cispo_gradient_routes_through_logprobs_only(eps_clip, eps_clip_higher): + logprobs, proximal, advantages, loss_mask, log_ratio = _inputs() + # Make the proximal logp a grad-tracking leaf: a correct stop-gradient must + # leave it with no gradient. + proximal = proximal.clone().requires_grad_(True) + + loss, _ = cispo_loss_fn( + logprobs=logprobs, + proximal_logprobs=proximal, + advantages=advantages, + eps_clip=eps_clip, + eps_clip_higher=eps_clip_higher, + loss_mask=loss_mask, + ) + loss.backward() + + ratio = torch.exp(log_ratio) + ratio_clipped = ratio.clamp(1.0 - eps_clip, 1.0 + eps_clip_higher) + n = loss_mask.count_nonzero() + # Gradient flows ONLY through the explicit `logprobs` factor; the clipped + # ratio is a stop-gradient constant. If the detach were dropped, ratio would + # depend on logprobs and add a term, breaking this exact equality. + expected_grad = torch.where( + loss_mask, -ratio_clipped * advantages / n, torch.zeros_like(ratio) + ) + torch.testing.assert_close(logprobs.grad, expected_grad) + # No gradient leaks to the importance-ratio (proximal) path. + assert proximal.grad is None + + +def test_cispo_rejects_nonpositive_eps_clip_higher(): + logprobs, proximal, advantages, loss_mask, _ = _inputs() + for bad in (None, 0.0, -1.0): + with pytest.raises(ValueError, match="eps_clip_higher"): + cispo_loss_fn( + logprobs=logprobs, + proximal_logprobs=proximal, + advantages=advantages, + eps_clip=1.0, + eps_clip_higher=bad, + loss_mask=loss_mask, + ) + + +def test_cispo_decoupled_applies_behave_imp_weight(): + # Decoupled loss: surrogate rescaled by detached pi_proximal/pi_behave; grad + # still routes only through logprobs. + eps_clip, eps_clip_higher = 1.0, 4.0 + logprobs, proximal, advantages, loss_mask, log_ratio = _inputs() + # upper is wide so clamp is a no-op and behave_imp_weight == exp(behave_log_ratio). + behave_log_ratio = torch.tensor([0.2, -0.4, 0.0, 0.1, -0.3, 0.5]) + old_logprobs = proximal - behave_log_ratio # proximal - old == behave_log_ratio + rs = RejectionSamplingConfig( + level="token", action="clamp", metric="ratio", upper=100.0 + ) + + proximal_leaf = proximal.clone().requires_grad_(True) + old_leaf = old_logprobs.clone().requires_grad_(True) + + loss, stat = cispo_loss_fn( + logprobs=logprobs, + proximal_logprobs=proximal_leaf, + advantages=advantages, + eps_clip=eps_clip, + eps_clip_higher=eps_clip_higher, + loss_mask=loss_mask, + old_logprobs=old_leaf, + rejection_sampling=rs, + ) + + ratio = torch.exp(log_ratio) + ratio_clipped = ratio.clamp(1.0 - eps_clip, 1.0 + eps_clip_higher) + behave_w = torch.exp(behave_log_ratio) + n = loss_mask.count_nonzero() + per_token = -ratio_clipped * advantages * logprobs.detach() * behave_w + expected = torch.where(loss_mask, per_token, torch.zeros_like(per_token)).sum() / n + torch.testing.assert_close(loss, expected) + # behave_imp_weight is zeroed on masked tokens, so compare valid positions. + torch.testing.assert_close( + stat["behave_imp_weight"][loss_mask], behave_w[loss_mask] + ) + assert "behave_mask" in stat and "behave_approx_kl" in stat + + loss.backward() + expected_grad = torch.where( + loss_mask, -ratio_clipped * advantages * behave_w / n, torch.zeros_like(ratio) + ) + torch.testing.assert_close(logprobs.grad, expected_grad) + assert proximal_leaf.grad is None + assert old_leaf.grad is None + + +def test_cispo_config_validation(): + # Requires a positive upper clip. + with pytest.raises(ValueError, match="eps_clip_higher"): + PPOActorConfig(use_cispo_loss=True, eps_clip_higher=None) + # Mutually exclusive with SAPO. + with pytest.raises(ValueError, match="mutually exclusive"): + PPOActorConfig(use_cispo_loss=True, use_sapo_loss=True, eps_clip_higher=4.0) + # Token level only. + with pytest.raises(ValueError, match="importance_sampling_level"): + PPOActorConfig( + use_cispo_loss=True, + eps_clip_higher=4.0, + importance_sampling_level="sequence", + ) + # Valid configuration does not raise. + PPOActorConfig(use_cispo_loss=True, eps_clip=1.0, eps_clip_higher=4.0) From 58dee1d1b5f38804d04476247152f7e33198bbd7 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:43:43 +0800 Subject: [PATCH 12/36] fix(CI): fix vlm_grpo CI OOM bug (#1438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default fp32 master weights (PR #1369) push Qwen2.5-VL-3B beyond A100-40G capacity at the first AdamW step. Pin the test to bf16 storage with adam_bf16 (Kahan summation) so it runs on 40G runners. ## Description ## Related Issue Fixes #(issue) ## Type of Change - [x] 🐛 Bug fix - [ ] ✨ New feature - [ ] 💥 Breaking change - [ ] 📝 Documentation update - [ ] ♻️ Refactoring - [ ] ⚡ Performance improvement - [ ] ✅ Test coverage improvement ## Checklist - [ ] I have read the [Contributing Guide](https://github.com/areal-project/community/blob/main/CONTRIBUTING.md) - [ ] Pre-commit hooks pass (`pre-commit run --all-files`) - [ ] Relevant tests pass; new tests added for new functionality - [ ] Documentation updated (if applicable; built with `./docs/build_all.sh`) - [ ] Branch is up to date with `main` - [ ] Self-reviewed via `/review-pr` command - [ ] This PR was created by a coding agent via `/create-pr` - [ ] This PR is a breaking change **Breaking Change Details (if applicable):** ## Additional Context ______________________________________________________________________ **Need help?** Check the [Contributing Guide](https://github.com/areal-project/community/blob/main/CONTRIBUTING.md) or ask in [GitHub Discussions](https://github.com/areal-project/AReaL/discussions)! --- tests/test_examples.py | 1 + tests/test_packed_vs_padded_consistency.py | 1 + tests/test_train_engine.py | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/test_examples.py b/tests/test_examples.py index 2acb62f18a..60db9e0557 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -306,6 +306,7 @@ def test_vlm_grpo(tmp_path_factory, rollout_backend, actor_backend): "gconfig.n_samples=2", "gconfig.max_new_tokens=256", "actor.mb_spec.max_tokens_per_mb=1024", + "+actor.optimizer_dtype=bfloat16", "train_dataset.batch_size=2", "valid_dataset.batch_size=2", f"train_dataset.path={dataset_path}", diff --git a/tests/test_packed_vs_padded_consistency.py b/tests/test_packed_vs_padded_consistency.py index 7e1cee73a5..7dc08ab4ee 100644 --- a/tests/test_packed_vs_padded_consistency.py +++ b/tests/test_packed_vs_padded_consistency.py @@ -69,6 +69,7 @@ def test_llm_consistency(model_path, mock_padded_llm_data): backend="fsdp:d1", path=model_path, dtype="bfloat16", + optimizer_dtype="bfloat16", attn_impl="flash_attention_2", gradient_checkpointing=False, disable_dropout=True, diff --git a/tests/test_train_engine.py b/tests/test_train_engine.py index d5090b2fe7..3a72fe4f0d 100644 --- a/tests/test_train_engine.py +++ b/tests/test_train_engine.py @@ -325,6 +325,7 @@ def from_pretrained(pretrained_model_name_or_path, **kwargs): trial_name="trial0", path="test-model", attn_impl="kernels-community/flash-attn", + optimizer_dtype="bfloat16", fsdp=FSDPEngineConfig(memory_efficient_load=memory_efficient_load), ) engine = fsdp_module.FSDPEngine(config) From be8d7cf005f265e8de4d8437863eee2911790898 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:03:59 +0800 Subject: [PATCH 13/36] feat(cli): add experimental cli scaffold for service-style subcommands (#1440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): add experimental cli scaffold for service-style subcommands Prepares the ground for feat/inference-service-cli, feat/training-service-cli, and feat/agent-service-cli to land on top — each subcommand PR only adds its own click group and one cli.add_command call, instead of duplicating the same plumbing. Key pieces: - empty top-level `cli` click group with version + --help wiring; subcommand modules attach themselves via cli.add_command(...) - `state.areal_home()` + `atomic_write_json()` for AREAL_HOME-rooted local state files (atomic .tmp + os.replace) - `process` module: pid_alive / pick_free_port / signal_pid / kill_pids + spawn_process with start_new_session=True so detached worker processes survive parent SIGHUP and a graceful SIGTERM→grace→SIGKILL teardown sequence is one helper away - pyproject: `[project.scripts] areal = ...cli.main:cli`, optional `[cli]` deps (click, colorlog) Co-Authored-By: Claude Opus 4.7 * build(cli): regenerate uv lockfiles for cli optional deps Pre-commit's uv-lock hook expects the lockfiles to track click and colorlog after they were added to [project.optional-dependencies] cli. Co-Authored-By: Claude Opus 4.7 * fix(cli): address gemini review on process/state utilities - pid_alive: try os.waitpid(pid, WNOHANG) before os.kill so a zombie child does not look alive — otherwise kill_pids waits its full grace window and sends a redundant SIGKILL. - spawn_process: close the parent's log_handle copy after Popen dup()s it for the child, so repeated spawns do not leak fds. - atomic_write_json: use tempfile.NamedTemporaryFile + os.fsync, and unlink the tempfile on serialization or rename failure — fixes a tempfile leak on bad input, durability against crashes, and a race between concurrent writers on the same path. Co-Authored-By: Claude Opus 4.7 * refactor(cli): unify terminate + reuse kill_process_tree, drop pick_free_port Replace the scaffold's bespoke process primitives with shared utilities already in the codebase: - process: kill_pids now delegates to areal.infra.utils.proc.kill_process_tree, which walks the descendant tree via psutil. This fixes the bug TaoZex raised on signal_pid's inconsistent PermissionError handling — signal_pid is gone entirely. - process: drop pick_free_port. Callers should use areal.utils.network.find_free_ports, which draws from a non-ephemeral port range and supports exclude_ports — addresses the TOCTOU race guozhihao-224 flagged on the naive bind(0) implementation. - scheduler: new module with a stateless top-level terminate(ref, *, backend, grace_s) that subcommand CLIs can funnel teardown through. Only the local backend is implemented; future backends extend the dispatch in-module. The free-function shape keeps the dispatcher from drifting into stateful scheduler instances. Co-Authored-By: Claude Opus 4.7 * chore: integrate CLI shared files Co-Authored-By: Claude Opus 4.7 * style(cli): drop extra blank line after imports in utils.py Local pre-commit auto-fixed the blank line but the fix wasn't re-staged before commit 9401918bf, so CI's ruff hook caught it. * refactor(cli): collect namespace path helpers into NamespacedStateStore state.py previously exposed 13 free functions (namespace_root, services_dir, logs_dir, service_state_path, …, recover_pids_from_raw_state) that all took ``namespace`` as their first argument. Promote them to methods on a new NamespacedStateStore class so subcommand CLIs construct one instance per namespace and stop threading the namespace string through every call. areal_home, atomic_write_json, SupportsComponentProbe, and ServiceStateBase remain at module level. ServiceLifecycle and ConfigLoader each hold a store instance built from their namespace at __init__ time; LogsCommand resolves log paths via ``lifecycle.store.logs_dir``. No behavior change. Note: downstream branches (inf / agent / train) will need to swap their imports from ``state.service_state_path``/``state.clear_current_service``/ etc. to ``store.`` on the next rebase. --------- Co-authored-by: Claude Opus 4.7 --- areal/experimental/cli/__init__.py | 1 + areal/experimental/cli/__main__.py | 6 + areal/experimental/cli/cli.py | 21 ++ areal/experimental/cli/client.py | 130 +++++++++ areal/experimental/cli/commands/__init__.py | 1 + areal/experimental/cli/commands/logs.py | 88 ++++++ areal/experimental/cli/config.py | 106 ++++++++ areal/experimental/cli/lifecycle.py | 149 +++++++++++ areal/experimental/cli/main.py | 8 + areal/experimental/cli/process.py | 93 +++++++ areal/experimental/cli/scheduler.py | 35 +++ areal/experimental/cli/state.py | 280 ++++++++++++++++++++ areal/experimental/cli/status.py | 134 ++++++++++ areal/experimental/cli/utils.py | 168 ++++++++++++ areal/experimental/cli/watcher.py | 109 ++++++++ pyproject.toml | 7 + pyproject.vllm.toml | 7 + uv.lock | 8 +- uv.vllm.lock | 8 +- 19 files changed, 1357 insertions(+), 2 deletions(-) create mode 100644 areal/experimental/cli/__init__.py create mode 100644 areal/experimental/cli/__main__.py create mode 100644 areal/experimental/cli/cli.py create mode 100644 areal/experimental/cli/client.py create mode 100644 areal/experimental/cli/commands/__init__.py create mode 100644 areal/experimental/cli/commands/logs.py create mode 100644 areal/experimental/cli/config.py create mode 100644 areal/experimental/cli/lifecycle.py create mode 100644 areal/experimental/cli/main.py create mode 100644 areal/experimental/cli/process.py create mode 100644 areal/experimental/cli/scheduler.py create mode 100644 areal/experimental/cli/state.py create mode 100644 areal/experimental/cli/status.py create mode 100644 areal/experimental/cli/utils.py create mode 100644 areal/experimental/cli/watcher.py diff --git a/areal/experimental/cli/__init__.py b/areal/experimental/cli/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/areal/experimental/cli/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/areal/experimental/cli/__main__.py b/areal/experimental/cli/__main__.py new file mode 100644 index 0000000000..87c14941f4 --- /dev/null +++ b/areal/experimental/cli/__main__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 + +from areal.experimental.cli.cli import cli + +if __name__ == "__main__": + cli() diff --git a/areal/experimental/cli/cli.py b/areal/experimental/cli/cli.py new file mode 100644 index 0000000000..b97e28cf79 --- /dev/null +++ b/areal/experimental/cli/cli.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.version import __version__ + + +@click.group( + context_settings={"help_option_names": ["-h", "--help"]}, + help="AReaL operator CLI.", +) +@click.version_option(__version__, prog_name="areal") +def cli() -> None: + pass + + +# Subcommand groups (inf / train / agent / ...) attach themselves to ``cli`` +# from their own modules — see e.g. ``areal.experimental.cli.inference``, +# ``areal.experimental.cli.training``, ``areal.experimental.cli.agent``. diff --git a/areal/experimental/cli/client.py b/areal/experimental/cli/client.py new file mode 100644 index 0000000000..fac1ae5f39 --- /dev/null +++ b/areal/experimental/cli/client.py @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""HTTP client base for subcommand CLIs. + +Provides: + +- ``ServiceHTTPError`` / ``ServiceUnreachable`` exceptions to distinguish + "server replied with 4xx/5xx" from "couldn't reach the server". +- A low-level ``request_json`` helper that builds the request, handles + optional Bearer auth, decodes JSON, and translates ``urllib`` errors + into the two exception types above. +- ``BaseHTTPClient`` — minimal base for per-component clients + (gateway / router / data-proxy / etc.). Subclasses add their own RPC + methods that call ``self._get`` / ``self._post`` / ``self._request``. +""" + +from __future__ import annotations + +import json +import urllib.error +import urllib.request +from typing import Any + + +class ServiceHTTPError(Exception): + """Non-2xx response from the service. Body is captured so callers + can inspect application-level error payloads.""" + + def __init__(self, status: int, body: str) -> None: + super().__init__(f"HTTP {status}: {body}") + self.status = status + self.body = body + + +class ServiceUnreachable(Exception): + """Network-level error reaching the service: connection refused, + DNS failure, timeout, etc.""" + + +def request_json( + url: str, + *, + method: str = "GET", + payload: dict | None = None, + bearer: str | None = None, + timeout: float = 5.0, +) -> dict[str, Any]: + """Issue an HTTP request returning parsed JSON. + + A ``payload`` dict triggers JSON encoding + ``Content-Type``. + ``bearer`` adds ``Authorization: Bearer ``. 2xx → returns the + parsed body (empty dict if body is empty). 4xx/5xx → ``ServiceHTTPError``. + Network errors → ``ServiceUnreachable``. + """ + + data = None + headers = {"Accept": "application/json"} + if payload is not None: + data = json.dumps(payload).encode() + headers["Content-Type"] = "application/json" + if bearer: + headers["Authorization"] = f"Bearer {bearer}" + req = urllib.request.Request(url, data=data, method=method, headers=headers) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + body = resp.read().decode() + return json.loads(body) if body else {} + except urllib.error.HTTPError as exc: + raise ServiceHTTPError(exc.code, exc.read().decode(errors="replace")) from exc + except (urllib.error.URLError, ConnectionError, TimeoutError, OSError) as exc: + raise ServiceUnreachable(str(exc)) from exc + + +class BaseHTTPClient: + """Minimal HTTP client base. Subclasses add business RPC methods. + + Holds ``base_url`` and an optional ``api_key`` used as Bearer auth on + every helper call. ``health()`` is provided as it's universal across + all daemon types. + """ + + def __init__(self, base_url: str, api_key: str | None = None) -> None: + self.base = base_url.rstrip("/") + self.api_key = api_key + + def health(self, *, timeout: float = 2.0) -> dict[str, Any]: + return self._get("/health", timeout=timeout, auth=False) + + # ------------------------------------------------------------------ + # Helpers for subclass RPC methods + + def _get(self, path: str, *, timeout: float = 5.0, auth: bool = True) -> dict: + return request_json( + f"{self.base}{path}", + timeout=timeout, + bearer=self.api_key if auth else None, + ) + + def _post( + self, + path: str, + payload: dict | None = None, + *, + timeout: float = 10.0, + auth: bool = True, + ) -> dict: + return request_json( + f"{self.base}{path}", + method="POST", + payload=payload, + timeout=timeout, + bearer=self.api_key if auth else None, + ) + + def _request( + self, + method: str, + path: str, + payload: dict | None = None, + *, + timeout: float = 10.0, + auth: bool = True, + ) -> dict: + return request_json( + f"{self.base}{path}", + method=method, + payload=payload, + timeout=timeout, + bearer=self.api_key if auth else None, + ) diff --git a/areal/experimental/cli/commands/__init__.py b/areal/experimental/cli/commands/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/areal/experimental/cli/commands/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/areal/experimental/cli/commands/logs.py b/areal/experimental/cli/commands/logs.py new file mode 100644 index 0000000000..98a0f3aa66 --- /dev/null +++ b/areal/experimental/cli/commands/logs.py @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""`` logs`` verb factory. + +Every service-style CLI has a ``logs`` command that does the same +thing: resolve the service, find the log directory, tail one component +file. This class packages that pattern. Subcommand CLIs do:: + + from areal.experimental.cli.commands.logs import LogsCommand + from areal.experimental.cli.inference.lifecycle import inf_lifecycle + + logs_cmd = LogsCommand(lifecycle=inf_lifecycle).build() + inf.add_command(logs_cmd) +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import click + +from areal.experimental.cli.lifecycle import ServiceLifecycle + + +class LogsCommand: + default_component: str = "gateway" + default_lines: int = 200 + + def __init__(self, *, lifecycle: ServiceLifecycle) -> None: + self.lifecycle = lifecycle + + def build(self) -> click.Command: + @click.command(name="logs", help="Tail a service log file.") + @click.option("--service", default=None) + @click.option( + "--component", + default=self.default_component, + show_default=True, + help="Component log to tail (gateway / router / worker-N / etc.).", + ) + @click.option("-f", "--follow", is_flag=True, help="Stream appended lines.") + @click.option( + "-n", + "--lines", + type=int, + default=self.default_lines, + show_default=True, + help="Number of trailing lines to show before optional follow.", + ) + def logs_cmd( + service: str | None, component: str, follow: bool, lines: int + ) -> None: + self.execute(service, component, follow, lines) + + return logs_cmd + + def execute( + self, + service: str | None, + component: str, + follow: bool, + lines: int, + ) -> None: + name = self.lifecycle.resolve_service_name(service) + target = self.log_target(name, component) + if not target.exists(): + available = sorted(p.stem for p in target.parent.glob("*.log")) + if not available: + raise click.ClickException(f"no logs found under {target.parent}") + raise click.ClickException( + f"no log named {component!r}; available: {', '.join(available)}" + ) + self._exec_tail(target, lines=lines, follow=follow) + + def log_target(self, service: str, component: str) -> Path: + """Resolve the log file path. Subclasses may override to add a + component name alias / translation layer (e.g. accepting + ``qwen/0/worker`` as shorthand for ``qwen-worker-0``).""" + + return self.lifecycle.store.logs_dir(service) / f"{component}.log" + + def _exec_tail(self, target: Path, *, lines: int, follow: bool) -> None: + cmd = ["tail", f"-n{lines}"] + if follow: + cmd.append("-F") + cmd.append(str(target)) + os.execvp(cmd[0], cmd) diff --git a/areal/experimental/cli/config.py b/areal/experimental/cli/config.py new file mode 100644 index 0000000000..0dd5203737 --- /dev/null +++ b/areal/experimental/cli/config.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""TOML config loader for subcommand CLIs. + +Each CLI's config file lives at ``$AREAL_HOME//config.toml``. +Sections + keys map to ``(verb, click_option_name)`` via a per-CLI +``bindings`` dict that subclasses provide; this base handles all the +flatten + merge + dispatch plumbing. + +Precedence: explicit CLI flag > extra config passed via ``--config`` > +``$AREAL_HOME//config.toml`` > the click option's hard-coded +default. +""" + +from __future__ import annotations + +from pathlib import Path + +from areal.experimental.cli.state import NamespacedStateStore + +# (section, key) → (verb_name_or_tuple, click_option_name) +BindingMap = dict[tuple[str, str], tuple[str | tuple[str, ...], str]] + + +class ConfigLoader: + """Subclass and set ``namespace`` + ``bindings`` (or pass them in).""" + + namespace: str = "" + bindings: BindingMap = {} + + def __init__( + self, + *, + namespace: str | None = None, + bindings: BindingMap | None = None, + ) -> None: + if namespace is not None: + self.namespace = namespace + if bindings is not None: + self.bindings = bindings + if not self.namespace: + raise ValueError("ConfigLoader requires a namespace") + self.store = NamespacedStateStore(self.namespace) + + def default_config_path(self) -> Path: + return self.store.config_path() + + def load_click_default_map(self, extra: Path | None = None) -> dict: + """Return a ``click`` ``default_map`` built by merging the + namespace's default config.toml with an optional extra file. + + ``extra`` (typically the ``--config FILE`` flag) wins on + conflicts; bindings absent from the map are silently ignored. + """ + + merged: dict[tuple[str, str], object] = {} + for path in (self.default_config_path(), extra): + if path is None: + continue + merged.update(self._flatten(self._read_toml(path))) + + default_map: dict[str, dict] = {} + for (section, key), value in merged.items(): + binding = self.bindings.get((section, key)) + if binding is None: + continue + verbs, opt = binding + if isinstance(verbs, str): + verbs = (verbs,) + for verb in verbs: + default_map.setdefault(verb, {})[opt] = value + return default_map + + # ------------------------------------------------------------------ + # Helpers — subclasses generally don't need to override these + + def _read_toml(self, path: Path) -> dict: + try: + import tomllib + except ImportError: + return {} + if not path.exists(): + return {} + try: + with open(path, "rb") as f: + return tomllib.load(f) + except Exception: + return {} + + def _flatten(self, toml: dict, parent: str = "") -> dict[tuple[str, str], object]: + """Flatten nested TOML tables to ``(section, key) -> value``. + + Nested keys are dotted on the section side: + ``[register.internal] backend = "..."`` → + ``("register.internal", "backend") -> "..."``. + """ + + out: dict[tuple[str, str], object] = {} + for k, v in toml.items(): + if isinstance(v, dict): + sub_parent = f"{parent}.{k}" if parent else k + for (sec, key), val in self._flatten(v, sub_parent).items(): + out[(sec, key)] = val + else: + out[(parent, k)] = v + return out diff --git a/areal/experimental/cli/lifecycle.py b/areal/experimental/cli/lifecycle.py new file mode 100644 index 0000000000..da62c87c91 --- /dev/null +++ b/areal/experimental/cli/lifecycle.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Service lifecycle helpers shared across subcommand CLIs. + +A ``ServiceLifecycle`` bundles a CLI's namespace + state class + stop +command name, then exposes the three "is this service running" +predicates (``running_state`` / ``load_running_state`` / +``refuse_if_running``) plus ``force_replace_slot`` for ``run --force``. + +Subcommand CLIs subclass to inject their state class and may override +``gateway_alive`` if they don't want the default +``state.gateway_alive()`` delegation. +""" + +from __future__ import annotations + +from pathlib import Path + +import click + +from areal.experimental.cli.process import kill_pids +from areal.experimental.cli.state import DEFAULT_SERVICE, NamespacedStateStore + + +class ServiceLifecycle: + namespace: str = "" + state_class: type = type(None) + stop_command: str = "" + default_service: str = DEFAULT_SERVICE + + def __init__( + self, + *, + namespace: str | None = None, + state_class: type | None = None, + stop_command: str | None = None, + ) -> None: + if namespace is not None: + self.namespace = namespace + if state_class is not None: + self.state_class = state_class + if stop_command is not None: + self.stop_command = stop_command + if not self.namespace: + raise ValueError("ServiceLifecycle requires a namespace") + if not self.stop_command: + raise ValueError("ServiceLifecycle requires a stop_command") + self.store = NamespacedStateStore(self.namespace) + + # ------------------------------------------------------------------ + # Hooks subclasses may override + + def gateway_alive(self, state) -> bool: + """Default delegates to ``state.gateway_alive()`` (the + ServiceStateBase contract). Subclasses may override if their + state object can't or shouldn't implement that method itself.""" + + return state.gateway_alive() + + def state_path(self, service: str) -> Path: + return self.store.service_state_path(service) + + def resolve_service_name(self, explicit: str | None) -> str: + return self.store.resolve_service_name(explicit, fallback=self.default_service) + + def list_services(self) -> list[str]: + return self.store.list_service_names() + + def load_state(self, service: str): + return self.state_class.load(service) + + # ------------------------------------------------------------------ + # Public API + + def running_state(self, service: str | None = None): + """Return the loaded state iff it exists, parses, and reports + the gateway alive. Returns None otherwise — never raises.""" + + name = self.resolve_service_name(service) + if not self.state_path(name).exists(): + return None + try: + state = self.load_state(name) + except Exception: + return None + if not self.gateway_alive(state): + return None + return state + + def load_running_state(self, service: str | None = None): + """Like ``running_state`` but raises ``ClickException`` on + every failure — for command bodies that need a state to proceed.""" + + name = self.resolve_service_name(service) + if not self.state_path(name).exists(): + raise click.ClickException(f"service {name!r} is not running") + try: + state = self.load_state(name) + except Exception as exc: + raise click.ClickException(f"failed to load state: {exc}") from exc + if not self.gateway_alive(state): + raise click.ClickException(f"service {name!r} gateway is not alive") + return state + + def refuse_if_running(self, service: str | None = None) -> None: + """Used by ``run`` to refuse double-start. No-op if no running + service exists; raises ``ClickException`` if one does.""" + + state = self.running_state(service) + if state is None: + return + raise click.ClickException( + f"service {state.service!r} already running. " + f"Run `{self.stop_command} --service {state.service}` first." + ) + + def force_replace_slot(self, service: str, *, grace_s: float = 5.0) -> None: + """``run --force`` path: tear down any existing children for + ``service`` (loading the state for an orderly kill, falling back + to the raw-JSON PID walk if the state file is corrupted), then + unlink the state file so the next spawn starts clean. + + Caller is still responsible for the subsequent fresh spawn. + """ + + path = self.state_path(service) + if not path.exists(): + return + pids: list[int] = [] + try: + state = self.load_state(service) + pids = self._collect_pids(state) + except Exception: + try: + pids = self.store.recover_pids_from_raw_state(service) + except Exception: + pids = [] + if pids: + kill_pids(pids, grace_s=grace_s) + path.unlink(missing_ok=True) + self.store.clear_current_service(service) + + def _collect_pids(self, state) -> list[int]: + """Default implementation pulls ``.pid`` off every component + ``state.components()`` yields. Subclasses with extra state files + (e.g. inf's ``models/.json``) may override to extend the + list.""" + + return [pid for _, h in state.components() if (pid := h.pid) > 0] diff --git a/areal/experimental/cli/main.py b/areal/experimental/cli/main.py new file mode 100644 index 0000000000..0929ca84ec --- /dev/null +++ b/areal/experimental/cli/main.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from areal.experimental.cli.cli import cli + +if __name__ == "__main__": + cli() diff --git a/areal/experimental/cli/process.py b/areal/experimental/cli/process.py new file mode 100644 index 0000000000..c72a4bf10e --- /dev/null +++ b/areal/experimental/cli/process.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Local-only process primitives for service-style CLIs. + +These helpers operate on local PIDs and process trees. Remote backends +(K8s / Slurm) should not consume them directly — route teardown through +``areal.experimental.cli.scheduler.terminate`` instead, which dispatches +per backend. + +For port allocation use ``areal.utils.network.find_free_ports``; it +draws from a non-ephemeral range and supports excluding already-handed- +out ports, avoiding the TOCTOU collisions a naive ``bind(0)`` walks +into. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +from areal.infra.utils.proc import kill_process_tree + + +def pid_alive(pid: int) -> bool: + if pid <= 0: + return False + # If ``pid`` is one of our own children we may catch it mid-zombie: + # ``os.kill(pid, 0)`` still succeeds for zombies, so an unreaped child + # would look alive forever and kill_pids would burn its full grace + # window before sending a redundant SIGKILL. Reap it first via WNOHANG + # so a zombie is reported dead immediately. + try: + reaped, _ = os.waitpid(pid, os.WNOHANG) + if reaped == pid: + return False + except (ChildProcessError, OSError): + # Not our child, or already reaped — fall through to the kill probe. + pass + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +def spawn_process( + cmd: list[str], log_file: Path, env: dict[str, str] | None = None +) -> int: + """Spawn a detached subprocess that survives parent exit. + + ``start_new_session=True`` puts the child in its own session so the + parent receiving SIGHUP (terminal close) does not propagate to the + child. stdout / stderr are appended to ``log_file``. Extra env vars + in ``env`` are merged on top of the parent environment. + """ + + log_file.parent.mkdir(parents=True, exist_ok=True) + final_env = os.environ.copy() + final_env.setdefault("PYTHONUNBUFFERED", "1") + if env: + final_env.update(env) + # Popen dup()s the fd for the child, so closing our copy here does not + # affect the child's stdout/stderr — and it stops us from leaking a fd + # in the parent every time spawn_process is called. + with open(log_file, "ab", buffering=0) as log_handle: + proc = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stdout=log_handle, + stderr=subprocess.STDOUT, + start_new_session=True, + env=final_env, + ) + return proc.pid + + +def kill_pids(pids: list[int], grace_s: float) -> None: + """SIGTERM → wait ``grace_s`` → SIGKILL across local pids and their + descendant process trees. + + Delegates per-pid to ``areal.infra.utils.proc.kill_process_tree``, + which walks ``psutil.Process(pid).children(recursive=True)`` so + grandchildren that escaped the original process group are still + caught. Local-only — for remote backends use + ``areal.experimental.cli.scheduler.terminate``. + """ + + for pid in pids: + if pid > 0: + kill_process_tree(pid, timeout=int(grace_s), graceful=True) diff --git a/areal/experimental/cli/scheduler.py b/areal/experimental/cli/scheduler.py new file mode 100644 index 0000000000..8b9c9224aa --- /dev/null +++ b/areal/experimental/cli/scheduler.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Unified termination dispatch for service-style CLIs. + +Provides a stateless top-level ``terminate`` that takes a TaskHandle's +``ref`` dict plus the backend tag, and routes to the matching kill +path. Currently only the local backend is implemented; future backends +extend this module with their own branches. +""" + +from __future__ import annotations + +from areal.experimental.cli.process import kill_pids + + +def terminate( + ref: dict, + *, + backend: str = "local", + grace_s: float = 10.0, +) -> None: + """Terminate the task identified by ``ref`` using the given backend. + + For ``backend="local"`` ``ref`` must contain a ``"pid"`` key — the + local PID and its descendant process tree are SIGTERM'd, given + ``grace_s`` seconds to exit, then SIGKILL'd. + """ + + if backend == "local": + pid = int(ref.get("pid", 0) or 0) + if pid > 0: + kill_pids([pid], grace_s=grace_s) + return + + raise ValueError(f"unknown scheduler backend: {backend!r}") diff --git a/areal/experimental/cli/state.py b/areal/experimental/cli/state.py new file mode 100644 index 0000000000..251946760b --- /dev/null +++ b/areal/experimental/cli/state.py @@ -0,0 +1,280 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""On-disk state primitives shared across service-style CLIs. + +Three layers live here: + +1. **Global helpers** — ``areal_home`` (the user's CLI root) and + ``atomic_write_json`` (a generic write primitive). Both are + stateless; no namespace involvement. + +2. **NamespacedStateStore** — every subcommand CLI binds a namespace + (``inf``, ``agent``, ``train``, …) and lives under + ``$AREAL_HOME//``. The store class collects the path + resolution / pointer file / orphan-recovery operations so callers + construct one instance per namespace and use methods on it rather + than threading the namespace string through every call site. + +3. **Contract types** — ``SupportsComponentProbe`` (Protocol) and + ``ServiceStateBase`` (ABC). Subcommand CLIs implement their own + ServiceState dataclass that satisfies the protocol / base class, and + in return get to plug into scaffold's ``ServiceLifecycle`` / + ``StatusReporter`` / etc. without further glue. +""" + +from __future__ import annotations + +import json +import os +import tempfile +from abc import ABC, abstractmethod +from collections.abc import Iterable +from pathlib import Path +from typing import Any, Protocol, runtime_checkable + +DEFAULT_SERVICE = "default" + + +# --------------------------------------------------------------------------- +# Global helpers +# --------------------------------------------------------------------------- + + +def areal_home() -> Path: + """Return the AReaL CLI home directory. + + Resolves ``$AREAL_HOME`` if set, otherwise ``~/.areal``. Created on + first access so callers can mkdir-then-write subdirs without an + explicit setup step. + """ + + env = os.environ.get("AREAL_HOME") + root = Path(env).expanduser() if env else Path.home() / ".areal" + root.mkdir(parents=True, exist_ok=True) + return root + + +def atomic_write_json(path: Path, data: Any, *, indent: int = 2) -> None: + """Atomically write ``data`` as JSON to ``path``. + + Writes to a unique tempfile in ``path``'s directory, fsync()s it to + disk, then renames into place. ``NamedTemporaryFile(delete=False)`` + gives us a fresh name per call so concurrent writers do not stomp on + each other's tempfiles, and the tempfile is unlinked on serialization + or rename failure so half-written state never lingers on disk. + """ + + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + "w", dir=path.parent, delete=False, suffix=".tmp" + ) as f: + tmp_path = Path(f.name) + try: + json.dump(data, f, indent=indent, default=str) + f.write("\n") + f.flush() + os.fsync(f.fileno()) + except Exception: + tmp_path.unlink(missing_ok=True) + raise + try: + os.replace(tmp_path, path) + except Exception: + tmp_path.unlink(missing_ok=True) + raise + + +# --------------------------------------------------------------------------- +# Namespace-bound state store +# --------------------------------------------------------------------------- + + +class NamespacedStateStore: + """All ``$AREAL_HOME//...`` path resolution + pointer + file management for one subcommand CLI. + + Construct one per CLI (e.g. ``store = NamespacedStateStore("inf")``) + and use the methods on it. Paths are re-resolved on every call so + tests that override the home directory via ``AREAL_HOME`` work + without rebuilding the store. + """ + + def __init__(self, namespace: str) -> None: + if not namespace: + raise ValueError("NamespacedStateStore requires a non-empty namespace") + self.namespace = namespace + + # --- directory roots ------------------------------------------------- + + def root(self) -> Path: + d = areal_home() / self.namespace + d.mkdir(parents=True, exist_ok=True) + return d + + def services_dir(self) -> Path: + d = self.root() / "services" + d.mkdir(parents=True, exist_ok=True) + return d + + def logs_root(self) -> Path: + d = self.root() / "logs" + d.mkdir(parents=True, exist_ok=True) + return d + + def logs_dir(self, service: str) -> Path: + d = self.logs_root() / service + d.mkdir(parents=True, exist_ok=True) + return d + + # --- file paths ------------------------------------------------------ + + def service_state_path(self, service: str) -> Path: + return self.services_dir() / f"{service}.json" + + def service_lock_path(self, service: str) -> Path: + return self.services_dir() / f"{service}.lock" + + def current_service_path(self) -> Path: + return self.root() / "current-service" + + def config_path(self) -> Path: + return self.root() / "config.toml" + + # --- pointer file ---------------------------------------------------- + + def list_service_names(self) -> list[str]: + return sorted(p.stem for p in self.services_dir().glob("*.json")) + + def set_current_service(self, service: str) -> None: + self.current_service_path().write_text(service + "\n") + + def clear_current_service(self, service: str) -> None: + path = self.current_service_path() + if path.exists() and path.read_text().strip() == service: + path.unlink() + + def resolve_service_name( + self, + explicit: str | None = None, + *, + fallback: str = DEFAULT_SERVICE, + ) -> str: + """Resolve the active service name for a CLI call. + + Order: ``--service`` flag > current-service pointer file > the + single running service (if exactly one) > ``fallback``. + """ + + if explicit: + return explicit + pointer = self.current_service_path() + if pointer.exists(): + value = pointer.read_text().strip() + if value: + return value + running = self.list_service_names() + if len(running) == 1: + return running[0] + return fallback + + # --- best-effort orphan PID recovery --------------------------------- + + def recover_pids_from_raw_state(self, service: str) -> list[int]: + """Walk the service-state file for ``service`` and pull any + ``pid`` / ``pids`` numbers. + + Used by ``run --force`` to clean up children when the dataclass + parse fails (state file from an older / corrupted schema). + Subclasses with extra state files (e.g. inf's + ``models/.json``) override to walk the additional files. + """ + + pids: list[int] = [] + pid_keys = {"pid", "pids"} + + def add(value) -> None: + if isinstance(value, int) and value > 0: + pids.append(value) + elif isinstance(value, list): + for item in value: + add(item) + + def walk(value) -> None: + if isinstance(value, dict): + for key, item in value.items(): + if key in pid_keys: + add(item) + else: + walk(item) + elif isinstance(value, list): + for item in value: + walk(item) + + path = self.service_state_path(service) + if path.exists(): + with open(path) as f: + walk(json.load(f)) + + seen: set[int] = set() + unique: list[int] = [] + for pid in pids: + if pid not in seen: + seen.add(pid) + unique.append(pid) + return unique + + +# --------------------------------------------------------------------------- +# Contract types +# --------------------------------------------------------------------------- + + +@runtime_checkable +class SupportsComponentProbe(Protocol): + """Minimum surface a subcommand's component handle must expose so + scaffold helpers can probe it and identify it. + + Each subcommand CLI's handle type — inf's ``TaskHandle``, agent's + ``ProcessState``, etc. — already provides ``addr`` (HTTP base for + ``/health`` probes) and ``pid`` (for local liveness / kill paths). + No inheritance needed: structural duck typing. + """ + + @property + def addr(self) -> str: ... + + @property + def pid(self) -> int: ... + + +class ServiceStateBase(ABC): + """Abstract base every subcommand CLI's ServiceState should satisfy. + + The base nails down the universal fields and the two methods + (``gateway_alive`` / ``components``) that scaffold's lifecycle and + status reporters rely on. Subclasses are free to add backend-specific + fields (engine handles, model registries, agent pair configs, etc.). + """ + + service: str + admin_api_key: str + launch_mode: str + started_at: float + + @abstractmethod + def gateway_alive(self) -> bool: + """Return True iff the service's central entry point is reachable. + + ``ServiceLifecycle`` uses this to decide "running" — every CLI + must define what alive means for its own architecture (local PID + alive / k8s pod ready / slurm job state). + """ + + @abstractmethod + def components(self) -> Iterable[tuple[str, SupportsComponentProbe]]: + """Yield ``(label, handle)`` for every component of this service. + + Used by ``StatusReporter`` to enumerate rows; the order is the + order rows appear in the table. Labels are display-only strings + (e.g. ``"gateway"``, ``"worker[qwen/0]"``). + """ diff --git a/areal/experimental/cli/status.py b/areal/experimental/cli/status.py new file mode 100644 index 0000000000..4e60721276 --- /dev/null +++ b/areal/experimental/cli/status.py @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Status drill-down table with parallel /health probing. + +A ``StatusReporter`` is constructed with the components to inspect plus +a list of ``ColumnSpec`` describing the columns to render. It probes +every component's ``/health`` endpoint concurrently and produces either +a text table or a JSON payload. + +Both the column set and the per-column value extractors are caller- +supplied — subcommand CLIs decide what to show. Scaffold provides only +the orchestration (parallel probe + table rendering). +""" + +from __future__ import annotations + +import urllib.error +import urllib.request +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass + +import click + +from areal.experimental.cli.state import SupportsComponentProbe + + +@dataclass +class ColumnSpec: + """One column in the status table. + + ``value`` receives the row's ``(label, handle, alive)`` tuple and + returns the rendered string. Returning ``"-"`` for "not applicable" + is the conventional placeholder. + """ + + header: str + value: Callable[[str, SupportsComponentProbe, bool], str] + + +class StatusReporter: + def __init__( + self, + components: list[tuple[str, SupportsComponentProbe]], + columns: list[ColumnSpec], + *, + probe_timeout: float = 3.0, + max_workers: int = 8, + ) -> None: + self.components = list(components) + self.columns = list(columns) + self.probe_timeout = probe_timeout + self.max_workers = max_workers + + def probe_all(self) -> list[bool]: + """Probe every component's ``/health`` endpoint concurrently. + + Returns a list of bools aligned with ``self.components``. A + non-5xx response within ``probe_timeout`` counts as alive; any + network error or 5xx counts as not alive. + """ + + if not self.components: + return [] + addrs = [handle.addr or "" for _, handle in self.components] + + def probe(addr: str) -> bool: + if not addr: + return False + try: + with urllib.request.urlopen( + f"{addr.rstrip('/')}/health", timeout=self.probe_timeout + ) as resp: + return resp.status < 500 + except ( + urllib.error.URLError, + ConnectionError, + TimeoutError, + OSError, + ): + return False + + with ThreadPoolExecutor( + max_workers=max(1, min(self.max_workers, len(addrs))) + ) as pool: + return list(pool.map(probe, addrs)) + + def render_rows(self, alive_flags: list[bool] | None = None) -> list[list[str]]: + """Build per-row string lists using each column's value extractor.""" + + if alive_flags is None: + alive_flags = self.probe_all() + rows: list[list[str]] = [] + for (label, handle), alive in zip(self.components, alive_flags, strict=True): + rows.append([col.value(label, handle, alive) for col in self.columns]) + return rows + + def print_table( + self, + rows: list[list[str]] | None = None, + *, + header_line: str | None = None, + ) -> None: + """Print a left-aligned table with column widths derived from + the longest cell in each column. Optional ``header_line`` is + echoed before the table (e.g. ``"service: foo backend: local"``). + """ + + if rows is None: + rows = self.render_rows() + if header_line: + click.echo(header_line) + click.echo() + headers = [c.header for c in self.columns] + widths = [ + max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(headers) + ] + fmt = " ".join(f"{{:<{w}}}" for w in widths) + click.echo(fmt.format(*headers)) + for row in rows: + click.echo(fmt.format(*row)) + + def json_snapshot(self, alive_flags: list[bool] | None = None) -> list[dict]: + """Return the table content as a list of dicts (one per row) + keyed by column header — convenient for ``--json`` output.""" + + if alive_flags is None: + alive_flags = self.probe_all() + snapshot = [] + for (label, handle), alive in zip(self.components, alive_flags, strict=True): + snapshot.append( + {col.header: col.value(label, handle, alive) for col in self.columns} + ) + return snapshot diff --git a/areal/experimental/cli/utils.py b/areal/experimental/cli/utils.py new file mode 100644 index 0000000000..16a11ba203 --- /dev/null +++ b/areal/experimental/cli/utils.py @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Stateless utility helpers shared across subcommand CLIs. + +Four groups live here: + +- :func:`file_lock` — per-service mutual exclusion via POSIX flock. +- :func:`wait_http_health` / :func:`wait_client_health` — block until a + newly-spawned component reports ready. +- :func:`json_or_table` — ``--json`` vs table dual-output dispatch. +- :func:`register_cli_logger` — register a CLI logger name + color in + the global color table and return the configured Logger. +""" + +from __future__ import annotations + +import fcntl +import json +import logging +import time +import urllib.error +import urllib.request +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Protocol + +import click + +from areal.experimental.cli.process import pid_alive +from areal.utils.logging import LOGGER_COLORS_EXACT, getLogger + +# --------------------------------------------------------------------------- +# File locking +# --------------------------------------------------------------------------- + + +@contextmanager +def file_lock(path: Path) -> Iterator[None]: + """Hold an exclusive flock on ``path`` for the duration of the ``with`` block. + + The lock file is created if missing; concurrent waiters block until + the current holder exits the ``with`` block. Used by subcommand CLIs + to serialize concurrent ``register`` / ``deregister`` / ``stop`` + calls against the same service's state file. + """ + + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "a+") as fp: + fcntl.flock(fp.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(fp.fileno(), fcntl.LOCK_UN) + + +# --------------------------------------------------------------------------- +# Health polling +# --------------------------------------------------------------------------- + + +class _HealthCheckable(Protocol): + def health(self, *, timeout: float) -> object: ... + + +def wait_http_health( + url: str, + *, + pid: int, + timeout: float, + label: str, + poll_interval: float = 0.5, + request_timeout: float = 2.0, +) -> None: + """Poll ``GET /health`` until it returns < 5xx. + + Raises ``ClickException`` if the deadline elapses, or if the spawned + PID dies during startup (cheap early-fail signal for the local + case). Pass ``pid=0`` for non-local backends to skip the PID check. + """ + + deadline = time.time() + timeout + last_err: Exception | None = None + while time.time() < deadline: + if pid > 0 and not pid_alive(pid): + raise click.ClickException(f"{label} subprocess died during startup") + try: + with urllib.request.urlopen( + f"{url.rstrip('/')}/health", timeout=request_timeout + ) as resp: + if resp.status < 500: + return + except (urllib.error.URLError, ConnectionError, TimeoutError, OSError) as exc: + last_err = exc + time.sleep(poll_interval) + raise click.ClickException(f"{label} did not become healthy: {last_err}") + + +def wait_client_health( + client: _HealthCheckable, + *, + timeout: float, + label: str, + poll_interval: float = 0.3, + request_timeout: float = 1.5, +) -> None: + """Poll ``client.health(timeout=...)`` until it returns without raising. + + Any exception from ``client.health`` is treated as "not ready yet" + and the loop sleeps until the deadline. + """ + + deadline = time.time() + timeout + last_err: Exception | None = None + while time.time() < deadline: + try: + client.health(timeout=request_timeout) + return + except Exception as exc: + last_err = exc + time.sleep(poll_interval) + raise click.ClickException( + f"{label} did not become healthy within {timeout:.0f}s (last error: {last_err})" + ) + + +# --------------------------------------------------------------------------- +# Output dispatch +# --------------------------------------------------------------------------- + + +def json_or_table( + payload: Any, + *, + as_json: bool, + table_renderer: Callable[[Any], None], + indent: int = 2, +) -> None: + """Emit ``payload`` as JSON or hand it to a table renderer. + + Most subcommand ``ps`` / ``status`` / ``models`` verbs follow the + same pattern: ``--json`` dumps the raw payload, otherwise a table + is rendered. Funneling both through this helper keeps the branching + out of the command body. + """ + + if as_json: + click.echo(json.dumps(payload, indent=indent, default=str)) + return + table_renderer(payload) + + +# --------------------------------------------------------------------------- +# Logger registration +# --------------------------------------------------------------------------- + + +def register_cli_logger(name: str, color: str = "blue") -> logging.Logger: + """Register ``name`` with ``color`` in ``areal.utils.logging``'s + color table and return the configured Logger. + + Service-style CLIs conventionally use ``"blue"`` (infrastructure / + scheduler category). See ``areal/utils/logging.py`` for the palette + if a different color fits. + """ + + LOGGER_COLORS_EXACT[name] = color + return getLogger(name) diff --git a/areal/experimental/cli/watcher.py b/areal/experimental/cli/watcher.py new file mode 100644 index 0000000000..806717a6b5 --- /dev/null +++ b/areal/experimental/cli/watcher.py @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Foreground watcher with signal-aware teardown. + +Subcommand CLIs use this in their ``run`` (non-detach) path to block +until the service exits, with a clear distinction between: + + - SIGINT / SIGTERM → user-requested stop → run the teardown callback + (kill workers, remove state file) → return 0 + - SIGHUP → terminal disconnect → exit quietly without teardown; + workers spawned with ``start_new_session=True`` survive + +Subclasses or constructor kwargs can swap the SIGHUP semantics for +``"teardown"`` if a particular CLI wants the older "all signals tear +down" behavior. +""" + +from __future__ import annotations + +import signal +import time +from collections.abc import Callable +from typing import Literal + +Disposition = Literal["teardown", "detach"] + + +class ForegroundWatcher: + """Block the calling thread until the service exits. + + Construct with an ``is_alive`` callback (typically + ``lambda: lifecycle.gateway_alive(state)``) and a ``teardown`` + callback (kills workers + removes state file). Call ``watch()`` — + it returns the desired CLI exit code. + """ + + default_signal_dispositions: dict[int, Disposition] = { + signal.SIGTERM: "teardown", + } + if hasattr(signal, "SIGHUP"): + default_signal_dispositions[signal.SIGHUP] = "detach" + + def __init__( + self, + *, + is_alive: Callable[[], bool], + teardown: Callable[[], None], + idle_poll: float = 1.0, + service_name: str = "", + signal_dispositions: dict[int, Disposition] | None = None, + ) -> None: + self.is_alive = is_alive + self.teardown = teardown + self.idle_poll = idle_poll + self.service_name = service_name + self.dispositions = ( + dict(signal_dispositions) + if signal_dispositions is not None + else dict(self.default_signal_dispositions) + ) + + def watch(self) -> int: + previous = self._install_handlers() + try: + while self.is_alive(): + time.sleep(self.idle_poll) + except KeyboardInterrupt: + # SIGINT (Ctrl+C) plus any SIGTERM-dispatched teardown lands here. + self.teardown() + return 0 + except SystemExit as exc: + # SIGHUP-dispatched detach path lands here. + return exc.code if isinstance(exc.code, int) else 0 + except BaseException: + self.teardown() + raise + finally: + self._restore_handlers(previous) + # is_alive() flipped to False on its own — gateway died externally. + # We do not call teardown; the children are already gone, but the + # caller may want to clean up state (handled outside this class). + return 0 + + # ------------------------------------------------------------------ + # Signal plumbing + + def _install_handlers(self) -> dict[int, object]: + previous: dict[int, object] = {} + for sig, disposition in self.dispositions.items(): + previous[sig] = signal.getsignal(sig) + signal.signal(sig, self._handler_for(disposition)) + return previous + + def _restore_handlers(self, previous: dict[int, object]) -> None: + for sig, handler in previous.items(): + signal.signal(sig, handler) + + def _handler_for(self, disposition: Disposition): + if disposition == "teardown": + + def teardown_handler(signum, frame): # noqa: ARG001 + raise KeyboardInterrupt + + return teardown_handler + + def detach_handler(signum, frame): # noqa: ARG001 + raise SystemExit(0) + + return detach_handler diff --git a/pyproject.toml b/pyproject.toml index 1d90e3e717..9a3b8333b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,6 +183,13 @@ cuda = [ sandbox = [ "daytona>=0.167.0", ] +cli = [ + "click>=8.1", + "colorlog", +] + +[project.scripts] +areal = "areal.experimental.cli.main:cli" [project.urls] "Homepage" = "https://github.com/areal-project/AReaL" diff --git a/pyproject.vllm.toml b/pyproject.vllm.toml index 4ab022c367..3bcdd12f59 100644 --- a/pyproject.vllm.toml +++ b/pyproject.vllm.toml @@ -194,6 +194,13 @@ cuda = [ sandbox = [ "daytona>=0.167.0", ] +cli = [ + "click>=8.1", + "colorlog", +] + +[project.scripts] +areal = "areal.experimental.cli.main:cli" [project.urls] "Homepage" = "https://github.com/areal-project/AReaL" diff --git a/uv.lock b/uv.lock index e468608cc3..389832ba4e 100644 --- a/uv.lock +++ b/uv.lock @@ -400,6 +400,10 @@ dependencies = [ ] [package.optional-dependencies] +cli = [ + { name = "click", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "colorlog", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] cuda = [ { name = "flash-linear-attention", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "kernels", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -480,8 +484,10 @@ requires-dist = [ { name = "blosc" }, { name = "build", specifier = ">=1.2.1" }, { name = "claude-agent-sdk" }, + { name = "click", marker = "extra == 'cli'", specifier = ">=8.1" }, { name = "colorama" }, { name = "colorlog" }, + { name = "colorlog", marker = "extra == 'cli'" }, { name = "cookiecutter", specifier = ">2.1.1" }, { name = "datasets", specifier = ">=3.0.0" }, { name = "daytona", marker = "extra == 'sandbox'", specifier = ">=0.167.0" }, @@ -573,7 +579,7 @@ requires-dist = [ { name = "word2number" }, { name = "zstandard" }, ] -provides-extras = ["sglang", "tms", "kernels", "megatron", "cuda-train", "cuda", "sandbox"] +provides-extras = ["sglang", "tms", "kernels", "megatron", "cuda-train", "cuda", "sandbox", "cli"] [package.metadata.requires-dev] dev = [ diff --git a/uv.vllm.lock b/uv.vllm.lock index 9e9b587e61..bbd554432d 100644 --- a/uv.vllm.lock +++ b/uv.vllm.lock @@ -448,6 +448,10 @@ dependencies = [ ] [package.optional-dependencies] +cli = [ + { name = "click", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "colorlog", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] cuda = [ { name = "flash-linear-attention", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "kernels", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -526,8 +530,10 @@ requires-dist = [ { name = "blosc" }, { name = "build", specifier = ">=1.2.1" }, { name = "claude-agent-sdk" }, + { name = "click", marker = "extra == 'cli'", specifier = ">=8.1" }, { name = "colorama" }, { name = "colorlog" }, + { name = "colorlog", marker = "extra == 'cli'" }, { name = "cookiecutter", specifier = ">2.1.1" }, { name = "datasets", specifier = ">=3.0.0" }, { name = "daytona", marker = "extra == 'sandbox'", specifier = ">=0.167.0" }, @@ -619,7 +625,7 @@ requires-dist = [ { name = "word2number" }, { name = "zstandard" }, ] -provides-extras = ["vllm", "tms", "kernels", "megatron", "cuda-train", "cuda", "sandbox"] +provides-extras = ["vllm", "tms", "kernels", "megatron", "cuda-train", "cuda", "sandbox", "cli"] [package.metadata.requires-dev] dev = [ From ae669922ead55766fdf805006dded4a11c5225d0 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:06:48 +0800 Subject: [PATCH 14/36] refactor: move 5 experimental modules into areal/v2 for 2.0 release (#1448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: move 5 experimental modules into areal/v2 for 2.0 release Move agent_service, inference_service, training_service, weight_update, and cli from areal/experimental/ to areal/v2/, and rewrite every reference (Python imports, `python -m` invocations, console-script entry points, CODEOWNERS, docs, review-pr signals) to the new path. - 5 directories migrated via `git mv` (history preserved) - 70 files modified across areal/, tests/, examples/, pyproject{,vllm}.toml, .github/CODEOWNERS, ROADMAP.md, and tooling docs - `areal/v2/__init__.py` added so v2 is an importable package - Build config (tool.uv.build-backend with module-root="") auto-discovers the new package — no pyproject changes beyond the entry point Co-Authored-By: Claude Opus 4.7 * style: apply pre-commit auto-fixes after areal/v2 move CI pre-commit job reformatted 54 files automatically: - .github/CODEOWNERS: realign owner columns (32-col) after shorter /areal/v2/ paths broke the previous 40-col alignment - areal/**, tests/**, examples/**, docs/**: ruff isort reorders `areal.v2.*` imports into their new alphabetical slot (between `areal.engine` and `areal.infra`) - markdown/yaml whitespace normalized by mdformat / ruff-format Pure formatting; no logic change. `pre-commit run --all-files` is now green locally. Co-Authored-By: Claude Opus 4.7 * fix(tests): point py<3.12 conftest stubs at areal/v2/ Two test conftest.py stubs (Python 3.10/3.11 compat) still had the `areal/experimental/{inference_service,weight_update}` path as the namespace package's __path__, broken by the v2 move. The sed pass missed them because the paths were comma-separated `os.path.join` args (`"areal", "experimental", "X"`), not slash-form path strings. Additionally insert an `areal.v2` stub between `areal` and the leaf package so the parent→child attribute wiring loop (which uses `name.rsplit(".", 1)`) can find a parent module in sys.modules. Without it `setattr(parent, child, ...)` silently no-ops and `unittest.mock.patch` traversal breaks on the new path. Spotted by gemini-code-assist on PR #1448. Co-Authored-By: Claude Opus 4.7 * refactor(tests): mirror areal/v2 move under tests/v2 Move agent_service, inference_service, training_service, and weight_update test directories from tests/experimental/ to tests/v2/ to mirror the source-tree layout. tests/experimental/ retains archon/ and openai/ (still-experimental modules). - 50 files migrated via `git mv` (history preserved) - tests/v2/__init__.py added - 9 files rewritten for the new dotted/slashed test paths: `tests.experimental.{4 modules}` → `tests.v2.{4 modules}` `tests/experimental/{4 modules}` → `tests/v2/{4 modules}` (covers integration_utils imports, fake_train_engine engine_class, pytest invocation strings in docstrings, and the tests/v2/weight_update/torchrun/run_nccl_weight_transfer.py path) conftest stubs (Python <3.12 namespace shims) keep working because _REPO_ROOT is computed via "..", "..", ".." from the conftest file — same depth under tests/v2/X/ as under tests/experimental/X/. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- .agents/skills/review-pr/MIGRATION.md | 4 +- .../review-pr-domains-and-signals.md | 32 ++++++------- .../skills/upgrade-deps/checklists/sglang.md | 46 +++++++++---------- .claude/data/review-pr-domains-and-signals.md | 32 ++++++------- .github/CODEOWNERS | 42 ++++++++--------- .../data/review-pr-domains-and-signals.md | 32 ++++++------- ROADMAP.md | 4 +- areal/api/cli_args.py | 6 +-- areal/engine/sglang_remote.py | 2 +- areal/engine/vllm_remote.py | 2 +- areal/infra/rpc/guard/__init__.py | 2 +- areal/infra/rpc/guard/app.py | 2 +- areal/trainer/dpo/dpo_engine.py | 6 +-- areal/trainer/ppo/actor.py | 6 +-- areal/trainer/ppo/critic.py | 6 +-- areal/trainer/rl_trainer.py | 6 +-- areal/trainer/rw/rw_engine.py | 6 +-- areal/trainer/sft/lm_engine.py | 6 +-- areal/utils/recover.py | 2 +- areal/{experimental/cli => v2}/__init__.py | 0 .../agent_service/README.md | 12 ++--- .../agent_service/__init__.py | 0 .../agent_service/auth.py | 0 .../agent_service/controller/__init__.py | 0 .../agent_service/controller/controller.py | 12 ++--- .../agent_service/data_proxy/__init__.py | 0 .../agent_service/data_proxy/__main__.py | 2 +- .../agent_service/data_proxy/app.py | 0 .../agent_service/data_proxy/client.py | 0 .../agent_service/data_proxy/config.py | 0 .../agent_service/gateway/__init__.py | 0 .../agent_service/gateway/__main__.py | 2 +- .../agent_service/gateway/app.py | 0 .../agent_service/gateway/bridge.py | 0 .../agent_service/gateway/config.py | 0 .../agent_service/guard/__init__.py | 4 +- .../agent_service/guard/__main__.py | 10 ++-- .../agent_service/guard/app.py | 2 +- .../agent_service/protocol.py | 0 .../agent_service/router/__init__.py | 0 .../agent_service/router/__main__.py | 2 +- .../agent_service/router/app.py | 0 .../agent_service/router/client.py | 0 .../agent_service/router/config.py | 0 .../agent_service/types.py | 0 .../agent_service/worker/__init__.py | 0 .../agent_service/worker/__main__.py | 4 +- .../agent_service/worker/app.py | 0 .../agent_service/worker/config.py | 0 .../cli/commands => v2/cli}/__init__.py | 0 areal/{experimental => v2}/cli/__main__.py | 2 +- areal/{experimental => v2}/cli/cli.py | 4 +- areal/{experimental => v2}/cli/client.py | 0 .../cli/commands}/__init__.py | 0 .../{experimental => v2}/cli/commands/logs.py | 6 +-- areal/{experimental => v2}/cli/config.py | 2 +- areal/{experimental => v2}/cli/lifecycle.py | 4 +- areal/{experimental => v2}/cli/main.py | 2 +- areal/{experimental => v2}/cli/process.py | 4 +- areal/{experimental => v2}/cli/scheduler.py | 2 +- areal/{experimental => v2}/cli/state.py | 0 areal/{experimental => v2}/cli/status.py | 2 +- areal/{experimental => v2}/cli/utils.py | 2 +- areal/{experimental => v2}/cli/watcher.py | 0 .../inference_service/__init__.py | 2 +- .../inference_service/backend.py | 0 .../inference_service/controller/__init__.py | 2 +- .../controller/controller.py | 14 +++--- .../inference_service/controller/workflow.py | 4 +- .../inference_service/data_proxy}/__init__.py | 0 .../inference_service/data_proxy/__main__.py | 6 +-- .../inference_service/data_proxy/app.py | 38 +++++++-------- .../inference_service/data_proxy/config.py | 0 .../inference_service/data_proxy/pause.py | 0 .../inference_service/data_proxy/session.py | 0 .../data_proxy/tokenizer_proxy.py | 0 .../inference_service/gateway}/__init__.py | 0 .../inference_service/gateway/__main__.py | 6 +-- .../inference_service/gateway/app.py | 10 ++-- .../inference_service/gateway/auth.py | 0 .../inference_service/gateway/config.py | 0 .../inference_service/gateway/streaming.py | 0 .../inference_service/guard}/__init__.py | 0 .../inference_service/guard/__main__.py | 10 ++-- .../inference_service/guard/app.py | 0 .../inference_service/inf_bridge.py | 4 +- .../inference_service/router}/__init__.py | 0 .../inference_service/router/__main__.py | 6 +-- .../inference_service/router/app.py | 10 ++-- .../inference_service/router/config.py | 0 .../inference_service/router/state.py | 0 .../inference_service/router/strategies.py | 2 +- .../inference_service/sglang}/__init__.py | 0 .../inference_service/sglang/awex.py | 2 +- .../inference_service/sglang/bridge.py | 0 .../inference_service/sglang/launch_server.py | 6 +-- .../inference_service/sglang/pp_bridge.py | 0 .../inference_service/sglang/rpc_proxy.py | 0 .../inference_service/sglang/scheduler.py | 4 +- .../inference_service/vllm}/__init__.py | 0 .../inference_service/vllm/bridge.py | 0 .../training_service/__init__.py | 0 .../training_service/controller}/__init__.py | 0 .../training_service/controller/controller.py | 16 +++---- .../training_service/data_proxy}/__init__.py | 0 .../training_service/data_proxy/__main__.py | 4 +- .../training_service/data_proxy/app.py | 8 ++-- .../training_service/data_proxy/config.py | 0 .../training_service/data_proxy/dispatcher.py | 2 +- .../training_service/data_proxy/engine.py | 0 .../training_service/data_proxy/topology.py | 0 .../training_service/gateway}/__init__.py | 0 .../training_service/gateway/__main__.py | 4 +- .../training_service/gateway/app.py | 8 ++-- .../training_service/gateway/auth.py | 0 .../training_service/gateway/config.py | 0 .../training_service/gateway/engine.py | 2 +- .../training_service/gateway/streaming.py | 0 .../training_service/guard/__init__.py | 0 .../training_service/guard/__main__.py | 4 +- .../training_service/guard/app.py | 0 .../training_service/router}/__init__.py | 0 .../training_service/router/__main__.py | 4 +- .../training_service/router/app.py | 4 +- .../training_service/router/config.py | 0 .../training_service/router/state.py | 0 .../training_service/worker}/__init__.py | 0 .../training_service/worker/__main__.py | 4 +- .../training_service/worker/app.py | 6 +-- .../training_service/worker/awex.py | 4 +- .../training_service/worker/config.py | 0 .../training_service/worker/engine.py | 0 .../weight_update/__init__.py | 2 +- .../weight_update/awex/__init__.py | 0 .../weight_update/awex/fsdp_adapter.py | 8 ++-- .../weight_update/awex/megatron_adapter.py | 8 ++-- .../weight_update/awex/sglang_adapter.py | 8 ++-- .../weight_update/controller/__init__.py | 4 +- .../weight_update/controller/config.py | 0 .../weight_update/controller/controller.py | 10 ++-- .../v2/weight_update/gateway}/__init__.py | 0 .../weight_update/gateway/__main__.py | 6 +-- .../weight_update/gateway/app.py | 14 +++--- .../weight_update/gateway/auth.py | 0 .../weight_update/gateway/config.py | 0 .../weight_update/gateway/kv_store.py | 0 .../weight_update/gateway/pair_registry.py | 2 +- .../weight_update/inference_adapter.py | 0 .../weight_update/nccl_group.py | 0 .../weight_update/training_adapter.py | 0 examples/agent_service/README.md | 2 +- examples/agent_service/agent.py | 4 +- examples/agent_service/run_agent_service.py | 2 +- .../inference_service/online_rollout.py | 6 +-- .../inference_service/tau2_rollout.py | 4 +- .../inference_service/tau2_rollout.yaml | 2 +- pyproject.toml | 2 +- pyproject.vllm.toml | 2 +- tests/test_examples.py | 2 +- tests/test_recover.py | 4 +- tests/test_sglang_pp_unit.py | 8 ++-- tests/v2/__init__.py | 1 + .../agent_service/__init__.py | 0 .../agent_service/test_agent_router.py | 6 +-- .../agent_service/test_agent_worker.py | 6 +-- .../agent_service/test_controller.py | 9 ++-- .../agent_service/test_guard.py | 6 +-- .../agent_service/test_integration.py | 22 ++++----- .../agent_service/test_protocol.py | 2 +- .../inference_service/__init__.py | 0 .../inference_service/conftest.py | 7 +-- .../inference_service/integration_utils.py | 0 .../inference_service/test_controller.py | 18 ++++---- .../test_controller_integration.py | 32 +++++-------- .../test_controller_version.py | 2 +- .../inference_service/test_data_proxy_chat.py | 14 +++--- .../test_data_proxy_integration.py | 36 +++++++-------- .../test_data_proxy_pause.py | 12 ++--- .../test_data_proxy_rtensor.py | 8 ++-- .../test_data_proxy_standalone.py | 18 ++++---- .../test_data_proxy_version.py | 12 ++--- .../inference_service/test_examples.py | 0 .../inference_service/test_external_model.py | 28 +++++------ .../test_external_model_integration.py | 20 ++++---- .../inference_service/test_gateway.py | 8 ++-- .../test_gateway_integration.py | 28 +++++------ .../inference_service/test_guard.py | 8 ++-- .../inference_service/test_image_input.py | 14 +++--- .../inference_service/test_inf_bridge.py | 8 ++-- .../test_ipv6_entrypoints.py | 4 +- .../inference_service/test_online_stack.py | 24 +++++----- .../inference_service/test_router.py | 8 ++-- .../test_sglang_multinode.py | 0 .../inference_service/test_vllm_multinode.py | 0 .../training_service/__init__.py | 0 .../training_service/fake_train_engine.py | 0 .../test_controller_integration.py | 6 +-- .../training_service/test_controller_unit.py | 4 +- .../training_service/test_data_proxy_unit.py | 12 ++--- .../training_service/test_gateway_unit.py | 8 ++-- .../training_service/test_router_unit.py | 4 +- .../training_service/test_worker_unit.py | 22 ++++----- .../torchrun => v2/weight_update}/__init__.py | 0 .../weight_update/conftest.py | 7 +-- .../weight_update/test_disk_integration.py | 18 ++++---- .../weight_update/test_kv_store.py | 2 +- .../weight_update/test_nccl_integration.py | 42 ++++++++--------- .../weight_update/test_pair_registry.py | 4 +- .../weight_update/test_sglang_integration.py | 4 +- .../weight_update/test_wu_controller.py | 6 +-- tests/v2/weight_update/torchrun/__init__.py | 1 + .../weight_update/torchrun/dist_utils.py | 0 .../torchrun/run_nccl_weight_transfer.py | 6 +-- 213 files changed, 558 insertions(+), 569 deletions(-) rename areal/{experimental/cli => v2}/__init__.py (100%) rename areal/{experimental => v2}/agent_service/README.md (93%) rename areal/{experimental => v2}/agent_service/__init__.py (100%) rename areal/{experimental => v2}/agent_service/auth.py (100%) rename areal/{experimental => v2}/agent_service/controller/__init__.py (100%) rename areal/{experimental => v2}/agent_service/controller/controller.py (97%) rename areal/{experimental => v2}/agent_service/data_proxy/__init__.py (100%) rename areal/{experimental => v2}/agent_service/data_proxy/__main__.py (94%) rename areal/{experimental => v2}/agent_service/data_proxy/app.py (100%) rename areal/{experimental => v2}/agent_service/data_proxy/client.py (100%) rename areal/{experimental => v2}/agent_service/data_proxy/config.py (100%) rename areal/{experimental => v2}/agent_service/gateway/__init__.py (100%) rename areal/{experimental => v2}/agent_service/gateway/__main__.py (96%) rename areal/{experimental => v2}/agent_service/gateway/app.py (100%) rename areal/{experimental => v2}/agent_service/gateway/bridge.py (100%) rename areal/{experimental => v2}/agent_service/gateway/config.py (100%) rename areal/{experimental => v2}/agent_service/guard/__init__.py (76%) rename areal/{experimental => v2}/agent_service/guard/__main__.py (80%) rename areal/{experimental => v2}/agent_service/guard/app.py (91%) rename areal/{experimental => v2}/agent_service/protocol.py (100%) rename areal/{experimental => v2}/agent_service/router/__init__.py (100%) rename areal/{experimental => v2}/agent_service/router/__main__.py (95%) rename areal/{experimental => v2}/agent_service/router/app.py (100%) rename areal/{experimental => v2}/agent_service/router/client.py (100%) rename areal/{experimental => v2}/agent_service/router/config.py (100%) rename areal/{experimental => v2}/agent_service/types.py (100%) rename areal/{experimental => v2}/agent_service/worker/__init__.py (100%) rename areal/{experimental => v2}/agent_service/worker/__main__.py (89%) rename areal/{experimental => v2}/agent_service/worker/app.py (100%) rename areal/{experimental => v2}/agent_service/worker/config.py (100%) rename areal/{experimental/cli/commands => v2/cli}/__init__.py (100%) rename areal/{experimental => v2}/cli/__main__.py (64%) rename areal/{experimental => v2}/cli/cli.py (72%) rename areal/{experimental => v2}/cli/client.py (100%) rename areal/{experimental/inference_service/data_proxy => v2/cli/commands}/__init__.py (100%) rename areal/{experimental => v2}/cli/commands/logs.py (93%) rename areal/{experimental => v2}/cli/config.py (98%) rename areal/{experimental => v2}/cli/lifecycle.py (97%) rename areal/{experimental => v2}/cli/main.py (72%) rename areal/{experimental => v2}/cli/process.py (96%) rename areal/{experimental => v2}/cli/scheduler.py (94%) rename areal/{experimental => v2}/cli/state.py (100%) rename areal/{experimental => v2}/cli/status.py (98%) rename areal/{experimental => v2}/cli/utils.py (99%) rename areal/{experimental => v2}/cli/watcher.py (100%) rename areal/{experimental => v2}/inference_service/__init__.py (60%) rename areal/{experimental => v2}/inference_service/backend.py (100%) rename areal/{experimental => v2}/inference_service/controller/__init__.py (71%) rename areal/{experimental => v2}/inference_service/controller/controller.py (99%) rename areal/{experimental => v2}/inference_service/controller/workflow.py (99%) rename areal/{experimental/inference_service/gateway => v2/inference_service/data_proxy}/__init__.py (100%) rename areal/{experimental => v2}/inference_service/data_proxy/__main__.py (92%) rename areal/{experimental => v2}/inference_service/data_proxy/app.py (98%) rename areal/{experimental => v2}/inference_service/data_proxy/config.py (100%) rename areal/{experimental => v2}/inference_service/data_proxy/pause.py (100%) rename areal/{experimental => v2}/inference_service/data_proxy/session.py (100%) rename areal/{experimental => v2}/inference_service/data_proxy/tokenizer_proxy.py (100%) rename areal/{experimental/inference_service/guard => v2/inference_service/gateway}/__init__.py (100%) rename areal/{experimental => v2}/inference_service/gateway/__main__.py (88%) rename areal/{experimental => v2}/inference_service/gateway/app.py (99%) rename areal/{experimental => v2}/inference_service/gateway/auth.py (100%) rename areal/{experimental => v2}/inference_service/gateway/config.py (100%) rename areal/{experimental => v2}/inference_service/gateway/streaming.py (100%) rename areal/{experimental/inference_service/router => v2/inference_service/guard}/__init__.py (100%) rename areal/{experimental => v2}/inference_service/guard/__main__.py (79%) rename areal/{experimental => v2}/inference_service/guard/app.py (100%) rename areal/{experimental => v2}/inference_service/inf_bridge.py (98%) rename areal/{experimental/inference_service/sglang => v2/inference_service/router}/__init__.py (100%) rename areal/{experimental => v2}/inference_service/router/__main__.py (88%) rename areal/{experimental => v2}/inference_service/router/app.py (98%) rename areal/{experimental => v2}/inference_service/router/config.py (100%) rename areal/{experimental => v2}/inference_service/router/state.py (100%) rename areal/{experimental => v2}/inference_service/router/strategies.py (94%) rename areal/{experimental/inference_service/vllm => v2/inference_service/sglang}/__init__.py (100%) rename areal/{experimental => v2}/inference_service/sglang/awex.py (98%) rename areal/{experimental => v2}/inference_service/sglang/bridge.py (100%) rename areal/{experimental => v2}/inference_service/sglang/launch_server.py (92%) rename areal/{experimental => v2}/inference_service/sglang/pp_bridge.py (100%) rename areal/{experimental => v2}/inference_service/sglang/rpc_proxy.py (100%) rename areal/{experimental => v2}/inference_service/sglang/scheduler.py (98%) rename areal/{experimental/training_service/controller => v2/inference_service/vllm}/__init__.py (100%) rename areal/{experimental => v2}/inference_service/vllm/bridge.py (100%) rename areal/{experimental => v2}/training_service/__init__.py (100%) rename areal/{experimental/training_service/data_proxy => v2/training_service/controller}/__init__.py (100%) rename areal/{experimental => v2}/training_service/controller/controller.py (98%) rename areal/{experimental/training_service/gateway => v2/training_service/data_proxy}/__init__.py (100%) rename areal/{experimental => v2}/training_service/data_proxy/__main__.py (91%) rename areal/{experimental => v2}/training_service/data_proxy/app.py (78%) rename areal/{experimental => v2}/training_service/data_proxy/config.py (100%) rename areal/{experimental => v2}/training_service/data_proxy/dispatcher.py (99%) rename areal/{experimental => v2}/training_service/data_proxy/engine.py (100%) rename areal/{experimental => v2}/training_service/data_proxy/topology.py (100%) rename areal/{experimental/training_service/router => v2/training_service/gateway}/__init__.py (100%) rename areal/{experimental => v2}/training_service/gateway/__main__.py (92%) rename areal/{experimental => v2}/training_service/gateway/app.py (92%) rename areal/{experimental => v2}/training_service/gateway/auth.py (100%) rename areal/{experimental => v2}/training_service/gateway/config.py (100%) rename areal/{experimental => v2}/training_service/gateway/engine.py (98%) rename areal/{experimental => v2}/training_service/gateway/streaming.py (100%) rename areal/{experimental => v2}/training_service/guard/__init__.py (100%) rename areal/{experimental => v2}/training_service/guard/__main__.py (77%) rename areal/{experimental => v2}/training_service/guard/app.py (100%) rename areal/{experimental/training_service/worker => v2/training_service/router}/__init__.py (100%) rename areal/{experimental => v2}/training_service/router/__main__.py (92%) rename areal/{experimental => v2}/training_service/router/app.py (97%) rename areal/{experimental => v2}/training_service/router/config.py (100%) rename areal/{experimental => v2}/training_service/router/state.py (100%) rename areal/{experimental/weight_update/gateway => v2/training_service/worker}/__init__.py (100%) rename areal/{experimental => v2}/training_service/worker/__main__.py (88%) rename areal/{experimental => v2}/training_service/worker/app.py (96%) rename areal/{experimental => v2}/training_service/worker/awex.py (97%) rename areal/{experimental => v2}/training_service/worker/config.py (100%) rename areal/{experimental => v2}/training_service/worker/engine.py (100%) rename areal/{experimental => v2}/weight_update/__init__.py (81%) rename areal/{experimental => v2}/weight_update/awex/__init__.py (100%) rename areal/{experimental => v2}/weight_update/awex/fsdp_adapter.py (98%) rename areal/{experimental => v2}/weight_update/awex/megatron_adapter.py (99%) rename areal/{experimental => v2}/weight_update/awex/sglang_adapter.py (99%) rename areal/{experimental => v2}/weight_update/controller/__init__.py (58%) rename areal/{experimental => v2}/weight_update/controller/config.py (100%) rename areal/{experimental => v2}/weight_update/controller/controller.py (97%) rename {tests/experimental/weight_update => areal/v2/weight_update/gateway}/__init__.py (100%) rename areal/{experimental => v2}/weight_update/gateway/__main__.py (88%) rename areal/{experimental => v2}/weight_update/gateway/app.py (98%) rename areal/{experimental => v2}/weight_update/gateway/auth.py (100%) rename areal/{experimental => v2}/weight_update/gateway/config.py (100%) rename areal/{experimental => v2}/weight_update/gateway/kv_store.py (100%) rename areal/{experimental => v2}/weight_update/gateway/pair_registry.py (94%) rename areal/{experimental => v2}/weight_update/inference_adapter.py (100%) rename areal/{experimental => v2}/weight_update/nccl_group.py (100%) rename areal/{experimental => v2}/weight_update/training_adapter.py (100%) create mode 100644 tests/v2/__init__.py rename tests/{experimental => v2}/agent_service/__init__.py (100%) rename tests/{experimental => v2}/agent_service/test_agent_router.py (94%) rename tests/{experimental => v2}/agent_service/test_agent_worker.py (95%) rename tests/{experimental => v2}/agent_service/test_controller.py (97%) rename tests/{experimental => v2}/agent_service/test_guard.py (91%) rename tests/{experimental => v2}/agent_service/test_integration.py (92%) rename tests/{experimental => v2}/agent_service/test_protocol.py (99%) rename tests/{experimental => v2}/inference_service/__init__.py (100%) rename tests/{experimental => v2}/inference_service/conftest.py (86%) rename tests/{experimental => v2}/inference_service/integration_utils.py (100%) rename tests/{experimental => v2}/inference_service/test_controller.py (97%) rename tests/{experimental => v2}/inference_service/test_controller_integration.py (97%) rename tests/{experimental => v2}/inference_service/test_controller_version.py (98%) rename tests/{experimental => v2}/inference_service/test_data_proxy_chat.py (98%) rename tests/{experimental => v2}/inference_service/test_data_proxy_integration.py (97%) rename tests/{experimental => v2}/inference_service/test_data_proxy_pause.py (94%) rename tests/{experimental => v2}/inference_service/test_data_proxy_rtensor.py (97%) rename tests/{experimental => v2}/inference_service/test_data_proxy_standalone.py (91%) rename tests/{experimental => v2}/inference_service/test_data_proxy_version.py (93%) rename tests/{experimental => v2}/inference_service/test_examples.py (100%) rename tests/{experimental => v2}/inference_service/test_external_model.py (96%) rename tests/{experimental => v2}/inference_service/test_external_model_integration.py (94%) rename tests/{experimental => v2}/inference_service/test_gateway.py (98%) rename tests/{experimental => v2}/inference_service/test_gateway_integration.py (97%) rename tests/{experimental => v2}/inference_service/test_guard.py (97%) rename tests/{experimental => v2}/inference_service/test_image_input.py (98%) rename tests/{experimental => v2}/inference_service/test_inf_bridge.py (98%) rename tests/{experimental => v2}/inference_service/test_ipv6_entrypoints.py (94%) rename tests/{experimental => v2}/inference_service/test_online_stack.py (92%) rename tests/{experimental => v2}/inference_service/test_router.py (98%) rename tests/{experimental => v2}/inference_service/test_sglang_multinode.py (100%) rename tests/{experimental => v2}/inference_service/test_vllm_multinode.py (100%) rename tests/{experimental => v2}/training_service/__init__.py (100%) rename tests/{experimental => v2}/training_service/fake_train_engine.py (100%) rename tests/{experimental => v2}/training_service/test_controller_integration.py (99%) rename tests/{experimental => v2}/training_service/test_controller_unit.py (97%) rename tests/{experimental => v2}/training_service/test_data_proxy_unit.py (98%) rename tests/{experimental => v2}/training_service/test_gateway_unit.py (97%) rename tests/{experimental => v2}/training_service/test_router_unit.py (97%) rename tests/{experimental => v2}/training_service/test_worker_unit.py (87%) rename tests/{experimental/weight_update/torchrun => v2/weight_update}/__init__.py (100%) rename tests/{experimental => v2}/weight_update/conftest.py (86%) rename tests/{experimental => v2}/weight_update/test_disk_integration.py (96%) rename tests/{experimental => v2}/weight_update/test_kv_store.py (97%) rename tests/{experimental => v2}/weight_update/test_nccl_integration.py (96%) rename tests/{experimental => v2}/weight_update/test_pair_registry.py (94%) rename tests/{experimental => v2}/weight_update/test_sglang_integration.py (96%) rename tests/{experimental => v2}/weight_update/test_wu_controller.py (96%) create mode 100644 tests/v2/weight_update/torchrun/__init__.py rename tests/{experimental => v2}/weight_update/torchrun/dist_utils.py (100%) rename tests/{experimental => v2}/weight_update/torchrun/run_nccl_weight_transfer.py (98%) diff --git a/.agents/skills/review-pr/MIGRATION.md b/.agents/skills/review-pr/MIGRATION.md index 473a24376f..8bcb75c3ff 100644 --- a/.agents/skills/review-pr/MIGRATION.md +++ b/.agents/skills/review-pr/MIGRATION.md @@ -104,8 +104,8 @@ ______________________________________________________________________ 1. `areal/experimental/models/archon/**` + `areal/experimental/engine/archon_engine.py` \+ `areal/experimental/engine/archon_checkpoint.py` -> Distributed Runtime + Model Compute & Attention (CRITICAL/HIGH) -1. `areal/experimental/agent_service/**` -> Service Orchestration (HIGH) -1. `areal/experimental/inference_service/**` -> Service Orchestration (HIGH) +1. `areal/v2/agent_service/**` -> Service Orchestration (HIGH) +1. `areal/v2/inference_service/**` -> Service Orchestration (HIGH) 1. `areal/experimental/engine/archon_weight_sync.py` -> Distributed Runtime (CRITICAL) 1. `areal/infra/rpc/**` -> Launcher & Infrastructure + Distributed Runtime (HIGH) 1. `areal/workflow/**`, `areal/trainer/**` -> Workflow & Trainer Contract (HIGH/MEDIUM) diff --git a/.agents/skills/review-pr/references/review-pr-domains-and-signals.md b/.agents/skills/review-pr/references/review-pr-domains-and-signals.md index a835419ef9..0ce5b3c607 100644 --- a/.agents/skills/review-pr/references/review-pr-domains-and-signals.md +++ b/.agents/skills/review-pr/references/review-pr-domains-and-signals.md @@ -52,19 +52,19 @@ ______________________________________________________________________ ## Domain 4: Service Orchestration (HIGH) -| L2 Signal | File Path Pattern | Code Pattern | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| `service_routing_dataflow` | `areal/experimental/agent_service/gateway/`, `areal/experimental/agent_service/router/`, `areal/experimental/inference_service/data_proxy/`, `areal/experimental/inference_service/controller/` | `route`, `gateway`, `router`, `DataProxy`, `controller`, `batch` | -| `session_consistency` | `areal/experimental/agent_service/`, `areal/experimental/inference_service/` | `session`, `affinity`, `history`, `state` | +| L2 Signal | File Path Pattern | Code Pattern | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `service_routing_dataflow` | `areal/v2/agent_service/gateway/`, `areal/v2/agent_service/router/`, `areal/v2/inference_service/data_proxy/`, `areal/v2/inference_service/controller/` | `route`, `gateway`, `router`, `DataProxy`, `controller`, `batch` | +| `session_consistency` | `areal/v2/agent_service/`, `areal/v2/inference_service/` | `session`, `affinity`, `history`, `state` | ## Domain 5: Workflow & Trainer Contract (HIGH/MEDIUM) -| L2 Signal | File Path Pattern | Code Pattern | -| -------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| `workflow_engine_boundary` | `areal/workflow/`, `areal/trainer/`, `areal/engine/` | `RolloutWorkflow`, `arun_episode`, `agenerate` | -| `dataset_surface` | `areal/dataset/` | `DataLoader`, `IterableDataset`, `get_*_dataset` | -| `async_contract` | `areal/workflow/`, `areal/experimental/agent_service/`, `areal/experimental/inference_service/` | `async def`, `await`, `aiofiles`, `asyncio` | -| `weight_version_contract` | `areal/api/engine_api.py`, `areal/workflow/`, `areal/trainer/` | `WeightUpdateMeta`, `set_version`, `weight version` | +| L2 Signal | File Path Pattern | Code Pattern | +| -------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------- | +| `workflow_engine_boundary` | `areal/workflow/`, `areal/trainer/`, `areal/engine/` | `RolloutWorkflow`, `arun_episode`, `agenerate` | +| `dataset_surface` | `areal/dataset/` | `DataLoader`, `IterableDataset`, `get_*_dataset` | +| `async_contract` | `areal/workflow/`, `areal/v2/agent_service/`, `areal/v2/inference_service/` | `async def`, `await`, `aiofiles`, `asyncio` | +| `weight_version_contract` | `areal/api/engine_api.py`, `areal/workflow/`, `areal/trainer/` | `WeightUpdateMeta`, `set_version`, `weight version` | ## Domain 6: API & Config Compatibility (MEDIUM) @@ -95,12 +95,12 @@ ______________________________________________________________________ ## Domain 9: Launcher & Infrastructure (HIGH/MEDIUM) -| L2 Signal | File Path Pattern | Code Pattern | -| ------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- | -| `launcher_resource_match` | `areal/infra/launcher/` | `LaunchConfig`, `RayLauncher`, `SlurmLauncher` | -| `scheduler_contract` | `areal/infra/scheduler/`, `areal/scheduler/` | `Scheduler`, `placement`, `resource` | -| `rpc_transport` | `areal/infra/rpc/`, `areal/experimental/inference_service/data_proxy/` | `RTensor`, `serialize`, `rpc`, `fetch` | -| `runtime_image_config` | `Dockerfile`, `.dockerignore` | `FROM`, `ARG`, `RUN`, `ENV`, `COPY`, `uv sync`, `VARIANT` | +| L2 Signal | File Path Pattern | Code Pattern | +| ------------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | +| `launcher_resource_match` | `areal/infra/launcher/` | `LaunchConfig`, `RayLauncher`, `SlurmLauncher` | +| `scheduler_contract` | `areal/infra/scheduler/`, `areal/scheduler/` | `Scheduler`, `placement`, `resource` | +| `rpc_transport` | `areal/infra/rpc/`, `areal/v2/inference_service/data_proxy/` | `RTensor`, `serialize`, `rpc`, `fetch` | +| `runtime_image_config` | `Dockerfile`, `.dockerignore` | `FROM`, `ARG`, `RUN`, `ENV`, `COPY`, `uv sync`, `VARIANT` | ## Domain 10: Low-Risk Hygiene (LOW) diff --git a/.agents/skills/upgrade-deps/checklists/sglang.md b/.agents/skills/upgrade-deps/checklists/sglang.md index 11c05eb569..f9d34e46d3 100644 --- a/.agents/skills/upgrade-deps/checklists/sglang.md +++ b/.agents/skills/upgrade-deps/checklists/sglang.md @@ -23,27 +23,27 @@ upstream_paths: ### Primary (engine layer — most likely to break) -| File | Imports / Usage | -| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `areal/engine/sglang_remote.py` | HTTP-only (no Python imports). Sends requests to `/generate`, `/load_lora_adapter`, `/update_weights_from_disk`, `/update_weights_from_distributed`, `/init_weights_update_group`, `/pause_generation`, `/continue_generation`, `/health`, `/release_memory_occupation`, `/resume_memory_occupation`. Parses `meta_info`, `finish_reason`, `output_token_logprobs`, `routed_experts` from JSON responses. | -| `areal/experimental/openai/tool_call_parser.py` | `sglang.srt.entrypoints.openai.protocol.Function`, `sglang.srt.entrypoints.openai.protocol.Tool`, `sglang.srt.function_call.function_call_parser.FunctionCallParser`, `sglang.srt.parser.reasoning_parser.ReasoningParser` | -| `areal/experimental/inference_service/sglang/launch_server.py` | `sglang.srt.entrypoints.engine.Engine`, `sglang.srt.entrypoints.engine.init_tokenizer_manager`, `sglang.srt.entrypoints.http_server._execute_server_warmup`, `sglang.srt.entrypoints.http_server._setup_and_run_http_server`, `sglang.srt.entrypoints.http_server.app`, `sglang.srt.managers.detokenizer_manager.run_detokenizer_process` | -| `areal/experimental/inference_service/sglang/scheduler.py` | `sglang.srt.managers.scheduler.Scheduler`, `sglang.srt.managers.scheduler.configure_scheduler`, `sglang.srt.observability.trace.process_tracing_init`, `sglang.srt.observability.trace.trace_set_thread_info`, `sglang.srt.utils.get_bool_env_var`, `sglang.srt.utils.kill_itself_when_parent_died`, `sglang.srt.utils.set_gpu_proc_affinity`, `sglang.srt.utils.numa_utils.get_numa_node_if_available`, `sglang.srt.utils.numa_utils.numa_bind_to_node`, `sglang.srt.environ.envs`, `sglang.utils.get_exception_traceback` | -| `areal/experimental/inference_service/sglang/rpc_proxy.py` | `sglang.srt.managers.io_struct.RpcReqInput`, `sglang.srt.managers.io_struct.RpcReqOutput`, `sglang.srt.server_args.PortArgs`, `sglang.srt.utils.network.get_zmq_socket` | -| `areal/experimental/inference_service/sglang/bridge.py` | HTTP-only (same protocol as `sglang_remote.py`). `/generate` request/response, `/pause_generation`, `/continue_generation`, `/release_memory_occupation`, `/resume_memory_occupation`. | +| File | Imports / Usage | +| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `areal/engine/sglang_remote.py` | HTTP-only (no Python imports). Sends requests to `/generate`, `/load_lora_adapter`, `/update_weights_from_disk`, `/update_weights_from_distributed`, `/init_weights_update_group`, `/pause_generation`, `/continue_generation`, `/health`, `/release_memory_occupation`, `/resume_memory_occupation`. Parses `meta_info`, `finish_reason`, `output_token_logprobs`, `routed_experts` from JSON responses. | +| `areal/experimental/openai/tool_call_parser.py` | `sglang.srt.entrypoints.openai.protocol.Function`, `sglang.srt.entrypoints.openai.protocol.Tool`, `sglang.srt.function_call.function_call_parser.FunctionCallParser`, `sglang.srt.parser.reasoning_parser.ReasoningParser` | +| `areal/v2/inference_service/sglang/launch_server.py` | `sglang.srt.entrypoints.engine.Engine`, `sglang.srt.entrypoints.engine.init_tokenizer_manager`, `sglang.srt.entrypoints.http_server._execute_server_warmup`, `sglang.srt.entrypoints.http_server._setup_and_run_http_server`, `sglang.srt.entrypoints.http_server.app`, `sglang.srt.managers.detokenizer_manager.run_detokenizer_process` | +| `areal/v2/inference_service/sglang/scheduler.py` | `sglang.srt.managers.scheduler.Scheduler`, `sglang.srt.managers.scheduler.configure_scheduler`, `sglang.srt.observability.trace.process_tracing_init`, `sglang.srt.observability.trace.trace_set_thread_info`, `sglang.srt.utils.get_bool_env_var`, `sglang.srt.utils.kill_itself_when_parent_died`, `sglang.srt.utils.set_gpu_proc_affinity`, `sglang.srt.utils.numa_utils.get_numa_node_if_available`, `sglang.srt.utils.numa_utils.numa_bind_to_node`, `sglang.srt.environ.envs`, `sglang.utils.get_exception_traceback` | +| `areal/v2/inference_service/sglang/rpc_proxy.py` | `sglang.srt.managers.io_struct.RpcReqInput`, `sglang.srt.managers.io_struct.RpcReqOutput`, `sglang.srt.server_args.PortArgs`, `sglang.srt.utils.network.get_zmq_socket` | +| `areal/v2/inference_service/sglang/bridge.py` | HTTP-only (same protocol as `sglang_remote.py`). `/generate` request/response, `/pause_generation`, `/continue_generation`, `/release_memory_occupation`, `/resume_memory_occupation`. | ### Secondary (model / infra layer) -| File | Imports / Usage | -| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `areal/infra/launcher/sglang_server.py` | HTTP-only. Polls `/v1/models` to confirm server readiness. Calls `SGLangConfig.build_cmd()` to construct the server launch command. | -| `areal/api/cli_args.py` | `SGLangConfig.build_cmd()`, `SGLangConfig.build_cmd_from_args()`, `SGLangConfig.build_args()`. Version check against `0.5.10.post1`. | -| `areal/trainer/rl_trainer.py` | `SGLangConfig.build_args()` to construct `RemoteSGLangEngine`. | -| `areal/infra/launcher/ray.py` | `SGLangConfig` import + `to_structured_cfg`. | -| `areal/infra/launcher/local.py` | `SGLangConfig` import + `to_structured_cfg`. | -| `areal/infra/launcher/slurm.py` | `SGLangConfig` import + `to_structured_cfg`. | -| `areal/experimental/inference_service/controller/controller.py` | `SGLangConfig` in gateway controller. | -| `areal/experimental/inference_service/data_proxy/backend.py` | HTTP protocol implementation for inference service proxy (SGLangBridgeBackend). | +| File | Imports / Usage | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `areal/infra/launcher/sglang_server.py` | HTTP-only. Polls `/v1/models` to confirm server readiness. Calls `SGLangConfig.build_cmd()` to construct the server launch command. | +| `areal/api/cli_args.py` | `SGLangConfig.build_cmd()`, `SGLangConfig.build_cmd_from_args()`, `SGLangConfig.build_args()`. Version check against `0.5.10.post1`. | +| `areal/trainer/rl_trainer.py` | `SGLangConfig.build_args()` to construct `RemoteSGLangEngine`. | +| `areal/infra/launcher/ray.py` | `SGLangConfig` import + `to_structured_cfg`. | +| `areal/infra/launcher/local.py` | `SGLangConfig` import + `to_structured_cfg`. | +| `areal/infra/launcher/slurm.py` | `SGLangConfig` import + `to_structured_cfg`. | +| `areal/v2/inference_service/controller/controller.py` | `SGLangConfig` in gateway controller. | +| `areal/v2/inference_service/data_proxy/backend.py` | HTTP protocol implementation for inference service proxy (SGLangBridgeBackend). | ### Tertiary (tests, config) @@ -415,8 +415,7 @@ ______________________________________________________________________ **Source:** `python/sglang/srt/entrypoints/engine.py` -Called in `areal/experimental/inference_service/sglang/launch_server.py` -(`areal_launch_server`): +Called in `areal/v2/inference_service/sglang/launch_server.py` (`areal_launch_server`): ```python from sglang.srt.entrypoints.engine import Engine, init_tokenizer_manager @@ -447,8 +446,7 @@ ______________________________________________________________________ **Source:** `python/sglang/srt/entrypoints/http_server.py` -Called in `areal/experimental/inference_service/sglang/launch_server.py` -(`areal_launch_server`): +Called in `areal/v2/inference_service/sglang/launch_server.py` (`areal_launch_server`): ```python from sglang.srt.entrypoints.http_server import ( @@ -478,7 +476,7 @@ ______________________________________________________________________ **Source:** `python/sglang/srt/managers/scheduler.py` -Called in `areal/experimental/inference_service/sglang/scheduler.py` +Called in `areal/v2/inference_service/sglang/scheduler.py` (`areal_run_scheduler_process`): ```python @@ -510,7 +508,7 @@ ______________________________________________________________________ **Source:** `python/sglang/srt/managers/io_struct.py` -Used in `areal/experimental/inference_service/sglang/rpc_proxy.py`: +Used in `areal/v2/inference_service/sglang/rpc_proxy.py`: ```python from sglang.srt.managers.io_struct import RpcReqInput, RpcReqOutput diff --git a/.claude/data/review-pr-domains-and-signals.md b/.claude/data/review-pr-domains-and-signals.md index 7587d6dda9..16d71ff7ac 100644 --- a/.claude/data/review-pr-domains-and-signals.md +++ b/.claude/data/review-pr-domains-and-signals.md @@ -52,19 +52,19 @@ ______________________________________________________________________ ## Domain 4: Service Orchestration (HIGH) -| L2 Signal | File Path Pattern | Code Pattern | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| `service_routing_dataflow` | `areal/experimental/agent_service/gateway/`, `areal/experimental/agent_service/router/`, `areal/experimental/inference_service/data_proxy/`, `areal/experimental/inference_service/controller/` | `route`, `gateway`, `router`, `DataProxy`, `controller`, `batch` | -| `session_consistency` | `areal/experimental/agent_service/`, `areal/experimental/inference_service/` | `session`, `affinity`, `history`, `state` | +| L2 Signal | File Path Pattern | Code Pattern | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `service_routing_dataflow` | `areal/v2/agent_service/gateway/`, `areal/v2/agent_service/router/`, `areal/v2/inference_service/data_proxy/`, `areal/v2/inference_service/controller/` | `route`, `gateway`, `router`, `DataProxy`, `controller`, `batch` | +| `session_consistency` | `areal/v2/agent_service/`, `areal/v2/inference_service/` | `session`, `affinity`, `history`, `state` | ## Domain 5: Workflow & Trainer Contract (HIGH/MEDIUM) -| L2 Signal | File Path Pattern | Code Pattern | -| -------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| `workflow_engine_boundary` | `areal/workflow/`, `areal/trainer/`, `areal/engine/` | `RolloutWorkflow`, `arun_episode`, `agenerate` | -| `dataset_surface` | `areal/dataset/` | `DataLoader`, `IterableDataset`, `get_*_dataset` | -| `async_contract` | `areal/workflow/`, `areal/experimental/agent_service/`, `areal/experimental/inference_service/` | `async def`, `await`, `aiofiles`, `asyncio` | -| `weight_version_contract` | `areal/api/engine_api.py`, `areal/workflow/`, `areal/trainer/` | `WeightUpdateMeta`, `set_version`, `weight version` | +| L2 Signal | File Path Pattern | Code Pattern | +| -------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------- | +| `workflow_engine_boundary` | `areal/workflow/`, `areal/trainer/`, `areal/engine/` | `RolloutWorkflow`, `arun_episode`, `agenerate` | +| `dataset_surface` | `areal/dataset/` | `DataLoader`, `IterableDataset`, `get_*_dataset` | +| `async_contract` | `areal/workflow/`, `areal/v2/agent_service/`, `areal/v2/inference_service/` | `async def`, `await`, `aiofiles`, `asyncio` | +| `weight_version_contract` | `areal/api/engine_api.py`, `areal/workflow/`, `areal/trainer/` | `WeightUpdateMeta`, `set_version`, `weight version` | ## Domain 6: API & Config Compatibility (MEDIUM) @@ -95,12 +95,12 @@ ______________________________________________________________________ ## Domain 9: Launcher & Infrastructure (HIGH/MEDIUM) -| L2 Signal | File Path Pattern | Code Pattern | -| ------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- | -| `launcher_resource_match` | `areal/infra/launcher/` | `LaunchConfig`, `RayLauncher`, `SlurmLauncher` | -| `scheduler_contract` | `areal/infra/scheduler/`, `areal/scheduler/` | `Scheduler`, `placement`, `resource` | -| `rpc_transport` | `areal/infra/rpc/`, `areal/experimental/inference_service/data_proxy/` | `RTensor`, `serialize`, `rpc`, `fetch` | -| `runtime_image_config` | `Dockerfile`, `.dockerignore` | `FROM`, `ARG`, `RUN`, `ENV`, `COPY`, `uv sync`, `VARIANT` | +| L2 Signal | File Path Pattern | Code Pattern | +| ------------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | +| `launcher_resource_match` | `areal/infra/launcher/` | `LaunchConfig`, `RayLauncher`, `SlurmLauncher` | +| `scheduler_contract` | `areal/infra/scheduler/`, `areal/scheduler/` | `Scheduler`, `placement`, `resource` | +| `rpc_transport` | `areal/infra/rpc/`, `areal/v2/inference_service/data_proxy/` | `RTensor`, `serialize`, `rpc`, `fetch` | +| `runtime_image_config` | `Dockerfile`, `.dockerignore` | `FROM`, `ARG`, `RUN`, `ENV`, `COPY`, `uv sync`, `VARIANT` | ## Domain 10: Low-Risk Hygiene (LOW) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd1300bc62..7b5cd04f58 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,34 +1,34 @@ # Default: catch-all (lead + two active maintainers as backups). # Last matching rule wins, so this is the fallback for any path # not covered by a more specific rule below. -* @garrett4wade @sitabulaixizawaluduo @fishcrap +* @garrett4wade @sitabulaixizawaluduo @fishcrap # Core package -/areal/api/ @garrett4wade @rchardx @sitabulaixizawaluduo -/areal/engine/ @rchardx @nuzant @garrett4wade @geshi001 -/areal/experimental/inference_service/ @nuzant @guozhihao-224 @TaoZex -/areal/experimental/agent_service/ @CormickKneey @nuzant -/areal/experimental/training_service/ @sitabulaixizawaluduo @garrett4wade -/areal/experimental/weight_update/ @TaoZex @garrett4wade @sitabulaixizawaluduo -/areal/infra/ @HwVanICI @garrett4wade @guozhihao-224 -/areal/models/ @rchardx @nuzant @geshi001 @PrometheusComing -/areal/trainer/ @garrett4wade @rchardx @fishcrap +/areal/api/ @garrett4wade @rchardx @sitabulaixizawaluduo +/areal/engine/ @rchardx @nuzant @garrett4wade @geshi001 +/areal/v2/inference_service/ @nuzant @guozhihao-224 @TaoZex +/areal/v2/agent_service/ @CormickKneey @nuzant +/areal/v2/training_service/ @sitabulaixizawaluduo @garrett4wade +/areal/v2/weight_update/ @TaoZex @garrett4wade @sitabulaixizawaluduo +/areal/infra/ @HwVanICI @garrett4wade @guozhihao-224 +/areal/models/ @rchardx @nuzant @geshi001 @PrometheusComing +/areal/trainer/ @garrett4wade @rchardx @fishcrap # Tests & Examples -/tests/ @garrett4wade @sitabulaixizawaluduo -/examples/ @zhenanf @HwVanICI @CormickKneey @PrometheusComing +/tests/ @garrett4wade @sitabulaixizawaluduo +/examples/ @zhenanf @HwVanICI @CormickKneey @PrometheusComing # Documentation -/docs/ @garrett4wade @nuzant +/docs/ @garrett4wade @nuzant # CI/CD & infrastructure -/.github/ @garrett4wade @nuzant @sitabulaixizawaluduo -/Dockerfile @garrett4wade @fishcrap @sitabulaixizawaluduo -pyproject.toml @garrett4wade @fishcrap @sitabulaixizawaluduo -pyproject.vllm.toml @garrett4wade @fishcrap @sitabulaixizawaluduo -uv.lock @garrett4wade @fishcrap @sitabulaixizawaluduo -uv.vllm.lock @garrett4wade @fishcrap @sitabulaixizawaluduo +/.github/ @garrett4wade @nuzant @sitabulaixizawaluduo +/Dockerfile @garrett4wade @fishcrap @sitabulaixizawaluduo +pyproject.toml @garrett4wade @fishcrap @sitabulaixizawaluduo +pyproject.vllm.toml @garrett4wade @fishcrap @sitabulaixizawaluduo +uv.lock @garrett4wade @fishcrap @sitabulaixizawaluduo +uv.vllm.lock @garrett4wade @fishcrap @sitabulaixizawaluduo # Governance & community -GOVERNANCE.md @garrett4wade @sitabulaixizawaluduo -CONTRIBUTING.md @garrett4wade @sitabulaixizawaluduo +GOVERNANCE.md @garrett4wade @sitabulaixizawaluduo +CONTRIBUTING.md @garrett4wade @sitabulaixizawaluduo diff --git a/.opencode/data/review-pr-domains-and-signals.md b/.opencode/data/review-pr-domains-and-signals.md index 21c65a15af..fe9d9aef17 100644 --- a/.opencode/data/review-pr-domains-and-signals.md +++ b/.opencode/data/review-pr-domains-and-signals.md @@ -52,19 +52,19 @@ ______________________________________________________________________ ## Domain 4: Service Orchestration (HIGH) -| L2 Signal | File Path Pattern | Code Pattern | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| `service_routing_dataflow` | `areal/experimental/agent_service/gateway/`, `areal/experimental/agent_service/router/`, `areal/experimental/inference_service/data_proxy/`, `areal/experimental/inference_service/controller/` | `route`, `gateway`, `router`, `DataProxy`, `controller`, `batch` | -| `session_consistency` | `areal/experimental/agent_service/`, `areal/experimental/inference_service/` | `session`, `affinity`, `history`, `state` | +| L2 Signal | File Path Pattern | Code Pattern | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `service_routing_dataflow` | `areal/v2/agent_service/gateway/`, `areal/v2/agent_service/router/`, `areal/v2/inference_service/data_proxy/`, `areal/v2/inference_service/controller/` | `route`, `gateway`, `router`, `DataProxy`, `controller`, `batch` | +| `session_consistency` | `areal/v2/agent_service/`, `areal/v2/inference_service/` | `session`, `affinity`, `history`, `state` | ## Domain 5: Workflow & Trainer Contract (HIGH/MEDIUM) -| L2 Signal | File Path Pattern | Code Pattern | -| -------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| `workflow_engine_boundary` | `areal/workflow/`, `areal/trainer/`, `areal/engine/` | `RolloutWorkflow`, `arun_episode`, `agenerate` | -| `dataset_surface` | `areal/dataset/` | `DataLoader`, `IterableDataset`, `get_*_dataset` | -| `async_contract` | `areal/workflow/`, `areal/experimental/agent_service/`, `areal/experimental/inference_service/` | `async def`, `await`, `aiofiles`, `asyncio` | -| `weight_version_contract` | `areal/api/engine_api.py`, `areal/workflow/`, `areal/trainer/` | `WeightUpdateMeta`, `set_version`, `weight version` | +| L2 Signal | File Path Pattern | Code Pattern | +| -------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------- | +| `workflow_engine_boundary` | `areal/workflow/`, `areal/trainer/`, `areal/engine/` | `RolloutWorkflow`, `arun_episode`, `agenerate` | +| `dataset_surface` | `areal/dataset/` | `DataLoader`, `IterableDataset`, `get_*_dataset` | +| `async_contract` | `areal/workflow/`, `areal/v2/agent_service/`, `areal/v2/inference_service/` | `async def`, `await`, `aiofiles`, `asyncio` | +| `weight_version_contract` | `areal/api/engine_api.py`, `areal/workflow/`, `areal/trainer/` | `WeightUpdateMeta`, `set_version`, `weight version` | ## Domain 6: API & Config Compatibility (MEDIUM) @@ -95,12 +95,12 @@ ______________________________________________________________________ ## Domain 9: Launcher & Infrastructure (HIGH/MEDIUM) -| L2 Signal | File Path Pattern | Code Pattern | -| ------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- | -| `launcher_resource_match` | `areal/infra/launcher/` | `LaunchConfig`, `RayLauncher`, `SlurmLauncher` | -| `scheduler_contract` | `areal/infra/scheduler/`, `areal/scheduler/` | `Scheduler`, `placement`, `resource` | -| `rpc_transport` | `areal/infra/rpc/`, `areal/experimental/inference_service/data_proxy/` | `RTensor`, `serialize`, `rpc`, `fetch` | -| `runtime_image_config` | `Dockerfile`, `.dockerignore` | `FROM`, `ARG`, `RUN`, `ENV`, `COPY`, `uv sync`, `VARIANT` | +| L2 Signal | File Path Pattern | Code Pattern | +| ------------------------- | ------------------------------------------------------------ | --------------------------------------------------------- | +| `launcher_resource_match` | `areal/infra/launcher/` | `LaunchConfig`, `RayLauncher`, `SlurmLauncher` | +| `scheduler_contract` | `areal/infra/scheduler/`, `areal/scheduler/` | `Scheduler`, `placement`, `resource` | +| `rpc_transport` | `areal/infra/rpc/`, `areal/v2/inference_service/data_proxy/` | `RTensor`, `serialize`, `rpc`, `fetch` | +| `runtime_image_config` | `Dockerfile`, `.dockerignore` | `FROM`, `ARG`, `RUN`, `ENV`, `COPY`, `uv sync`, `VARIANT` | ## Domain 10: Low-Risk Hygiene (LOW) diff --git a/ROADMAP.md b/ROADMAP.md index 09c64d94af..68c525aded 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -91,9 +91,9 @@ in contributing, please reach out to discuss implementation details. - [ ] Migrate primary Megatron integration lib from `mbridge` to `megatron-bridge` ([#1260](https://github.com/areal-project/AReaL/issues/1260)) `[CC]` - [ ] Full support for colocation/separation weight transfer with `awex` as the backend - (`areal/experimental/weight_update/`) `[CC]` + (`areal/v2/weight_update/`) `[CC]` - [ ] Migrate legacy NCCL broadcast weight transfer approach from `areal/engine` into - `areal/experimental/weight_update/` `[CC]` + `areal/v2/weight_update/` `[CC]` - [ ] Omini model RL support with FSDP backend ([#879](https://github.com/areal-project/AReaL/issues/879)) `[CC]` - [ ] Support training the latest large MoE models with the Megatron backend, including diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 21e51e255b..b151894e88 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -1965,9 +1965,7 @@ def build_cmd( @staticmethod def build_cmd_from_args(args: dict[str, Any]): - return get_py_cmd( - "areal.experimental.inference_service.sglang.launch_server", args - ) + return get_py_cmd("areal.v2.inference_service.sglang.launch_server", args) @staticmethod def build_args( @@ -2050,7 +2048,7 @@ class AgentConfig: default_factory=lambda: ( SchedulingSpec( gpu=0, - cmd="python -m areal.experimental.agent_service.guard", + cmd="python -m areal.v2.agent_service.guard", ), ), metadata={ diff --git a/areal/engine/sglang_remote.py b/areal/engine/sglang_remote.py index a2b1677aa3..7221326b76 100644 --- a/areal/engine/sglang_remote.py +++ b/areal/engine/sglang_remote.py @@ -587,7 +587,7 @@ def export_stats(self) -> dict[str, float]: @classmethod def as_controller(cls, config: InferenceEngineConfig, scheduler: Scheduler): if config._version == "v2": - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) diff --git a/areal/engine/vllm_remote.py b/areal/engine/vllm_remote.py index 6f70747150..42aee39dc6 100644 --- a/areal/engine/vllm_remote.py +++ b/areal/engine/vllm_remote.py @@ -542,7 +542,7 @@ def export_stats(self) -> dict[str, float]: @classmethod def as_controller(cls, config: InferenceEngineConfig, scheduler: Scheduler): if config._version == "v2": - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) diff --git a/areal/infra/rpc/guard/__init__.py b/areal/infra/rpc/guard/__init__.py index af488fbbfd..7c8b721fca 100644 --- a/areal/infra/rpc/guard/__init__.py +++ b/areal/infra/rpc/guard/__init__.py @@ -5,7 +5,7 @@ The Guard is the base process management layer shared between: - ``areal.infra.rpc.rpc_server`` — RPC server (guard + data + engine) -- ``areal.experimental.inference_service.guard`` — inference service guard +- ``areal.v2.inference_service.guard`` — inference service guard Typical usage:: diff --git a/areal/infra/rpc/guard/app.py b/areal/infra/rpc/guard/app.py index 3e13f86dd3..e7067f9de0 100644 --- a/areal/infra/rpc/guard/app.py +++ b/areal/infra/rpc/guard/app.py @@ -5,7 +5,7 @@ This module provides the base Guard functionality shared between: - ``areal.infra.rpc.rpc_server`` (RPC server = guard + data + engine) -- ``areal.experimental.inference_service.guard`` (inference service guard) +- ``areal.v2.inference_service.guard`` (inference service guard) Key components: diff --git a/areal/trainer/dpo/dpo_engine.py b/areal/trainer/dpo/dpo_engine.py index 89331f18dd..cfd23b432c 100644 --- a/areal/trainer/dpo/dpo_engine.py +++ b/areal/trainer/dpo/dpo_engine.py @@ -6,15 +6,15 @@ import torch from areal.api import TrainEngine -from areal.experimental.training_service.controller.controller import ( - GatewayTrainController, -) from areal.infra import TrainController from areal.infra.rpc.serialization import serialize_value from areal.utils import logging, stats_tracker from areal.utils.data import batched_call from areal.utils.functional import dpo_pair_logratios, dpo_preference_loss from areal.utils.perf_tracer import trace_perf +from areal.v2.training_service.controller.controller import ( + GatewayTrainController, +) logger = logging.getLogger("DPOEngine") diff --git a/areal/trainer/ppo/actor.py b/areal/trainer/ppo/actor.py index 2d7972a450..a0922bafe9 100644 --- a/areal/trainer/ppo/actor.py +++ b/areal/trainer/ppo/actor.py @@ -7,9 +7,6 @@ from areal.api import TrainEngine from areal.api.cli_args import MicroBatchSpec, PPOActorConfig, RejectionSamplingConfig -from areal.experimental.training_service.controller.controller import ( - GatewayTrainController, -) from areal.infra import TrainController from areal.infra.rpc.serialization import serialize_value from areal.trainer.ppo.stats import infer_token_denominator @@ -37,6 +34,9 @@ sapo_loss_fn, ) from areal.utils.perf_tracer import trace_perf +from areal.v2.training_service.controller.controller import ( + GatewayTrainController, +) logger = logging.getLogger("PPOActor") diff --git a/areal/trainer/ppo/critic.py b/areal/trainer/ppo/critic.py index a554181bc2..05f83c7441 100644 --- a/areal/trainer/ppo/critic.py +++ b/areal/trainer/ppo/critic.py @@ -7,9 +7,6 @@ from areal.api import TrainEngine from areal.api.cli_args import MicroBatchSpec, PPOCriticConfig -from areal.experimental.training_service.controller.controller import ( - GatewayTrainController, -) from areal.infra import TrainController from areal.infra.rpc.serialization import serialize_value from areal.trainer.ppo.stats import infer_token_denominator @@ -20,6 +17,9 @@ ) from areal.utils.functional import ppo_critic_loss_fn from areal.utils.perf_tracer import trace_perf +from areal.v2.training_service.controller.controller import ( + GatewayTrainController, +) class PPOCritic: diff --git a/areal/trainer/rl_trainer.py b/areal/trainer/rl_trainer.py index 4747530cab..1dce676809 100644 --- a/areal/trainer/rl_trainer.py +++ b/areal/trainer/rl_trainer.py @@ -35,9 +35,6 @@ vLLMConfig, ) from areal.engine import RemoteSGLangEngine, RemotevLLMEngine -from areal.experimental.inference_service.controller.controller import ( - RolloutControllerV2, -) from areal.infra import ( LocalScheduler, RayScheduler, @@ -58,6 +55,9 @@ from areal.utils.recover import RecoverHandler from areal.utils.saver import Saver from areal.utils.stats_logger import StatsLogger +from areal.v2.inference_service.controller.controller import ( + RolloutControllerV2, +) if TYPE_CHECKING: from datasets import Dataset diff --git a/areal/trainer/rw/rw_engine.py b/areal/trainer/rw/rw_engine.py index d8c22e832c..1e5cbb0acf 100644 --- a/areal/trainer/rw/rw_engine.py +++ b/areal/trainer/rw/rw_engine.py @@ -5,14 +5,14 @@ import torch from areal.api import TrainEngine -from areal.experimental.training_service.controller.controller import ( - GatewayTrainController, -) from areal.infra import TrainController from areal.infra.rpc.serialization import serialize_value from areal.utils import logging, stats_tracker from areal.utils.data import batched_call from areal.utils.perf_tracer import trace_perf +from areal.v2.training_service.controller.controller import ( + GatewayTrainController, +) logger = logging.getLogger("RWEngine") diff --git a/areal/trainer/sft/lm_engine.py b/areal/trainer/sft/lm_engine.py index 6705af0dbd..047671eaab 100644 --- a/areal/trainer/sft/lm_engine.py +++ b/areal/trainer/sft/lm_engine.py @@ -5,14 +5,14 @@ import torch from areal.api import TrainEngine -from areal.experimental.training_service.controller.controller import ( - GatewayTrainController, -) from areal.infra import TrainController from areal.infra.rpc.serialization import serialize_value from areal.utils import stats_tracker from areal.utils.data import batched_call from areal.utils.perf_tracer import trace_perf +from areal.v2.training_service.controller.controller import ( + GatewayTrainController, +) class LMEngine: diff --git a/areal/utils/recover.py b/areal/utils/recover.py index 13d1016cca..9af81b7efb 100644 --- a/areal/utils/recover.py +++ b/areal/utils/recover.py @@ -181,7 +181,7 @@ def _is_gateway_train_controller( | TrainController | dict[str, TrainEngine | TrainController], ) -> bool: - from areal.experimental.training_service.controller.controller import ( + from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) diff --git a/areal/experimental/cli/__init__.py b/areal/v2/__init__.py similarity index 100% rename from areal/experimental/cli/__init__.py rename to areal/v2/__init__.py diff --git a/areal/experimental/agent_service/README.md b/areal/v2/agent_service/README.md similarity index 93% rename from areal/experimental/agent_service/README.md rename to areal/v2/agent_service/README.md index 30f76bb8ec..2a06d19fd0 100644 --- a/areal/experimental/agent_service/README.md +++ b/areal/v2/agent_service/README.md @@ -152,7 +152,7 @@ Turn 2: ## Code Organization ``` -areal/experimental/agent_service/ +areal/v2/agent_service/ ├── __init__.py # Public exports (AgentRequest, AgentResponse, etc.) ├── README.md # This document ├── auth.py # Admin key auth helpers (hmac-safe comparison) @@ -163,29 +163,29 @@ areal/experimental/agent_service/ │ └── controller.py # AgentController orchestrator ├── guard/ │ ├── __init__.py # Module docstring -│ ├── __main__.py # python -m areal.experimental.agent_service.guard +│ ├── __main__.py # python -m areal.v2.agent_service.guard │ └── app.py # Guard Flask app (pass-through to areal.infra.rpc.guard) ├── gateway/ │ ├── __init__.py # Public exports -│ ├── __main__.py # python -m areal.experimental.agent_service.gateway +│ ├── __main__.py # python -m areal.v2.agent_service.gateway │ ├── app.py # create_gateway_app() │ ├── bridge.py # OpenResponsesBridge, mount_bridge() │ └── config.py # GatewayConfig dataclass ├── router/ │ ├── __init__.py # Public exports -│ ├── __main__.py # python -m areal.experimental.agent_service.router +│ ├── __main__.py # python -m areal.v2.agent_service.router │ ├── app.py # create_router_app() │ ├── client.py # RouterClient │ └── config.py # RouterConfig dataclass ├── data_proxy/ │ ├── __init__.py # Public exports -│ ├── __main__.py # python -m areal.experimental.agent_service.data_proxy +│ ├── __main__.py # python -m areal.v2.agent_service.data_proxy │ ├── app.py # create_data_proxy_app() │ ├── client.py # DataProxyClient │ └── config.py # DataProxyConfig dataclass └── worker/ ├── __init__.py # Public exports - ├── __main__.py # python -m areal.experimental.agent_service.worker + ├── __main__.py # python -m areal.v2.agent_service.worker ├── app.py # create_worker_app() └── config.py # WorkerConfig dataclass diff --git a/areal/experimental/agent_service/__init__.py b/areal/v2/agent_service/__init__.py similarity index 100% rename from areal/experimental/agent_service/__init__.py rename to areal/v2/agent_service/__init__.py diff --git a/areal/experimental/agent_service/auth.py b/areal/v2/agent_service/auth.py similarity index 100% rename from areal/experimental/agent_service/auth.py rename to areal/v2/agent_service/auth.py diff --git a/areal/experimental/agent_service/controller/__init__.py b/areal/v2/agent_service/controller/__init__.py similarity index 100% rename from areal/experimental/agent_service/controller/__init__.py rename to areal/v2/agent_service/controller/__init__.py diff --git a/areal/experimental/agent_service/controller/controller.py b/areal/v2/agent_service/controller/controller.py similarity index 97% rename from areal/experimental/agent_service/controller/controller.py rename to areal/v2/agent_service/controller/controller.py index f63d08d780..409fd5870c 100644 --- a/areal/experimental/agent_service/controller/controller.py +++ b/areal/v2/agent_service/controller/controller.py @@ -3,7 +3,7 @@ """AgentController — orchestrates agent service micro-services via Guards. Mirrors the architecture of -:class:`~areal.experimental.inference_service.controller.controller.RolloutControllerV2`: +:class:`~areal.v2.inference_service.controller.controller.RolloutControllerV2`: Guard workers are created via the Scheduler, then the controller forks Router, Worker+DataProxy pairs, and Gateway onto them via HTTP API. @@ -126,7 +126,7 @@ def _do_initialize(self) -> None: # Step 1: Create Guard workers via scheduler guard_spec = SchedulingSpec(**asdict(cfg.scheduling_spec[0])) guard_spec.gpu = 0 - guard_spec.cmd = f"{sys.executable} -m areal.experimental.agent_service.guard" + guard_spec.cmd = f"{sys.executable} -m areal.v2.agent_service.guard" self._base_env = dict(guard_spec.env_vars) num_guards = _DEFAULT_PAIR_COUNT guard_job = Job( @@ -150,7 +150,7 @@ def _do_initialize(self) -> None: router_cmd = [ sys.executable, "-m", - "areal.experimental.agent_service.router", + "areal.v2.agent_service.router", "--admin-api-key", cfg.admin_api_key, ] @@ -170,7 +170,7 @@ def _do_initialize(self) -> None: gw_cmd = [ sys.executable, "-m", - "areal.experimental.agent_service.gateway", + "areal.v2.agent_service.gateway", "--router-addr", self._router_addr, "--admin-api-key", @@ -246,7 +246,7 @@ def scale_up(self, count: int) -> list[int]: worker_cmd = [ sys.executable, "-m", - "areal.experimental.agent_service.worker", + "areal.v2.agent_service.worker", "--agent", cfg.agent_cls_path, "--log-level", @@ -263,7 +263,7 @@ def scale_up(self, count: int) -> list[int]: proxy_cmd = [ sys.executable, "-m", - "areal.experimental.agent_service.data_proxy", + "areal.v2.agent_service.data_proxy", "--worker-addr", worker_addr, ] diff --git a/areal/experimental/agent_service/data_proxy/__init__.py b/areal/v2/agent_service/data_proxy/__init__.py similarity index 100% rename from areal/experimental/agent_service/data_proxy/__init__.py rename to areal/v2/agent_service/data_proxy/__init__.py diff --git a/areal/experimental/agent_service/data_proxy/__main__.py b/areal/v2/agent_service/data_proxy/__main__.py similarity index 94% rename from areal/experimental/agent_service/data_proxy/__main__.py rename to areal/v2/agent_service/data_proxy/__main__.py index cff27a6816..26c5b3a9a1 100644 --- a/areal/experimental/agent_service/data_proxy/__main__.py +++ b/areal/v2/agent_service/data_proxy/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""``python -m areal.experimental.agent_service.data_proxy``""" +"""``python -m areal.v2.agent_service.data_proxy``""" import argparse diff --git a/areal/experimental/agent_service/data_proxy/app.py b/areal/v2/agent_service/data_proxy/app.py similarity index 100% rename from areal/experimental/agent_service/data_proxy/app.py rename to areal/v2/agent_service/data_proxy/app.py diff --git a/areal/experimental/agent_service/data_proxy/client.py b/areal/v2/agent_service/data_proxy/client.py similarity index 100% rename from areal/experimental/agent_service/data_proxy/client.py rename to areal/v2/agent_service/data_proxy/client.py diff --git a/areal/experimental/agent_service/data_proxy/config.py b/areal/v2/agent_service/data_proxy/config.py similarity index 100% rename from areal/experimental/agent_service/data_proxy/config.py rename to areal/v2/agent_service/data_proxy/config.py diff --git a/areal/experimental/agent_service/gateway/__init__.py b/areal/v2/agent_service/gateway/__init__.py similarity index 100% rename from areal/experimental/agent_service/gateway/__init__.py rename to areal/v2/agent_service/gateway/__init__.py diff --git a/areal/experimental/agent_service/gateway/__main__.py b/areal/v2/agent_service/gateway/__main__.py similarity index 96% rename from areal/experimental/agent_service/gateway/__main__.py rename to areal/v2/agent_service/gateway/__main__.py index 72313bf641..151e6c382e 100644 --- a/areal/experimental/agent_service/gateway/__main__.py +++ b/areal/v2/agent_service/gateway/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""``python -m areal.experimental.agent_service.gateway``""" +"""``python -m areal.v2.agent_service.gateway``""" import argparse diff --git a/areal/experimental/agent_service/gateway/app.py b/areal/v2/agent_service/gateway/app.py similarity index 100% rename from areal/experimental/agent_service/gateway/app.py rename to areal/v2/agent_service/gateway/app.py diff --git a/areal/experimental/agent_service/gateway/bridge.py b/areal/v2/agent_service/gateway/bridge.py similarity index 100% rename from areal/experimental/agent_service/gateway/bridge.py rename to areal/v2/agent_service/gateway/bridge.py diff --git a/areal/experimental/agent_service/gateway/config.py b/areal/v2/agent_service/gateway/config.py similarity index 100% rename from areal/experimental/agent_service/gateway/config.py rename to areal/v2/agent_service/gateway/config.py diff --git a/areal/experimental/agent_service/guard/__init__.py b/areal/v2/agent_service/guard/__init__.py similarity index 76% rename from areal/experimental/agent_service/guard/__init__.py rename to areal/v2/agent_service/guard/__init__.py index 57f50162ec..6e266647ad 100644 --- a/areal/experimental/agent_service/guard/__init__.py +++ b/areal/v2/agent_service/guard/__init__.py @@ -4,11 +4,11 @@ Pure pass-through to ``areal.infra.rpc.guard``. All orchestration logic (launching Router, Gateway, Worker+DataProxy pairs) lives in the -:mod:`~areal.experimental.agent_service.controller` module. +:mod:`~areal.v2.agent_service.controller` module. Quick start:: - python -m areal.experimental.agent_service.guard \\ + python -m areal.v2.agent_service.guard \\ --experiment-name demo --trial-name run0 \\ --role agent-guard --worker-index 0 """ diff --git a/areal/experimental/agent_service/guard/__main__.py b/areal/v2/agent_service/guard/__main__.py similarity index 80% rename from areal/experimental/agent_service/guard/__main__.py rename to areal/v2/agent_service/guard/__main__.py index d311f6023b..621b29722d 100644 --- a/areal/experimental/agent_service/guard/__main__.py +++ b/areal/v2/agent_service/guard/__main__.py @@ -1,18 +1,18 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint: ``python -m areal.experimental.agent_service.guard``""" +"""CLI entrypoint: ``python -m areal.v2.agent_service.guard``""" from __future__ import annotations -from areal.experimental.agent_service.guard.app import ( - _state, - app, -) from areal.infra.rpc.guard.app import ( configure_state_from_args, make_base_parser, run_server, ) +from areal.v2.agent_service.guard.app import ( + _state, + app, +) def main(): diff --git a/areal/experimental/agent_service/guard/app.py b/areal/v2/agent_service/guard/app.py similarity index 91% rename from areal/experimental/agent_service/guard/app.py rename to areal/v2/agent_service/guard/app.py index b137feef91..4241dea6f4 100644 --- a/areal/experimental/agent_service/guard/app.py +++ b/areal/v2/agent_service/guard/app.py @@ -5,7 +5,7 @@ All core guard functionality (port allocation, process forking, health checks, cleanup) is provided by ``areal.infra.rpc.guard``. This module creates and exposes the Flask app and shared state instance, following -the same pattern as ``areal.experimental.inference_service.guard``. +the same pattern as ``areal.v2.inference_service.guard``. """ from __future__ import annotations diff --git a/areal/experimental/agent_service/protocol.py b/areal/v2/agent_service/protocol.py similarity index 100% rename from areal/experimental/agent_service/protocol.py rename to areal/v2/agent_service/protocol.py diff --git a/areal/experimental/agent_service/router/__init__.py b/areal/v2/agent_service/router/__init__.py similarity index 100% rename from areal/experimental/agent_service/router/__init__.py rename to areal/v2/agent_service/router/__init__.py diff --git a/areal/experimental/agent_service/router/__main__.py b/areal/v2/agent_service/router/__main__.py similarity index 95% rename from areal/experimental/agent_service/router/__main__.py rename to areal/v2/agent_service/router/__main__.py index ef5e97e7a5..a5a730b504 100644 --- a/areal/experimental/agent_service/router/__main__.py +++ b/areal/v2/agent_service/router/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""``python -m areal.experimental.agent_service.router``""" +"""``python -m areal.v2.agent_service.router``""" import argparse diff --git a/areal/experimental/agent_service/router/app.py b/areal/v2/agent_service/router/app.py similarity index 100% rename from areal/experimental/agent_service/router/app.py rename to areal/v2/agent_service/router/app.py diff --git a/areal/experimental/agent_service/router/client.py b/areal/v2/agent_service/router/client.py similarity index 100% rename from areal/experimental/agent_service/router/client.py rename to areal/v2/agent_service/router/client.py diff --git a/areal/experimental/agent_service/router/config.py b/areal/v2/agent_service/router/config.py similarity index 100% rename from areal/experimental/agent_service/router/config.py rename to areal/v2/agent_service/router/config.py diff --git a/areal/experimental/agent_service/types.py b/areal/v2/agent_service/types.py similarity index 100% rename from areal/experimental/agent_service/types.py rename to areal/v2/agent_service/types.py diff --git a/areal/experimental/agent_service/worker/__init__.py b/areal/v2/agent_service/worker/__init__.py similarity index 100% rename from areal/experimental/agent_service/worker/__init__.py rename to areal/v2/agent_service/worker/__init__.py diff --git a/areal/experimental/agent_service/worker/__main__.py b/areal/v2/agent_service/worker/__main__.py similarity index 89% rename from areal/experimental/agent_service/worker/__main__.py rename to areal/v2/agent_service/worker/__main__.py index f1cc525bdf..da840adb6f 100644 --- a/areal/experimental/agent_service/worker/__main__.py +++ b/areal/v2/agent_service/worker/__main__.py @@ -1,11 +1,11 @@ # SPDX-License-Identifier: Apache-2.0 -"""``python -m areal.experimental.agent_service.worker`` +"""``python -m areal.v2.agent_service.worker`` Start a standalone Agent Worker process. The Controller forks this via Guard to create Worker+DataProxy pairs. - python -m areal.experimental.agent_service.worker \ + python -m areal.v2.agent_service.worker \ --agent examples.agent_service.agent.ClaudeAgent \ --host 127.0.0.1 --port 9000 """ diff --git a/areal/experimental/agent_service/worker/app.py b/areal/v2/agent_service/worker/app.py similarity index 100% rename from areal/experimental/agent_service/worker/app.py rename to areal/v2/agent_service/worker/app.py diff --git a/areal/experimental/agent_service/worker/config.py b/areal/v2/agent_service/worker/config.py similarity index 100% rename from areal/experimental/agent_service/worker/config.py rename to areal/v2/agent_service/worker/config.py diff --git a/areal/experimental/cli/commands/__init__.py b/areal/v2/cli/__init__.py similarity index 100% rename from areal/experimental/cli/commands/__init__.py rename to areal/v2/cli/__init__.py diff --git a/areal/experimental/cli/__main__.py b/areal/v2/cli/__main__.py similarity index 64% rename from areal/experimental/cli/__main__.py rename to areal/v2/cli/__main__.py index 87c14941f4..02f5fa0c1f 100644 --- a/areal/experimental/cli/__main__.py +++ b/areal/v2/cli/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -from areal.experimental.cli.cli import cli +from areal.v2.cli.cli import cli if __name__ == "__main__": cli() diff --git a/areal/experimental/cli/cli.py b/areal/v2/cli/cli.py similarity index 72% rename from areal/experimental/cli/cli.py rename to areal/v2/cli/cli.py index b97e28cf79..44373c542b 100644 --- a/areal/experimental/cli/cli.py +++ b/areal/v2/cli/cli.py @@ -17,5 +17,5 @@ def cli() -> None: # Subcommand groups (inf / train / agent / ...) attach themselves to ``cli`` -# from their own modules — see e.g. ``areal.experimental.cli.inference``, -# ``areal.experimental.cli.training``, ``areal.experimental.cli.agent``. +# from their own modules — see e.g. ``areal.v2.cli.inference``, +# ``areal.v2.cli.training``, ``areal.v2.cli.agent``. diff --git a/areal/experimental/cli/client.py b/areal/v2/cli/client.py similarity index 100% rename from areal/experimental/cli/client.py rename to areal/v2/cli/client.py diff --git a/areal/experimental/inference_service/data_proxy/__init__.py b/areal/v2/cli/commands/__init__.py similarity index 100% rename from areal/experimental/inference_service/data_proxy/__init__.py rename to areal/v2/cli/commands/__init__.py diff --git a/areal/experimental/cli/commands/logs.py b/areal/v2/cli/commands/logs.py similarity index 93% rename from areal/experimental/cli/commands/logs.py rename to areal/v2/cli/commands/logs.py index 98a0f3aa66..a4f0ae6703 100644 --- a/areal/experimental/cli/commands/logs.py +++ b/areal/v2/cli/commands/logs.py @@ -6,8 +6,8 @@ thing: resolve the service, find the log directory, tail one component file. This class packages that pattern. Subcommand CLIs do:: - from areal.experimental.cli.commands.logs import LogsCommand - from areal.experimental.cli.inference.lifecycle import inf_lifecycle + from areal.v2.cli.commands.logs import LogsCommand + from areal.v2.cli.inference.lifecycle import inf_lifecycle logs_cmd = LogsCommand(lifecycle=inf_lifecycle).build() inf.add_command(logs_cmd) @@ -20,7 +20,7 @@ import click -from areal.experimental.cli.lifecycle import ServiceLifecycle +from areal.v2.cli.lifecycle import ServiceLifecycle class LogsCommand: diff --git a/areal/experimental/cli/config.py b/areal/v2/cli/config.py similarity index 98% rename from areal/experimental/cli/config.py rename to areal/v2/cli/config.py index 0dd5203737..60abf2b809 100644 --- a/areal/experimental/cli/config.py +++ b/areal/v2/cli/config.py @@ -16,7 +16,7 @@ from pathlib import Path -from areal.experimental.cli.state import NamespacedStateStore +from areal.v2.cli.state import NamespacedStateStore # (section, key) → (verb_name_or_tuple, click_option_name) BindingMap = dict[tuple[str, str], tuple[str | tuple[str, ...], str]] diff --git a/areal/experimental/cli/lifecycle.py b/areal/v2/cli/lifecycle.py similarity index 97% rename from areal/experimental/cli/lifecycle.py rename to areal/v2/cli/lifecycle.py index da62c87c91..5f6b8985fc 100644 --- a/areal/experimental/cli/lifecycle.py +++ b/areal/v2/cli/lifecycle.py @@ -18,8 +18,8 @@ import click -from areal.experimental.cli.process import kill_pids -from areal.experimental.cli.state import DEFAULT_SERVICE, NamespacedStateStore +from areal.v2.cli.process import kill_pids +from areal.v2.cli.state import DEFAULT_SERVICE, NamespacedStateStore class ServiceLifecycle: diff --git a/areal/experimental/cli/main.py b/areal/v2/cli/main.py similarity index 72% rename from areal/experimental/cli/main.py rename to areal/v2/cli/main.py index 0929ca84ec..4d723a724d 100644 --- a/areal/experimental/cli/main.py +++ b/areal/v2/cli/main.py @@ -2,7 +2,7 @@ from __future__ import annotations -from areal.experimental.cli.cli import cli +from areal.v2.cli.cli import cli if __name__ == "__main__": cli() diff --git a/areal/experimental/cli/process.py b/areal/v2/cli/process.py similarity index 96% rename from areal/experimental/cli/process.py rename to areal/v2/cli/process.py index c72a4bf10e..9df92fda06 100644 --- a/areal/experimental/cli/process.py +++ b/areal/v2/cli/process.py @@ -4,7 +4,7 @@ These helpers operate on local PIDs and process trees. Remote backends (K8s / Slurm) should not consume them directly — route teardown through -``areal.experimental.cli.scheduler.terminate`` instead, which dispatches +``areal.v2.cli.scheduler.terminate`` instead, which dispatches per backend. For port allocation use ``areal.utils.network.find_free_ports``; it @@ -85,7 +85,7 @@ def kill_pids(pids: list[int], grace_s: float) -> None: which walks ``psutil.Process(pid).children(recursive=True)`` so grandchildren that escaped the original process group are still caught. Local-only — for remote backends use - ``areal.experimental.cli.scheduler.terminate``. + ``areal.v2.cli.scheduler.terminate``. """ for pid in pids: diff --git a/areal/experimental/cli/scheduler.py b/areal/v2/cli/scheduler.py similarity index 94% rename from areal/experimental/cli/scheduler.py rename to areal/v2/cli/scheduler.py index 8b9c9224aa..750d828f63 100644 --- a/areal/experimental/cli/scheduler.py +++ b/areal/v2/cli/scheduler.py @@ -10,7 +10,7 @@ from __future__ import annotations -from areal.experimental.cli.process import kill_pids +from areal.v2.cli.process import kill_pids def terminate( diff --git a/areal/experimental/cli/state.py b/areal/v2/cli/state.py similarity index 100% rename from areal/experimental/cli/state.py rename to areal/v2/cli/state.py diff --git a/areal/experimental/cli/status.py b/areal/v2/cli/status.py similarity index 98% rename from areal/experimental/cli/status.py rename to areal/v2/cli/status.py index 4e60721276..f3389168d6 100644 --- a/areal/experimental/cli/status.py +++ b/areal/v2/cli/status.py @@ -22,7 +22,7 @@ import click -from areal.experimental.cli.state import SupportsComponentProbe +from areal.v2.cli.state import SupportsComponentProbe @dataclass diff --git a/areal/experimental/cli/utils.py b/areal/v2/cli/utils.py similarity index 99% rename from areal/experimental/cli/utils.py rename to areal/v2/cli/utils.py index 16a11ba203..47a0a45604 100644 --- a/areal/experimental/cli/utils.py +++ b/areal/v2/cli/utils.py @@ -27,8 +27,8 @@ import click -from areal.experimental.cli.process import pid_alive from areal.utils.logging import LOGGER_COLORS_EXACT, getLogger +from areal.v2.cli.process import pid_alive # --------------------------------------------------------------------------- # File locking diff --git a/areal/experimental/cli/watcher.py b/areal/v2/cli/watcher.py similarity index 100% rename from areal/experimental/cli/watcher.py rename to areal/v2/cli/watcher.py diff --git a/areal/experimental/inference_service/__init__.py b/areal/v2/inference_service/__init__.py similarity index 60% rename from areal/experimental/inference_service/__init__.py rename to areal/v2/inference_service/__init__.py index edbc897123..1517b17181 100644 --- a/areal/experimental/inference_service/__init__.py +++ b/areal/v2/inference_service/__init__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -from areal.experimental.inference_service.controller.workflow import ( +from areal.v2.inference_service.controller.workflow import ( InferenceServiceWorkflow, ) diff --git a/areal/experimental/inference_service/backend.py b/areal/v2/inference_service/backend.py similarity index 100% rename from areal/experimental/inference_service/backend.py rename to areal/v2/inference_service/backend.py diff --git a/areal/experimental/inference_service/controller/__init__.py b/areal/v2/inference_service/controller/__init__.py similarity index 71% rename from areal/experimental/inference_service/controller/__init__.py rename to areal/v2/inference_service/controller/__init__.py index 9ac57d0d68..63c6127d57 100644 --- a/areal/experimental/inference_service/controller/__init__.py +++ b/areal/v2/inference_service/controller/__init__.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 from areal.api.cli_args import InferenceEngineConfig -from areal.experimental.inference_service.controller.controller import ( +from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) diff --git a/areal/experimental/inference_service/controller/controller.py b/areal/v2/inference_service/controller/controller.py similarity index 99% rename from areal/experimental/inference_service/controller/controller.py rename to areal/v2/inference_service/controller/controller.py index 6f45861c3b..40e3d13831 100644 --- a/areal/experimental/inference_service/controller/controller.py +++ b/areal/v2/inference_service/controller/controller.py @@ -338,7 +338,7 @@ async def _async_initialize( port_count=2, gpu=0, mem=8, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ) total_workers = dp_size else: @@ -357,7 +357,7 @@ async def _async_initialize( if inf_spec.gpu > 0: inf_spec.gpu = gpus_per_worker - inf_spec.cmd = "python -m areal.experimental.inference_service.guard" + inf_spec.cmd = "python -m areal.v2.inference_service.guard" inf_role = f"{self._worker_role}{self._INF_SUFFIX}" inf_job = Job( @@ -392,7 +392,7 @@ async def _async_initialize( router_cmd = [ sys.executable, "-m", - "areal.experimental.inference_service.router", + "areal.v2.inference_service.router", "--admin-api-key", admin_api_key, "--routing-strategy", @@ -451,7 +451,7 @@ async def _async_initialize( data_proxy_base_cmd = [ sys.executable, "-m", - "areal.experimental.inference_service.data_proxy", + "areal.v2.inference_service.data_proxy", "--tokenizer-path", cfg.tokenizer_path, "--admin-api-key", @@ -507,7 +507,7 @@ async def _fork_data_proxy(group_idx: int) -> tuple[str, int, str]: gw_cmd = [ sys.executable, "-m", - "areal.experimental.inference_service.gateway", + "areal.v2.inference_service.gateway", "--admin-api-key", admin_api_key, "--router-addr", @@ -1523,7 +1523,7 @@ def _wrap_agent(self, agent: Any, group_size: int = 1): group_size : int Number of parallel trajectories per episode. """ - from areal.experimental.inference_service.controller.workflow import ( + from areal.v2.inference_service.controller.workflow import ( InferenceServiceWorkflow, ) @@ -1593,7 +1593,7 @@ def _resolve_workflow( f"Got group_size={group_size}." ) - from areal.experimental.inference_service.controller.workflow import ( + from areal.v2.inference_service.controller.workflow import ( InferenceServiceWorkflow, ) diff --git a/areal/experimental/inference_service/controller/workflow.py b/areal/v2/inference_service/controller/workflow.py similarity index 99% rename from areal/experimental/inference_service/controller/workflow.py rename to areal/v2/inference_service/controller/workflow.py index 98be7bb1b8..dd51256ba7 100644 --- a/areal/experimental/inference_service/controller/workflow.py +++ b/areal/v2/inference_service/controller/workflow.py @@ -18,10 +18,10 @@ if TYPE_CHECKING: from areal.api.engine_api import InferenceEngine - from areal.experimental.inference_service.controller.controller import ( + from areal.experimental.openai.types import InteractionWithTokenLogpReward + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.openai.types import InteractionWithTokenLogpReward logger = logging.getLogger("InferenceServiceWorkflow") diff --git a/areal/experimental/inference_service/gateway/__init__.py b/areal/v2/inference_service/data_proxy/__init__.py similarity index 100% rename from areal/experimental/inference_service/gateway/__init__.py rename to areal/v2/inference_service/data_proxy/__init__.py diff --git a/areal/experimental/inference_service/data_proxy/__main__.py b/areal/v2/inference_service/data_proxy/__main__.py similarity index 92% rename from areal/experimental/inference_service/data_proxy/__main__.py rename to areal/v2/inference_service/data_proxy/__main__.py index 010c393e36..e0458bebf9 100644 --- a/areal/experimental/inference_service/data_proxy/__main__.py +++ b/areal/v2/inference_service/data_proxy/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint: python -m areal.experimental.inference_service.data_proxy""" +"""CLI entrypoint: python -m areal.v2.inference_service.data_proxy""" from __future__ import annotations @@ -8,14 +8,14 @@ import uvicorn -from areal.experimental.inference_service.data_proxy.app import create_app -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers from areal.utils.network import format_hostport +from areal.v2.inference_service.data_proxy.app import create_app +from areal.v2.inference_service.data_proxy.config import DataProxyConfig def main(): diff --git a/areal/experimental/inference_service/data_proxy/app.py b/areal/v2/inference_service/data_proxy/app.py similarity index 98% rename from areal/experimental/inference_service/data_proxy/app.py rename to areal/v2/inference_service/data_proxy/app.py index fd2794983c..5f703d7e86 100644 --- a/areal/experimental/inference_service/data_proxy/app.py +++ b/areal/v2/inference_service/data_proxy/app.py @@ -17,25 +17,6 @@ from flask import Flask from pydantic import BaseModel -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.pause import PauseState -from areal.experimental.inference_service.data_proxy.session import ( - ExportTrajectoriesRequest, - ExportTrajectoriesResponse, - ReadyNotification, - SessionCredentials, - SessionData, - SessionStore, - SetRewardRequest, - StartSessionRequest, - StartSessionResponse, -) -from areal.experimental.inference_service.data_proxy.tokenizer_proxy import ( - TokenizerProxy, -) -from areal.experimental.inference_service.inf_bridge import InfBridge -from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend -from areal.experimental.inference_service.vllm.bridge import VLLMBridgeBackend from areal.experimental.openai.client import ArealOpenAI from areal.experimental.openai.types import ( InteractionWithTokenLogpReward, @@ -49,6 +30,25 @@ from areal.infra.utils.http import create_httpx_client from areal.utils import logging from areal.utils.data import concat_padded_tensors +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.pause import PauseState +from areal.v2.inference_service.data_proxy.session import ( + ExportTrajectoriesRequest, + ExportTrajectoriesResponse, + ReadyNotification, + SessionCredentials, + SessionData, + SessionStore, + SetRewardRequest, + StartSessionRequest, + StartSessionResponse, +) +from areal.v2.inference_service.data_proxy.tokenizer_proxy import ( + TokenizerProxy, +) +from areal.v2.inference_service.inf_bridge import InfBridge +from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend +from areal.v2.inference_service.vllm.bridge import VLLMBridgeBackend logger = logging.getLogger("InferenceDataProxy") diff --git a/areal/experimental/inference_service/data_proxy/config.py b/areal/v2/inference_service/data_proxy/config.py similarity index 100% rename from areal/experimental/inference_service/data_proxy/config.py rename to areal/v2/inference_service/data_proxy/config.py diff --git a/areal/experimental/inference_service/data_proxy/pause.py b/areal/v2/inference_service/data_proxy/pause.py similarity index 100% rename from areal/experimental/inference_service/data_proxy/pause.py rename to areal/v2/inference_service/data_proxy/pause.py diff --git a/areal/experimental/inference_service/data_proxy/session.py b/areal/v2/inference_service/data_proxy/session.py similarity index 100% rename from areal/experimental/inference_service/data_proxy/session.py rename to areal/v2/inference_service/data_proxy/session.py diff --git a/areal/experimental/inference_service/data_proxy/tokenizer_proxy.py b/areal/v2/inference_service/data_proxy/tokenizer_proxy.py similarity index 100% rename from areal/experimental/inference_service/data_proxy/tokenizer_proxy.py rename to areal/v2/inference_service/data_proxy/tokenizer_proxy.py diff --git a/areal/experimental/inference_service/guard/__init__.py b/areal/v2/inference_service/gateway/__init__.py similarity index 100% rename from areal/experimental/inference_service/guard/__init__.py rename to areal/v2/inference_service/gateway/__init__.py diff --git a/areal/experimental/inference_service/gateway/__main__.py b/areal/v2/inference_service/gateway/__main__.py similarity index 88% rename from areal/experimental/inference_service/gateway/__main__.py rename to areal/v2/inference_service/gateway/__main__.py index 1d6845d91d..c5b278d8a6 100644 --- a/areal/experimental/inference_service/gateway/__main__.py +++ b/areal/v2/inference_service/gateway/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint for the inference gateway: ``python -m areal.experimental.inference_service.gateway``.""" +"""CLI entrypoint for the inference gateway: ``python -m areal.v2.inference_service.gateway``.""" from __future__ import annotations @@ -41,13 +41,13 @@ def main(): ) args, _ = parser.parse_known_args() - from areal.experimental.inference_service.gateway.app import create_app - from areal.experimental.inference_service.gateway.config import GatewayConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers + from areal.v2.inference_service.gateway.app import create_app + from areal.v2.inference_service.gateway.config import GatewayConfig validate_admin_api_key(args.host, args.admin_api_key) diff --git a/areal/experimental/inference_service/gateway/app.py b/areal/v2/inference_service/gateway/app.py similarity index 99% rename from areal/experimental/inference_service/gateway/app.py rename to areal/v2/inference_service/gateway/app.py index 902db4caad..a908780f2f 100644 --- a/areal/experimental/inference_service/gateway/app.py +++ b/areal/v2/inference_service/gateway/app.py @@ -18,12 +18,14 @@ from fastapi.responses import JSONResponse, Response, StreamingResponse from pydantic import BaseModel -from areal.experimental.inference_service.gateway.auth import ( +from areal.infra.utils.http import create_httpx_client +from areal.utils import logging +from areal.v2.inference_service.gateway.auth import ( extract_bearer_token, require_admin_key, ) -from areal.experimental.inference_service.gateway.config import GatewayConfig -from areal.experimental.inference_service.gateway.streaming import ( +from areal.v2.inference_service.gateway.config import GatewayConfig +from areal.v2.inference_service.gateway.streaming import ( RouterKeyRejectedError, RouterUnreachableError, _forwarding_headers, @@ -38,8 +40,6 @@ resolve_worker_addr, revoke_session_in_router, ) -from areal.infra.utils.http import create_httpx_client -from areal.utils import logging logger = logging.getLogger("InferenceGateway") diff --git a/areal/experimental/inference_service/gateway/auth.py b/areal/v2/inference_service/gateway/auth.py similarity index 100% rename from areal/experimental/inference_service/gateway/auth.py rename to areal/v2/inference_service/gateway/auth.py diff --git a/areal/experimental/inference_service/gateway/config.py b/areal/v2/inference_service/gateway/config.py similarity index 100% rename from areal/experimental/inference_service/gateway/config.py rename to areal/v2/inference_service/gateway/config.py diff --git a/areal/experimental/inference_service/gateway/streaming.py b/areal/v2/inference_service/gateway/streaming.py similarity index 100% rename from areal/experimental/inference_service/gateway/streaming.py rename to areal/v2/inference_service/gateway/streaming.py diff --git a/areal/experimental/inference_service/router/__init__.py b/areal/v2/inference_service/guard/__init__.py similarity index 100% rename from areal/experimental/inference_service/router/__init__.py rename to areal/v2/inference_service/guard/__init__.py diff --git a/areal/experimental/inference_service/guard/__main__.py b/areal/v2/inference_service/guard/__main__.py similarity index 79% rename from areal/experimental/inference_service/guard/__main__.py rename to areal/v2/inference_service/guard/__main__.py index 8e3a806b28..7710f46611 100644 --- a/areal/experimental/inference_service/guard/__main__.py +++ b/areal/v2/inference_service/guard/__main__.py @@ -1,18 +1,18 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint: ``python -m areal.experimental.inference_service.guard``""" +"""CLI entrypoint: ``python -m areal.v2.inference_service.guard``""" from __future__ import annotations -from areal.experimental.inference_service.guard.app import ( - _state, - app, -) from areal.infra.rpc.guard.app import ( configure_state_from_args, make_base_parser, run_server, ) +from areal.v2.inference_service.guard.app import ( + _state, + app, +) def main(): diff --git a/areal/experimental/inference_service/guard/app.py b/areal/v2/inference_service/guard/app.py similarity index 100% rename from areal/experimental/inference_service/guard/app.py rename to areal/v2/inference_service/guard/app.py diff --git a/areal/experimental/inference_service/inf_bridge.py b/areal/v2/inference_service/inf_bridge.py similarity index 98% rename from areal/experimental/inference_service/inf_bridge.py rename to areal/v2/inference_service/inf_bridge.py index f7532941f2..94ce45b118 100644 --- a/areal/experimental/inference_service/inf_bridge.py +++ b/areal/v2/inference_service/inf_bridge.py @@ -17,12 +17,12 @@ import numpy as np from areal.api.io_struct import HttpRequest -from areal.experimental.inference_service.backend import InfBridgeBackend from areal.utils import logging +from areal.v2.inference_service.backend import InfBridgeBackend if TYPE_CHECKING: from areal.api.io_struct import ModelRequest, ModelResponse - from areal.experimental.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.data_proxy.pause import PauseState _StopReason = Literal["length", "stop", "tool_calls", "abort"] diff --git a/areal/experimental/inference_service/sglang/__init__.py b/areal/v2/inference_service/router/__init__.py similarity index 100% rename from areal/experimental/inference_service/sglang/__init__.py rename to areal/v2/inference_service/router/__init__.py diff --git a/areal/experimental/inference_service/router/__main__.py b/areal/v2/inference_service/router/__main__.py similarity index 88% rename from areal/experimental/inference_service/router/__main__.py rename to areal/v2/inference_service/router/__main__.py index d2502bb146..5557abd812 100644 --- a/areal/experimental/inference_service/router/__main__.py +++ b/areal/v2/inference_service/router/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint for the router: ``python -m areal.experimental.inference_service.router``.""" +"""CLI entrypoint for the router: ``python -m areal.v2.inference_service.router``.""" from __future__ import annotations @@ -42,13 +42,13 @@ def main(): ) args, _ = parser.parse_known_args() - from areal.experimental.inference_service.router.app import create_app - from areal.experimental.inference_service.router.config import RouterConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers + from areal.v2.inference_service.router.app import create_app + from areal.v2.inference_service.router.config import RouterConfig validate_admin_api_key(args.host, args.admin_api_key) diff --git a/areal/experimental/inference_service/router/app.py b/areal/v2/inference_service/router/app.py similarity index 98% rename from areal/experimental/inference_service/router/app.py rename to areal/v2/inference_service/router/app.py index e8f5cb8b0a..6274327872 100644 --- a/areal/experimental/inference_service/router/app.py +++ b/areal/v2/inference_service/router/app.py @@ -7,7 +7,7 @@ It never proxies traffic — it only answers routing queries. Endpoint names are aligned with -``areal.experimental.agent_service.router.app``: +``areal.v2.agent_service.router.app``: ``/register``, ``/unregister``, ``/route``, ``/remove_session``. """ @@ -21,15 +21,15 @@ from fastapi import FastAPI, HTTPException, Request from pydantic import BaseModel -from areal.experimental.inference_service.router.config import RouterConfig -from areal.experimental.inference_service.router.state import ( +from areal.utils import logging +from areal.v2.inference_service.router.config import RouterConfig +from areal.v2.inference_service.router.state import ( GroupRegistry, ModelRegistry, SessionRegistry, WorkerRegistry, ) -from areal.experimental.inference_service.router.strategies import get_strategy -from areal.utils import logging +from areal.v2.inference_service.router.strategies import get_strategy logger = logging.getLogger("InferenceRouter") diff --git a/areal/experimental/inference_service/router/config.py b/areal/v2/inference_service/router/config.py similarity index 100% rename from areal/experimental/inference_service/router/config.py rename to areal/v2/inference_service/router/config.py diff --git a/areal/experimental/inference_service/router/state.py b/areal/v2/inference_service/router/state.py similarity index 100% rename from areal/experimental/inference_service/router/state.py rename to areal/v2/inference_service/router/state.py diff --git a/areal/experimental/inference_service/router/strategies.py b/areal/v2/inference_service/router/strategies.py similarity index 94% rename from areal/experimental/inference_service/router/strategies.py rename to areal/v2/inference_service/router/strategies.py index d76e2e87b9..3c7d7f8f3b 100644 --- a/areal/experimental/inference_service/router/strategies.py +++ b/areal/v2/inference_service/router/strategies.py @@ -6,7 +6,7 @@ from typing import Protocol -from areal.experimental.inference_service.router.state import WorkerInfo +from areal.v2.inference_service.router.state import WorkerInfo class RoutingStrategy(Protocol): diff --git a/areal/experimental/inference_service/vllm/__init__.py b/areal/v2/inference_service/sglang/__init__.py similarity index 100% rename from areal/experimental/inference_service/vllm/__init__.py rename to areal/v2/inference_service/sglang/__init__.py diff --git a/areal/experimental/inference_service/sglang/awex.py b/areal/v2/inference_service/sglang/awex.py similarity index 98% rename from areal/experimental/inference_service/sglang/awex.py rename to areal/v2/inference_service/sglang/awex.py index 6bd3f3c015..ceef2778f8 100644 --- a/areal/experimental/inference_service/sglang/awex.py +++ b/areal/v2/inference_service/sglang/awex.py @@ -9,7 +9,7 @@ from areal.utils import logging if TYPE_CHECKING: - from areal.experimental.inference_service.sglang.rpc_proxy import RpcProxy + from areal.v2.inference_service.sglang.rpc_proxy import RpcProxy logger = logging.getLogger("AwexInferenceEndpoints") diff --git a/areal/experimental/inference_service/sglang/bridge.py b/areal/v2/inference_service/sglang/bridge.py similarity index 100% rename from areal/experimental/inference_service/sglang/bridge.py rename to areal/v2/inference_service/sglang/bridge.py diff --git a/areal/experimental/inference_service/sglang/launch_server.py b/areal/v2/inference_service/sglang/launch_server.py similarity index 92% rename from areal/experimental/inference_service/sglang/launch_server.py rename to areal/v2/inference_service/sglang/launch_server.py index 1894398674..0406b2ba64 100644 --- a/areal/experimental/inference_service/sglang/launch_server.py +++ b/areal/v2/inference_service/sglang/launch_server.py @@ -24,11 +24,11 @@ def areal_launch_server(server_args) -> None: from sglang.srt.managers.detokenizer_manager import run_detokenizer_process # ---- BEGIN AREAL ---- - from areal.experimental.inference_service.sglang.awex import ( + from areal.v2.inference_service.sglang.awex import ( register_awex_endpoints, ) - from areal.experimental.inference_service.sglang.rpc_proxy import RpcProxy - from areal.experimental.inference_service.sglang.scheduler import ( + from areal.v2.inference_service.sglang.rpc_proxy import RpcProxy + from areal.v2.inference_service.sglang.scheduler import ( areal_run_scheduler_process, create_result_ipc, ) diff --git a/areal/experimental/inference_service/sglang/pp_bridge.py b/areal/v2/inference_service/sglang/pp_bridge.py similarity index 100% rename from areal/experimental/inference_service/sglang/pp_bridge.py rename to areal/v2/inference_service/sglang/pp_bridge.py diff --git a/areal/experimental/inference_service/sglang/rpc_proxy.py b/areal/v2/inference_service/sglang/rpc_proxy.py similarity index 100% rename from areal/experimental/inference_service/sglang/rpc_proxy.py rename to areal/v2/inference_service/sglang/rpc_proxy.py diff --git a/areal/experimental/inference_service/sglang/scheduler.py b/areal/v2/inference_service/sglang/scheduler.py similarity index 98% rename from areal/experimental/inference_service/sglang/scheduler.py rename to areal/v2/inference_service/sglang/scheduler.py index 34149a6fc9..57ce2db7c7 100644 --- a/areal/experimental/inference_service/sglang/scheduler.py +++ b/areal/v2/inference_service/sglang/scheduler.py @@ -72,7 +72,7 @@ def bind(self) -> None: def _require_adapter(self) -> Any: if self._adapter is None: - from areal.experimental.weight_update.awex.sglang_adapter import ( + from areal.v2.weight_update.awex.sglang_adapter import ( AwexSGLangAdapter, ) @@ -186,7 +186,7 @@ def areal_run_scheduler_process( ) from sglang.utils import get_exception_traceback - from areal.experimental.inference_service.sglang.pp_bridge import ( + from areal.v2.inference_service.sglang.pp_bridge import ( PPSchedulerBridge, ) diff --git a/areal/experimental/training_service/controller/__init__.py b/areal/v2/inference_service/vllm/__init__.py similarity index 100% rename from areal/experimental/training_service/controller/__init__.py rename to areal/v2/inference_service/vllm/__init__.py diff --git a/areal/experimental/inference_service/vllm/bridge.py b/areal/v2/inference_service/vllm/bridge.py similarity index 100% rename from areal/experimental/inference_service/vllm/bridge.py rename to areal/v2/inference_service/vllm/bridge.py diff --git a/areal/experimental/training_service/__init__.py b/areal/v2/training_service/__init__.py similarity index 100% rename from areal/experimental/training_service/__init__.py rename to areal/v2/training_service/__init__.py diff --git a/areal/experimental/training_service/data_proxy/__init__.py b/areal/v2/training_service/controller/__init__.py similarity index 100% rename from areal/experimental/training_service/data_proxy/__init__.py rename to areal/v2/training_service/controller/__init__.py diff --git a/areal/experimental/training_service/controller/controller.py b/areal/v2/training_service/controller/controller.py similarity index 98% rename from areal/experimental/training_service/controller/controller.py rename to areal/v2/training_service/controller/controller.py index 462e73d584..613153b88b 100644 --- a/areal/experimental/training_service/controller/controller.py +++ b/areal/v2/training_service/controller/controller.py @@ -178,7 +178,7 @@ async def _async_initialize( ) guard_spec = SchedulingSpec(**asdict(cfg.scheduling_spec[0])) - guard_spec.cmd = "python -m areal.experimental.training_service.guard" + guard_spec.cmd = "python -m areal.v2.training_service.guard" guard_role = f"{role}{self._GUARD_SUFFIX}" guard_job = Job( @@ -243,7 +243,7 @@ async def _fork_worker(rank: int) -> str: worker_cmd = [ sys.executable, "-m", - "areal.experimental.training_service.worker", + "areal.v2.training_service.worker", "--admin-api-key", cfg.admin_api_key, "--log-level", @@ -327,7 +327,7 @@ async def _fork_worker(rank: int) -> str: router_cmd = [ sys.executable, "-m", - "areal.experimental.training_service.router", + "areal.v2.training_service.router", "--admin-api-key", cfg.admin_api_key, "--log-level", @@ -351,7 +351,7 @@ async def _fork_worker(rank: int) -> str: data_proxy_cmd = [ sys.executable, "-m", - "areal.experimental.training_service.data_proxy", + "areal.v2.training_service.data_proxy", "--worker-addrs", ",".join(self._worker_addrs), "--admin-api-key", @@ -377,7 +377,7 @@ async def _fork_worker(rank: int) -> str: gw_cmd = [ sys.executable, "-m", - "areal.experimental.training_service.gateway", + "areal.v2.training_service.gateway", "--admin-api-key", cfg.admin_api_key, "--router-addr", @@ -936,13 +936,13 @@ def connect_engine(self, rollout: Any, meta: Any) -> None: self._ensure_initialized() import requests - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.weight_update.controller.config import ( + from areal.v2.weight_update.controller.config import ( WeightUpdateControllerConfig, ) - from areal.experimental.weight_update.controller.controller import ( + from areal.v2.weight_update.controller.controller import ( WeightUpdateController, ) diff --git a/areal/experimental/training_service/gateway/__init__.py b/areal/v2/training_service/data_proxy/__init__.py similarity index 100% rename from areal/experimental/training_service/gateway/__init__.py rename to areal/v2/training_service/data_proxy/__init__.py diff --git a/areal/experimental/training_service/data_proxy/__main__.py b/areal/v2/training_service/data_proxy/__main__.py similarity index 91% rename from areal/experimental/training_service/data_proxy/__main__.py rename to areal/v2/training_service/data_proxy/__main__.py index 70e18c2b08..82ff2cb515 100644 --- a/areal/experimental/training_service/data_proxy/__main__.py +++ b/areal/v2/training_service/data_proxy/__main__.py @@ -6,13 +6,13 @@ import uvicorn -from areal.experimental.training_service.data_proxy.app import create_app -from areal.experimental.training_service.data_proxy.config import TrainDataProxyConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers +from areal.v2.training_service.data_proxy.app import create_app +from areal.v2.training_service.data_proxy.config import TrainDataProxyConfig def main(): diff --git a/areal/experimental/training_service/data_proxy/app.py b/areal/v2/training_service/data_proxy/app.py similarity index 78% rename from areal/experimental/training_service/data_proxy/app.py rename to areal/v2/training_service/data_proxy/app.py index ac888918fb..55488f6bca 100644 --- a/areal/experimental/training_service/data_proxy/app.py +++ b/areal/v2/training_service/data_proxy/app.py @@ -7,11 +7,11 @@ from fastapi import FastAPI from fastapi.responses import Response as RawResponse -from areal.experimental.training_service.data_proxy.config import TrainDataProxyConfig -from areal.experimental.training_service.data_proxy.dispatcher import Dispatcher -from areal.experimental.training_service.data_proxy.engine import register_engine_routes -from areal.experimental.training_service.data_proxy.topology import discover_topology from areal.utils import logging +from areal.v2.training_service.data_proxy.config import TrainDataProxyConfig +from areal.v2.training_service.data_proxy.dispatcher import Dispatcher +from areal.v2.training_service.data_proxy.engine import register_engine_routes +from areal.v2.training_service.data_proxy.topology import discover_topology logger = logging.getLogger("TrainDataProxy") diff --git a/areal/experimental/training_service/data_proxy/config.py b/areal/v2/training_service/data_proxy/config.py similarity index 100% rename from areal/experimental/training_service/data_proxy/config.py rename to areal/v2/training_service/data_proxy/config.py diff --git a/areal/experimental/training_service/data_proxy/dispatcher.py b/areal/v2/training_service/data_proxy/dispatcher.py similarity index 99% rename from areal/experimental/training_service/data_proxy/dispatcher.py rename to areal/v2/training_service/data_proxy/dispatcher.py index 87df614c42..7f829a74b6 100644 --- a/areal/experimental/training_service/data_proxy/dispatcher.py +++ b/areal/v2/training_service/data_proxy/dispatcher.py @@ -34,7 +34,6 @@ import aiohttp import orjson -from areal.experimental.training_service.data_proxy.topology import WorkerTopology from areal.infra.controller.train_controller import ( _dispatch_tensors, _is_tensor_like, @@ -43,6 +42,7 @@ ) from areal.infra.rpc.serialization import deserialize_value, serialize_value from areal.utils import logging +from areal.v2.training_service.data_proxy.topology import WorkerTopology logger = logging.getLogger("TrainDataProxy") diff --git a/areal/experimental/training_service/data_proxy/engine.py b/areal/v2/training_service/data_proxy/engine.py similarity index 100% rename from areal/experimental/training_service/data_proxy/engine.py rename to areal/v2/training_service/data_proxy/engine.py diff --git a/areal/experimental/training_service/data_proxy/topology.py b/areal/v2/training_service/data_proxy/topology.py similarity index 100% rename from areal/experimental/training_service/data_proxy/topology.py rename to areal/v2/training_service/data_proxy/topology.py diff --git a/areal/experimental/training_service/router/__init__.py b/areal/v2/training_service/gateway/__init__.py similarity index 100% rename from areal/experimental/training_service/router/__init__.py rename to areal/v2/training_service/gateway/__init__.py diff --git a/areal/experimental/training_service/gateway/__main__.py b/areal/v2/training_service/gateway/__main__.py similarity index 92% rename from areal/experimental/training_service/gateway/__main__.py rename to areal/v2/training_service/gateway/__main__.py index a4cdfdff16..8940417e74 100644 --- a/areal/experimental/training_service/gateway/__main__.py +++ b/areal/v2/training_service/gateway/__main__.py @@ -39,13 +39,13 @@ def main(): ) args, _ = parser.parse_known_args() - from areal.experimental.training_service.gateway.app import create_app - from areal.experimental.training_service.gateway.config import GatewayConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers + from areal.v2.training_service.gateway.app import create_app + from areal.v2.training_service.gateway.config import GatewayConfig validate_admin_api_key(args.host, args.admin_api_key) diff --git a/areal/experimental/training_service/gateway/app.py b/areal/v2/training_service/gateway/app.py similarity index 92% rename from areal/experimental/training_service/gateway/app.py rename to areal/v2/training_service/gateway/app.py index f539a28b10..018273f1d8 100644 --- a/areal/experimental/training_service/gateway/app.py +++ b/areal/v2/training_service/gateway/app.py @@ -8,11 +8,11 @@ from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, Response -from areal.experimental.training_service.gateway import streaming -from areal.experimental.training_service.gateway.auth import extract_bearer_token -from areal.experimental.training_service.gateway.config import GatewayConfig -from areal.experimental.training_service.gateway.engine import register_engine_routes from areal.utils import logging +from areal.v2.training_service.gateway import streaming +from areal.v2.training_service.gateway.auth import extract_bearer_token +from areal.v2.training_service.gateway.config import GatewayConfig +from areal.v2.training_service.gateway.engine import register_engine_routes logger = logging.getLogger("TrainGateway") diff --git a/areal/experimental/training_service/gateway/auth.py b/areal/v2/training_service/gateway/auth.py similarity index 100% rename from areal/experimental/training_service/gateway/auth.py rename to areal/v2/training_service/gateway/auth.py diff --git a/areal/experimental/training_service/gateway/config.py b/areal/v2/training_service/gateway/config.py similarity index 100% rename from areal/experimental/training_service/gateway/config.py rename to areal/v2/training_service/gateway/config.py diff --git a/areal/experimental/training_service/gateway/engine.py b/areal/v2/training_service/gateway/engine.py similarity index 98% rename from areal/experimental/training_service/gateway/engine.py rename to areal/v2/training_service/gateway/engine.py index 736b5e4968..f14c195069 100644 --- a/areal/experimental/training_service/gateway/engine.py +++ b/areal/v2/training_service/gateway/engine.py @@ -7,7 +7,7 @@ from fastapi import FastAPI, Request -from areal.experimental.training_service.gateway.config import GatewayConfig +from areal.v2.training_service.gateway.config import GatewayConfig def register_engine_routes( diff --git a/areal/experimental/training_service/gateway/streaming.py b/areal/v2/training_service/gateway/streaming.py similarity index 100% rename from areal/experimental/training_service/gateway/streaming.py rename to areal/v2/training_service/gateway/streaming.py diff --git a/areal/experimental/training_service/guard/__init__.py b/areal/v2/training_service/guard/__init__.py similarity index 100% rename from areal/experimental/training_service/guard/__init__.py rename to areal/v2/training_service/guard/__init__.py diff --git a/areal/experimental/training_service/guard/__main__.py b/areal/v2/training_service/guard/__main__.py similarity index 77% rename from areal/experimental/training_service/guard/__main__.py rename to areal/v2/training_service/guard/__main__.py index 3028624721..02d648f781 100644 --- a/areal/experimental/training_service/guard/__main__.py +++ b/areal/v2/training_service/guard/__main__.py @@ -1,15 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint: ``python -m areal.experimental.training_service.guard``""" +"""CLI entrypoint: ``python -m areal.v2.training_service.guard``""" from __future__ import annotations -from areal.experimental.training_service.guard.app import _state, app from areal.infra.rpc.guard.app import ( configure_state_from_args, make_base_parser, run_server, ) +from areal.v2.training_service.guard.app import _state, app def main() -> None: diff --git a/areal/experimental/training_service/guard/app.py b/areal/v2/training_service/guard/app.py similarity index 100% rename from areal/experimental/training_service/guard/app.py rename to areal/v2/training_service/guard/app.py diff --git a/areal/experimental/training_service/worker/__init__.py b/areal/v2/training_service/router/__init__.py similarity index 100% rename from areal/experimental/training_service/worker/__init__.py rename to areal/v2/training_service/router/__init__.py diff --git a/areal/experimental/training_service/router/__main__.py b/areal/v2/training_service/router/__main__.py similarity index 92% rename from areal/experimental/training_service/router/__main__.py rename to areal/v2/training_service/router/__main__.py index 3eecfda2e9..bcbbb16372 100644 --- a/areal/experimental/training_service/router/__main__.py +++ b/areal/v2/training_service/router/__main__.py @@ -35,13 +35,13 @@ def main(): ) args, _ = parser.parse_known_args() - from areal.experimental.training_service.router.app import create_app - from areal.experimental.training_service.router.config import RouterConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers + from areal.v2.training_service.router.app import create_app + from areal.v2.training_service.router.config import RouterConfig validate_admin_api_key(args.host, args.admin_api_key) diff --git a/areal/experimental/training_service/router/app.py b/areal/v2/training_service/router/app.py similarity index 97% rename from areal/experimental/training_service/router/app.py rename to areal/v2/training_service/router/app.py index ad8b1aa257..1cca634595 100644 --- a/areal/experimental/training_service/router/app.py +++ b/areal/v2/training_service/router/app.py @@ -8,9 +8,9 @@ from contextlib import asynccontextmanager from typing import Any -from areal.experimental.training_service.router.config import RouterConfig -from areal.experimental.training_service.router.state import ModelRegistry from areal.utils import logging +from areal.v2.training_service.router.config import RouterConfig +from areal.v2.training_service.router.state import ModelRegistry httpx = importlib.import_module("httpx") fastapi = importlib.import_module("fastapi") diff --git a/areal/experimental/training_service/router/config.py b/areal/v2/training_service/router/config.py similarity index 100% rename from areal/experimental/training_service/router/config.py rename to areal/v2/training_service/router/config.py diff --git a/areal/experimental/training_service/router/state.py b/areal/v2/training_service/router/state.py similarity index 100% rename from areal/experimental/training_service/router/state.py rename to areal/v2/training_service/router/state.py diff --git a/areal/experimental/weight_update/gateway/__init__.py b/areal/v2/training_service/worker/__init__.py similarity index 100% rename from areal/experimental/weight_update/gateway/__init__.py rename to areal/v2/training_service/worker/__init__.py diff --git a/areal/experimental/training_service/worker/__main__.py b/areal/v2/training_service/worker/__main__.py similarity index 88% rename from areal/experimental/training_service/worker/__main__.py rename to areal/v2/training_service/worker/__main__.py index 4f253ce62e..6bff8cd768 100644 --- a/areal/experimental/training_service/worker/__main__.py +++ b/areal/v2/training_service/worker/__main__.py @@ -24,9 +24,9 @@ def main(): ) args, _ = parser.parse_known_args() - from areal.experimental.training_service.worker.app import create_app - from areal.experimental.training_service.worker.config import TrainWorkerConfig from areal.infra.utils.http import validate_admin_api_key + from areal.v2.training_service.worker.app import create_app + from areal.v2.training_service.worker.config import TrainWorkerConfig validate_admin_api_key(args.host, args.admin_api_key) diff --git a/areal/experimental/training_service/worker/app.py b/areal/v2/training_service/worker/app.py similarity index 96% rename from areal/experimental/training_service/worker/app.py rename to areal/v2/training_service/worker/app.py index ba5ac5bd45..ea812ebb36 100644 --- a/areal/experimental/training_service/worker/app.py +++ b/areal/v2/training_service/worker/app.py @@ -11,13 +11,13 @@ from typing import Any from areal.api import TrainEngine -from areal.experimental.training_service.worker.awex import create_awex_blueprint -from areal.experimental.training_service.worker.config import TrainWorkerConfig -from areal.experimental.training_service.worker.engine import create_engine_module from areal.infra.platforms import current_platform from areal.infra.rpc.serialization import deserialize_value, serialize_value from areal.utils import logging from areal.utils.data import broadcast_tensor_container, tensor_container_to +from areal.v2.training_service.worker.awex import create_awex_blueprint +from areal.v2.training_service.worker.config import TrainWorkerConfig +from areal.v2.training_service.worker.engine import create_engine_module logger = logging.getLogger("TrainWorker") diff --git a/areal/experimental/training_service/worker/awex.py b/areal/v2/training_service/worker/awex.py similarity index 97% rename from areal/experimental/training_service/worker/awex.py rename to areal/v2/training_service/worker/awex.py index e206810b83..1721bdfde5 100644 --- a/areal/experimental/training_service/worker/awex.py +++ b/areal/v2/training_service/worker/awex.py @@ -195,8 +195,8 @@ def action(): def _create_training_adapter(engine): from areal.engine.fsdp_engine import FSDPEngine from areal.engine.megatron_engine import MegatronEngine - from areal.experimental.weight_update.awex.fsdp_adapter import AwexFSDPAdapter - from areal.experimental.weight_update.awex.megatron_adapter import ( + from areal.v2.weight_update.awex.fsdp_adapter import AwexFSDPAdapter + from areal.v2.weight_update.awex.megatron_adapter import ( AwexMegatronAdapter, ) diff --git a/areal/experimental/training_service/worker/config.py b/areal/v2/training_service/worker/config.py similarity index 100% rename from areal/experimental/training_service/worker/config.py rename to areal/v2/training_service/worker/config.py diff --git a/areal/experimental/training_service/worker/engine.py b/areal/v2/training_service/worker/engine.py similarity index 100% rename from areal/experimental/training_service/worker/engine.py rename to areal/v2/training_service/worker/engine.py diff --git a/areal/experimental/weight_update/__init__.py b/areal/v2/weight_update/__init__.py similarity index 81% rename from areal/experimental/weight_update/__init__.py rename to areal/v2/weight_update/__init__.py index 14fd6c00a5..b71177ffa7 100644 --- a/areal/experimental/weight_update/__init__.py +++ b/areal/v2/weight_update/__init__.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 """Weight update protocol adapters for training and inference.""" -from areal.experimental.weight_update.controller import ( +from areal.v2.weight_update.controller import ( WeightUpdateController, WeightUpdateControllerConfig, ) diff --git a/areal/experimental/weight_update/awex/__init__.py b/areal/v2/weight_update/awex/__init__.py similarity index 100% rename from areal/experimental/weight_update/awex/__init__.py rename to areal/v2/weight_update/awex/__init__.py diff --git a/areal/experimental/weight_update/awex/fsdp_adapter.py b/areal/v2/weight_update/awex/fsdp_adapter.py similarity index 98% rename from areal/experimental/weight_update/awex/fsdp_adapter.py rename to areal/v2/weight_update/awex/fsdp_adapter.py index c5a3a987b0..2f33b60e63 100644 --- a/areal/experimental/weight_update/awex/fsdp_adapter.py +++ b/areal/v2/weight_update/awex/fsdp_adapter.py @@ -19,18 +19,18 @@ from torch.distributed.tensor.placement_types import Shard from areal.engine.core.model import is_qwen_vl_model -from areal.experimental.weight_update.awex import ( +from areal.utils import logging +from areal.v2.weight_update.awex import ( awex_wu_use_group, fetch_kv_metadata, ) -from areal.experimental.weight_update.nccl_group import ( +from areal.v2.weight_update.nccl_group import ( init_weights_update_group, setup_batch_isend_irecv, ) -from areal.experimental.weight_update.training_adapter import ( +from areal.v2.weight_update.training_adapter import ( AwexTrainingAdapter, ) -from areal.utils import logging if TYPE_CHECKING: from areal.engine.fsdp_engine import FSDPEngine diff --git a/areal/experimental/weight_update/awex/megatron_adapter.py b/areal/v2/weight_update/awex/megatron_adapter.py similarity index 99% rename from areal/experimental/weight_update/awex/megatron_adapter.py rename to areal/v2/weight_update/awex/megatron_adapter.py index 0fec179877..85306ff9c3 100644 --- a/areal/experimental/weight_update/awex/megatron_adapter.py +++ b/areal/v2/weight_update/awex/megatron_adapter.py @@ -24,18 +24,18 @@ group_tensors_by_shape_and_dtype, ) -from areal.experimental.weight_update.awex import ( +from areal.utils import logging +from areal.v2.weight_update.awex import ( awex_wu_use_group, fetch_kv_metadata, ) -from areal.experimental.weight_update.nccl_group import ( +from areal.v2.weight_update.nccl_group import ( init_weights_update_group, setup_batch_isend_irecv, ) -from areal.experimental.weight_update.training_adapter import ( +from areal.v2.weight_update.training_adapter import ( AwexTrainingAdapter, ) -from areal.utils import logging if TYPE_CHECKING: from areal.engine.megatron_engine import MegatronEngine diff --git a/areal/experimental/weight_update/awex/sglang_adapter.py b/areal/v2/weight_update/awex/sglang_adapter.py similarity index 99% rename from areal/experimental/weight_update/awex/sglang_adapter.py rename to areal/v2/weight_update/awex/sglang_adapter.py index 855fe9c5ef..d70b5d2cd0 100644 --- a/areal/experimental/weight_update/awex/sglang_adapter.py +++ b/areal/v2/weight_update/awex/sglang_adapter.py @@ -30,18 +30,18 @@ reconstruct_tensors_from_groups, ) -from areal.experimental.weight_update.awex import ( +from areal.utils import logging +from areal.v2.weight_update.awex import ( awex_wu_use_group, fetch_kv_metadata, ) -from areal.experimental.weight_update.inference_adapter import ( +from areal.v2.weight_update.inference_adapter import ( AwexInferenceAdapter, ) -from areal.experimental.weight_update.nccl_group import ( +from areal.v2.weight_update.nccl_group import ( init_weights_update_group, setup_batch_isend_irecv, ) -from areal.utils import logging logger = logging.getLogger("AwexSGLangAdapter") diff --git a/areal/experimental/weight_update/controller/__init__.py b/areal/v2/weight_update/controller/__init__.py similarity index 58% rename from areal/experimental/weight_update/controller/__init__.py rename to areal/v2/weight_update/controller/__init__.py index 09f0587d84..fd7bd8c7a0 100644 --- a/areal/experimental/weight_update/controller/__init__.py +++ b/areal/v2/weight_update/controller/__init__.py @@ -1,9 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 -from areal.experimental.weight_update.controller.config import ( +from areal.v2.weight_update.controller.config import ( WeightUpdateControllerConfig, ) -from areal.experimental.weight_update.controller.controller import ( +from areal.v2.weight_update.controller.controller import ( WeightUpdateController, ) diff --git a/areal/experimental/weight_update/controller/config.py b/areal/v2/weight_update/controller/config.py similarity index 100% rename from areal/experimental/weight_update/controller/config.py rename to areal/v2/weight_update/controller/config.py diff --git a/areal/experimental/weight_update/controller/controller.py b/areal/v2/weight_update/controller/controller.py similarity index 97% rename from areal/experimental/weight_update/controller/controller.py rename to areal/v2/weight_update/controller/controller.py index 558ea7fb9e..c880cac129 100644 --- a/areal/experimental/weight_update/controller/controller.py +++ b/areal/v2/weight_update/controller/controller.py @@ -9,13 +9,13 @@ import httpx -from areal.experimental.weight_update.controller.config import ( - WeightUpdateControllerConfig, -) -from areal.experimental.weight_update.gateway.config import WeightUpdateResult from areal.infra.utils.proc import kill_process_tree from areal.utils import logging from areal.utils.network import find_free_ports +from areal.v2.weight_update.controller.config import ( + WeightUpdateControllerConfig, +) +from areal.v2.weight_update.gateway.config import WeightUpdateResult logger = logging.getLogger("WeightUpdateController") @@ -47,7 +47,7 @@ def initialize(self) -> None: cmd = [ sys.executable, "-m", - "areal.experimental.weight_update.gateway", + "areal.v2.weight_update.gateway", "--host", cfg.host, "--port", diff --git a/tests/experimental/weight_update/__init__.py b/areal/v2/weight_update/gateway/__init__.py similarity index 100% rename from tests/experimental/weight_update/__init__.py rename to areal/v2/weight_update/gateway/__init__.py diff --git a/areal/experimental/weight_update/gateway/__main__.py b/areal/v2/weight_update/gateway/__main__.py similarity index 88% rename from areal/experimental/weight_update/gateway/__main__.py rename to areal/v2/weight_update/gateway/__main__.py index 513102e8b6..585858537b 100644 --- a/areal/experimental/weight_update/gateway/__main__.py +++ b/areal/v2/weight_update/gateway/__main__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""CLI entrypoint: ``python -m areal.experimental.weight_update.gateway``.""" +"""CLI entrypoint: ``python -m areal.v2.weight_update.gateway``.""" from __future__ import annotations @@ -36,13 +36,13 @@ def main(): ) args, _ = parser.parse_known_args() - from areal.experimental.weight_update.gateway.app import create_app - from areal.experimental.weight_update.gateway.config import WeightUpdateConfig from areal.infra.utils.http import ( get_default_uvicorn_kwargs, validate_admin_api_key, ) from areal.utils.logging import suppress_http_loggers + from areal.v2.weight_update.gateway.app import create_app + from areal.v2.weight_update.gateway.config import WeightUpdateConfig validate_admin_api_key(args.host, args.admin_api_key) diff --git a/areal/experimental/weight_update/gateway/app.py b/areal/v2/weight_update/gateway/app.py similarity index 98% rename from areal/experimental/weight_update/gateway/app.py rename to areal/v2/weight_update/gateway/app.py index 0a93b7fd19..d37392cbca 100644 --- a/areal/experimental/weight_update/gateway/app.py +++ b/areal/v2/weight_update/gateway/app.py @@ -13,17 +13,17 @@ from fastapi.responses import JSONResponse # pyright: ignore[reportMissingImports] from pydantic import BaseModel # pyright: ignore[reportMissingImports] -from areal.experimental.weight_update.gateway.auth import require_admin_key -from areal.experimental.weight_update.gateway.config import ( +from areal.infra.utils.http import async_http_retry +from areal.utils import logging +from areal.utils.network import find_free_ports +from areal.v2.weight_update.gateway.auth import require_admin_key +from areal.v2.weight_update.gateway.config import ( PairInfo, WeightUpdateConfig, WeightUpdateResult, ) -from areal.experimental.weight_update.gateway.kv_store import WeightMetaStore -from areal.experimental.weight_update.gateway.pair_registry import PairRegistry -from areal.infra.utils.http import async_http_retry -from areal.utils import logging -from areal.utils.network import find_free_ports +from areal.v2.weight_update.gateway.kv_store import WeightMetaStore +from areal.v2.weight_update.gateway.pair_registry import PairRegistry logger = logging.getLogger("WeightUpdateGateway") diff --git a/areal/experimental/weight_update/gateway/auth.py b/areal/v2/weight_update/gateway/auth.py similarity index 100% rename from areal/experimental/weight_update/gateway/auth.py rename to areal/v2/weight_update/gateway/auth.py diff --git a/areal/experimental/weight_update/gateway/config.py b/areal/v2/weight_update/gateway/config.py similarity index 100% rename from areal/experimental/weight_update/gateway/config.py rename to areal/v2/weight_update/gateway/config.py diff --git a/areal/experimental/weight_update/gateway/kv_store.py b/areal/v2/weight_update/gateway/kv_store.py similarity index 100% rename from areal/experimental/weight_update/gateway/kv_store.py rename to areal/v2/weight_update/gateway/kv_store.py diff --git a/areal/experimental/weight_update/gateway/pair_registry.py b/areal/v2/weight_update/gateway/pair_registry.py similarity index 94% rename from areal/experimental/weight_update/gateway/pair_registry.py rename to areal/v2/weight_update/gateway/pair_registry.py index 2b23b2497c..bbec35a565 100644 --- a/areal/experimental/weight_update/gateway/pair_registry.py +++ b/areal/v2/weight_update/gateway/pair_registry.py @@ -3,8 +3,8 @@ import threading -from areal.experimental.weight_update.gateway.config import PairInfo from areal.utils import logging +from areal.v2.weight_update.gateway.config import PairInfo logger = logging.getLogger("PairRegistry") diff --git a/areal/experimental/weight_update/inference_adapter.py b/areal/v2/weight_update/inference_adapter.py similarity index 100% rename from areal/experimental/weight_update/inference_adapter.py rename to areal/v2/weight_update/inference_adapter.py diff --git a/areal/experimental/weight_update/nccl_group.py b/areal/v2/weight_update/nccl_group.py similarity index 100% rename from areal/experimental/weight_update/nccl_group.py rename to areal/v2/weight_update/nccl_group.py diff --git a/areal/experimental/weight_update/training_adapter.py b/areal/v2/weight_update/training_adapter.py similarity index 100% rename from areal/experimental/weight_update/training_adapter.py rename to areal/v2/weight_update/training_adapter.py diff --git a/examples/agent_service/README.md b/examples/agent_service/README.md index 33e1deca5d..73371cfb45 100644 --- a/examples/agent_service/README.md +++ b/examples/agent_service/README.md @@ -84,7 +84,7 @@ Turn 2: Client → Gateway → Router (same DataProxy) → DataProxy → Worker import os from areal.api.cli_args import AgentConfig, SchedulingSpec -from areal.experimental.agent_service.controller import ( +from areal.v2.agent_service.controller import ( AgentController, ) from areal.infra.scheduler.local import LocalScheduler diff --git a/examples/agent_service/agent.py b/examples/agent_service/agent.py index c05f3bebe5..d6287038d6 100644 --- a/examples/agent_service/agent.py +++ b/examples/agent_service/agent.py @@ -30,12 +30,12 @@ ToolUseBlock, ) -from areal.experimental.agent_service.types import ( +from areal.utils import logging +from areal.v2.agent_service.types import ( AgentRequest, AgentResponse, EventEmitter, ) -from areal.utils import logging logger = logging.getLogger("ClaudeAgent") diff --git a/examples/agent_service/run_agent_service.py b/examples/agent_service/run_agent_service.py index 8f97ccbf32..799b40e4a0 100644 --- a/examples/agent_service/run_agent_service.py +++ b/examples/agent_service/run_agent_service.py @@ -22,7 +22,7 @@ import httpx from areal.api.cli_args import AgentConfig -from areal.experimental.agent_service.controller import AgentController +from areal.v2.agent_service.controller import AgentController async def _wait_healthy(url: str, timeout: float = 60.0) -> None: diff --git a/examples/experimental/inference_service/online_rollout.py b/examples/experimental/inference_service/online_rollout.py index a2f2d81677..74ee29c3ea 100644 --- a/examples/experimental/inference_service/online_rollout.py +++ b/examples/experimental/inference_service/online_rollout.py @@ -21,11 +21,11 @@ def main(args: list[str]) -> None: ext_args, remaining = parser.parse_known_args(args) from areal.api.cli_args import PPOConfig, load_expr_config - from areal.experimental.inference_service.controller.controller import ( - RolloutControllerV2, - ) from areal.utils import logging from areal.utils.environ import is_single_controller + from areal.v2.inference_service.controller.controller import ( + RolloutControllerV2, + ) logger = logging.getLogger("InferenceServiceOnlineTrain") diff --git a/examples/experimental/inference_service/tau2_rollout.py b/examples/experimental/inference_service/tau2_rollout.py index b78701b023..7bdfec0ef6 100644 --- a/examples/experimental/inference_service/tau2_rollout.py +++ b/examples/experimental/inference_service/tau2_rollout.py @@ -28,10 +28,10 @@ TrainDatasetConfig, load_expr_config, ) -from areal.experimental.inference_service.controller.controller import ( +from areal.utils import logging +from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) -from areal.utils import logging logger = logging.getLogger("Tau2GatewayRollout") diff --git a/examples/experimental/inference_service/tau2_rollout.yaml b/examples/experimental/inference_service/tau2_rollout.yaml index d76bab14aa..c467c56a48 100644 --- a/examples/experimental/inference_service/tau2_rollout.yaml +++ b/examples/experimental/inference_service/tau2_rollout.yaml @@ -56,7 +56,7 @@ rollout: gpu: 1 cpu: 2 mem: 16 - cmd: python3 -m areal.experimental.inference_service.guard + cmd: python3 -m areal.v2.inference_service.guard env_vars: AREAL_PROXY_WARN_ONCE: "1" diff --git a/pyproject.toml b/pyproject.toml index 9a3b8333b6..4ea91a4391 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,7 +189,7 @@ cli = [ ] [project.scripts] -areal = "areal.experimental.cli.main:cli" +areal = "areal.v2.cli.main:cli" [project.urls] "Homepage" = "https://github.com/areal-project/AReaL" diff --git a/pyproject.vllm.toml b/pyproject.vllm.toml index 3bcdd12f59..572a213d5f 100644 --- a/pyproject.vllm.toml +++ b/pyproject.vllm.toml @@ -200,7 +200,7 @@ cli = [ ] [project.scripts] -areal = "areal.experimental.cli.main:cli" +areal = "areal.v2.cli.main:cli" [project.urls] "Homepage" = "https://github.com/areal-project/AReaL" diff --git a/tests/test_examples.py b/tests/test_examples.py index 60db9e0557..7b8cf41805 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -815,7 +815,7 @@ def test_tau2(tmp_path_factory): [ "python3", "-m", - "areal.experimental.inference_service.sglang.launch_server", + "areal.v2.inference_service.sglang.launch_server", "--model-path", model_path, "--host", diff --git a/tests/test_recover.py b/tests/test_recover.py index 57e5634cd5..653e63a004 100644 --- a/tests/test_recover.py +++ b/tests/test_recover.py @@ -7,10 +7,10 @@ from areal.api.cli_args import RecoverConfig from areal.api.io_struct import FinetuneSpec, StepInfo -from areal.experimental.training_service.controller.controller import ( +from areal.utils.recover import RecoverHandler, check_if_auto_recover, check_if_recover +from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) -from areal.utils.recover import RecoverHandler, check_if_auto_recover, check_if_recover class TestRecoverConfig: diff --git a/tests/test_sglang_pp_unit.py b/tests/test_sglang_pp_unit.py index a9afe74263..203023c124 100644 --- a/tests/test_sglang_pp_unit.py +++ b/tests/test_sglang_pp_unit.py @@ -313,14 +313,14 @@ class TestPPBridgeModule: """Test the pp_bridge module can be imported and has expected symbols.""" def test_pp_bridge_class_exists(self): - from areal.experimental.inference_service.sglang.pp_bridge import ( + from areal.v2.inference_service.sglang.pp_bridge import ( PPSchedulerBridge, ) assert PPSchedulerBridge is not None def test_extract_pp_rank_from_group_name(self): - from areal.experimental.inference_service.sglang.pp_bridge import ( + from areal.v2.inference_service.sglang.pp_bridge import ( _extract_pp_rank_from_group_name, ) @@ -331,7 +331,7 @@ def test_extract_pp_rank_from_group_name(self): assert _extract_pp_rank_from_group_name("update_weight_group") is None def test_pp_bridge_bind_is_callable(self): - from areal.experimental.inference_service.sglang.pp_bridge import ( + from areal.v2.inference_service.sglang.pp_bridge import ( PPSchedulerBridge, ) @@ -339,7 +339,7 @@ def test_pp_bridge_bind_is_callable(self): def test_pp_bridge_noop_when_pp1(self): """PPSchedulerBridge.bind() should be a no-op when pp_size <= 1.""" - from areal.experimental.inference_service.sglang.pp_bridge import ( + from areal.v2.inference_service.sglang.pp_bridge import ( PPSchedulerBridge, ) diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py new file mode 100644 index 0000000000..a72302f3f2 --- /dev/null +++ b/tests/v2/__init__.py @@ -0,0 +1 @@ +"""V2 tests.""" diff --git a/tests/experimental/agent_service/__init__.py b/tests/v2/agent_service/__init__.py similarity index 100% rename from tests/experimental/agent_service/__init__.py rename to tests/v2/agent_service/__init__.py diff --git a/tests/experimental/agent_service/test_agent_router.py b/tests/v2/agent_service/test_agent_router.py similarity index 94% rename from tests/experimental/agent_service/test_agent_router.py rename to tests/v2/agent_service/test_agent_router.py index 13e682de23..365859bdd2 100644 --- a/tests/experimental/agent_service/test_agent_router.py +++ b/tests/v2/agent_service/test_agent_router.py @@ -4,9 +4,9 @@ import pytest -from areal.experimental.agent_service.auth import DEFAULT_ADMIN_API_KEY, admin_headers -from areal.experimental.agent_service.router.app import create_router_app -from areal.experimental.agent_service.router.config import RouterConfig +from areal.v2.agent_service.auth import DEFAULT_ADMIN_API_KEY, admin_headers +from areal.v2.agent_service.router.app import create_router_app +from areal.v2.agent_service.router.config import RouterConfig httpx = pytest.importorskip("httpx") diff --git a/tests/experimental/agent_service/test_agent_worker.py b/tests/v2/agent_service/test_agent_worker.py similarity index 95% rename from tests/experimental/agent_service/test_agent_worker.py rename to tests/v2/agent_service/test_agent_worker.py index 740622f877..4bcaa1d52a 100644 --- a/tests/experimental/agent_service/test_agent_worker.py +++ b/tests/v2/agent_service/test_agent_worker.py @@ -6,13 +6,13 @@ import pytest -from areal.experimental.agent_service.types import ( +from areal.v2.agent_service.types import ( AgentRequest, AgentResponse, AgentRunnable, EventEmitter, ) -from areal.experimental.agent_service.worker.app import ( +from areal.v2.agent_service.worker.app import ( _CollectingEmitter, create_worker_app, ) @@ -50,7 +50,7 @@ async def run( def _make_client(agent_cls): with patch( - "areal.experimental.agent_service.worker.app.import_from_string", + "areal.v2.agent_service.worker.app.import_from_string", return_value=agent_cls, ): app = create_worker_app("mock.path") diff --git a/tests/experimental/agent_service/test_controller.py b/tests/v2/agent_service/test_controller.py similarity index 97% rename from tests/experimental/agent_service/test_controller.py rename to tests/v2/agent_service/test_controller.py index 6f6aeb5f49..a7f846dfa9 100644 --- a/tests/experimental/agent_service/test_controller.py +++ b/tests/v2/agent_service/test_controller.py @@ -16,9 +16,9 @@ import pytest from areal.api.cli_args import AgentConfig, SchedulingSpec -from areal.experimental.agent_service.controller.controller import AgentController +from areal.v2.agent_service.controller.controller import AgentController -CTRL = "areal.experimental.agent_service.controller.controller" +CTRL = "areal.v2.agent_service.controller.controller" @dataclass @@ -170,10 +170,7 @@ def mock_post(url, **kwargs): create_call = scheduler.create_workers.call_args job = create_call.kwargs.get("job") or create_call.args[0] assert job.tasks[0].env_vars == {"ANTHROPIC_API_KEY": "test-anthropic-key"} - assert ( - job.tasks[0].cmd - == f"{sys.executable} -m areal.experimental.agent_service.guard" - ) + assert job.tasks[0].cmd == f"{sys.executable} -m areal.v2.agent_service.guard" assert all( payload.get("env") == {"ANTHROPIC_API_KEY": "test-anthropic-key"} for payload in fork_payloads diff --git a/tests/experimental/agent_service/test_guard.py b/tests/v2/agent_service/test_guard.py similarity index 91% rename from tests/experimental/agent_service/test_guard.py rename to tests/v2/agent_service/test_guard.py index 674808d618..e9e7dfdbfc 100644 --- a/tests/experimental/agent_service/test_guard.py +++ b/tests/v2/agent_service/test_guard.py @@ -4,7 +4,7 @@ The agent_blueprint has been removed in v2 — all orchestration logic now lives in AgentController. -Test structure mirrors ``tests/experimental/inference_service/test_guard.py``. +Test structure mirrors ``tests/v2/inference_service/test_guard.py``. """ from __future__ import annotations @@ -13,8 +13,8 @@ import pytest -from areal.experimental.agent_service.guard import app as guard_module -from areal.experimental.agent_service.guard.app import app +from areal.v2.agent_service.guard import app as guard_module +from areal.v2.agent_service.guard.app import app GUARD_APP = "areal.infra.rpc.guard.app" diff --git a/tests/experimental/agent_service/test_integration.py b/tests/v2/agent_service/test_integration.py similarity index 92% rename from tests/experimental/agent_service/test_integration.py rename to tests/v2/agent_service/test_integration.py index f815c5ca25..d5516d27b4 100644 --- a/tests/experimental/agent_service/test_integration.py +++ b/tests/v2/agent_service/test_integration.py @@ -10,20 +10,20 @@ import pytest -from areal.experimental.agent_service.auth import DEFAULT_ADMIN_API_KEY, admin_headers -from areal.experimental.agent_service.data_proxy.app import create_data_proxy_app -from areal.experimental.agent_service.data_proxy.config import DataProxyConfig -from areal.experimental.agent_service.gateway.app import create_gateway_app -from areal.experimental.agent_service.gateway.bridge import OpenResponsesBridge -from areal.experimental.agent_service.gateway.config import GatewayConfig -from areal.experimental.agent_service.router.app import create_router_app -from areal.experimental.agent_service.router.config import RouterConfig -from areal.experimental.agent_service.types import ( +from areal.v2.agent_service.auth import DEFAULT_ADMIN_API_KEY, admin_headers +from areal.v2.agent_service.data_proxy.app import create_data_proxy_app +from areal.v2.agent_service.data_proxy.config import DataProxyConfig +from areal.v2.agent_service.gateway.app import create_gateway_app +from areal.v2.agent_service.gateway.bridge import OpenResponsesBridge +from areal.v2.agent_service.gateway.config import GatewayConfig +from areal.v2.agent_service.router.app import create_router_app +from areal.v2.agent_service.router.config import RouterConfig +from areal.v2.agent_service.types import ( AgentRequest, AgentResponse, EventEmitter, ) -from areal.experimental.agent_service.worker.app import create_worker_app +from areal.v2.agent_service.worker.app import create_worker_app httpx = pytest.importorskip("httpx") @@ -54,7 +54,7 @@ async def run( def _make_worker_app(agent_cls): with patch( - "areal.experimental.agent_service.worker.app.import_from_string", + "areal.v2.agent_service.worker.app.import_from_string", return_value=agent_cls, ): return create_worker_app("mock.path") diff --git a/tests/experimental/agent_service/test_protocol.py b/tests/v2/agent_service/test_protocol.py similarity index 99% rename from tests/experimental/agent_service/test_protocol.py rename to tests/v2/agent_service/test_protocol.py index b2c3ca29ce..c34df8dce6 100644 --- a/tests/experimental/agent_service/test_protocol.py +++ b/tests/v2/agent_service/test_protocol.py @@ -6,7 +6,7 @@ import pytest -from areal.experimental.agent_service.protocol import ( +from areal.v2.agent_service.protocol import ( EventFrame, FrameType, RequestFrame, diff --git a/tests/experimental/inference_service/__init__.py b/tests/v2/inference_service/__init__.py similarity index 100% rename from tests/experimental/inference_service/__init__.py rename to tests/v2/inference_service/__init__.py diff --git a/tests/experimental/inference_service/conftest.py b/tests/v2/inference_service/conftest.py similarity index 86% rename from tests/experimental/inference_service/conftest.py rename to tests/v2/inference_service/conftest.py index 087b263b4b..fde627a8be 100644 --- a/tests/experimental/inference_service/conftest.py +++ b/tests/v2/inference_service/conftest.py @@ -3,7 +3,7 @@ The top-level ``areal/__init__.py`` imports from modules that use PEP 695 syntax (``def func[T](...)``) which is only valid on Python 3.12+. When running tests on Python 3.10/3.11 we register lightweight stubs for the ``areal`` namespace packages -so that importing ``areal.experimental.inference_service.data_proxy.*`` never triggers the +so that importing ``areal.v2.inference_service.data_proxy.*`` never triggers the problematic top-level init. """ @@ -20,9 +20,10 @@ _STUB_PACKAGES = [ ("areal", os.path.join(_REPO_ROOT, "areal")), ("areal.experimental", os.path.join(_REPO_ROOT, "areal", "experimental")), + ("areal.v2", os.path.join(_REPO_ROOT, "areal", "v2")), ( - "areal.experimental.inference_service", - os.path.join(_REPO_ROOT, "areal", "experimental", "inference_service"), + "areal.v2.inference_service", + os.path.join(_REPO_ROOT, "areal", "v2", "inference_service"), ), ] diff --git a/tests/experimental/inference_service/integration_utils.py b/tests/v2/inference_service/integration_utils.py similarity index 100% rename from tests/experimental/inference_service/integration_utils.py rename to tests/v2/inference_service/integration_utils.py diff --git a/tests/experimental/inference_service/test_controller.py b/tests/v2/inference_service/test_controller.py similarity index 97% rename from tests/experimental/inference_service/test_controller.py rename to tests/v2/inference_service/test_controller.py index 573fe90de7..2116c8dd11 100644 --- a/tests/experimental/inference_service/test_controller.py +++ b/tests/v2/inference_service/test_controller.py @@ -9,10 +9,10 @@ import pytest from areal.api.cli_args import AgentConfig, InferenceEngineConfig -from areal.experimental.inference_service.controller.controller import ( +from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) -from areal.experimental.inference_service.controller.workflow import ( +from areal.v2.inference_service.controller.workflow import ( InferenceServiceWorkflow, ) @@ -200,7 +200,7 @@ def test_resolve_workflow_rollout_workflow_class_raises(self): match="direct RolloutWorkflow classes are not supported", ): controller._resolve_workflow( - "areal.experimental.inference_service.controller.workflow.InferenceServiceWorkflow" + "areal.v2.inference_service.controller.workflow.InferenceServiceWorkflow" ) @@ -364,7 +364,7 @@ async def test_async_initialize_passes_callback_and_reward_timeout_to_data_proxy gpu=0, cpu=1, mem=1, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), admin_api_key="test-admin-key", @@ -532,13 +532,13 @@ async def test_online_mode_waits_on_controller(self): with ( patch( - "areal.experimental.inference_service.controller.workflow.workflow_context" + "areal.v2.inference_service.controller.workflow.workflow_context" ) as mock_wf_ctx, patch( - "areal.experimental.inference_service.controller.workflow.stats_tracker" + "areal.v2.inference_service.controller.workflow.stats_tracker" ) as mock_st, patch( - "areal.experimental.inference_service.controller.workflow.deserialize_interactions" + "areal.v2.inference_service.controller.workflow.deserialize_interactions" ) as mock_deserialize, ): mock_deserialize.return_value = {"chatcmpl-1": mock_interaction} @@ -595,10 +595,10 @@ async def run(self, data, **kwargs): with ( patch( - "areal.experimental.inference_service.controller.workflow.workflow_context" + "areal.v2.inference_service.controller.workflow.workflow_context" ) as mock_wf_ctx, patch( - "areal.experimental.inference_service.controller.workflow.stats_tracker" + "areal.v2.inference_service.controller.workflow.stats_tracker" ) as mock_st, ): mock_http_session = AsyncMock() diff --git a/tests/experimental/inference_service/test_controller_integration.py b/tests/v2/inference_service/test_controller_integration.py similarity index 97% rename from tests/experimental/inference_service/test_controller_integration.py rename to tests/v2/inference_service/test_controller_integration.py index 89711cd5c2..875b374c22 100644 --- a/tests/experimental/inference_service/test_controller_integration.py +++ b/tests/v2/inference_service/test_controller_integration.py @@ -2,7 +2,7 @@ Requires GPU and a model. Marked @pytest.mark.slow to exclude from default CI. Run manually: - uv run pytest tests/experimental/inference_service/test_controller_integration.py -v -s + uv run pytest tests/v2/inference_service/test_controller_integration.py -v -s Backend filtering: pytest -m "not vllm" # skip vLLM tests (run SGLang only) @@ -29,7 +29,7 @@ import torch from PIL import Image -from tests.experimental.inference_service.integration_utils import ( +from tests.v2.inference_service.integration_utils import ( EXPR_NAME, TRIAL_NAME, check_server_health, @@ -341,7 +341,7 @@ def _make_gateway_controller_config( gpu=0, cpu=1, mem=4, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), consumer_batch_size=8, @@ -367,7 +367,7 @@ def gateway_controller(sglang_server, model_path, tmp_path_factory): if not has_gpu(): pytest.skip("GPU required") - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) @@ -410,7 +410,7 @@ def gateway_controller_full_init(request, model_path, tmp_path_factory): backend = request.param from areal.api.cli_args import InferenceEngineConfig, SchedulingSpec - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) @@ -419,9 +419,7 @@ def gateway_controller_full_init(request, model_path, tmp_path_factory): model=model_path, backend=f"{backend}:d1", scheduling_spec=( - SchedulingSpec( - gpu=1, cmd="python -m areal.experimental.inference_service.guard" - ), + SchedulingSpec(gpu=1, cmd="python -m areal.v2.inference_service.guard"), ), consumer_batch_size=8, max_head_offpolicyness=1024, @@ -462,7 +460,7 @@ def gateway_controller_full_init_online(request, model_path, tmp_path_factory): InferenceEngineConfig, SchedulingSpec, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) @@ -475,9 +473,7 @@ def gateway_controller_full_init_online(request, model_path, tmp_path_factory): mode="online", ), scheduling_spec=( - SchedulingSpec( - gpu=1, cmd="python -m areal.experimental.inference_service.guard" - ), + SchedulingSpec(gpu=1, cmd="python -m areal.v2.inference_service.guard"), ), consumer_batch_size=8, max_head_offpolicyness=1024, @@ -520,7 +516,7 @@ def gateway_controller_full_init_with_reward_timeout( InferenceEngineConfig, SchedulingSpec, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) @@ -534,9 +530,7 @@ def gateway_controller_full_init_with_reward_timeout( set_reward_finish_timeout=3.0, ), scheduling_spec=( - SchedulingSpec( - gpu=1, cmd="python -m areal.experimental.inference_service.guard" - ), + SchedulingSpec(gpu=1, cmd="python -m areal.v2.inference_service.guard"), ), consumer_batch_size=8, max_head_offpolicyness=1024, @@ -579,7 +573,7 @@ def gateway_controller_full_init_vlm(request, vlm_model_path, tmp_path_factory): backend = request.param from areal.api.cli_args import InferenceEngineConfig, SchedulingSpec - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) @@ -588,9 +582,7 @@ def gateway_controller_full_init_vlm(request, vlm_model_path, tmp_path_factory): model=vlm_model_path, backend=f"{backend}:d1", scheduling_spec=( - SchedulingSpec( - gpu=1, cmd="python -m areal.experimental.inference_service.guard" - ), + SchedulingSpec(gpu=1, cmd="python -m areal.v2.inference_service.guard"), ), consumer_batch_size=8, max_head_offpolicyness=1024, diff --git a/tests/experimental/inference_service/test_controller_version.py b/tests/v2/inference_service/test_controller_version.py similarity index 98% rename from tests/experimental/inference_service/test_controller_version.py rename to tests/v2/inference_service/test_controller_version.py index bba3fa7841..af7ae18b73 100644 --- a/tests/experimental/inference_service/test_controller_version.py +++ b/tests/v2/inference_service/test_controller_version.py @@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from areal.api.cli_args import InferenceEngineConfig, SchedulingSpec -from areal.experimental.inference_service.controller.controller import ( +from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) diff --git a/tests/experimental/inference_service/test_data_proxy_chat.py b/tests/v2/inference_service/test_data_proxy_chat.py similarity index 98% rename from tests/experimental/inference_service/test_data_proxy_chat.py rename to tests/v2/inference_service/test_data_proxy_chat.py index 2965e70270..8c241374fb 100644 --- a/tests/experimental/inference_service/test_data_proxy_chat.py +++ b/tests/v2/inference_service/test_data_proxy_chat.py @@ -9,12 +9,12 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.data_proxy.app import ( +from areal.v2.inference_service.data_proxy.app import ( _flush_ready_trajectories, create_app, ) -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.session import ( +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.session import ( SessionData, SessionStore, ) @@ -121,9 +121,9 @@ async def _mock_create(*, areal_cache=None, **kwargs): @pytest_asyncio.fixture async def client(config, mock_tokenizer, mock_areal_client): """Create app with mocked deps and yield an httpx async client.""" - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend app = create_app(config) # Bypass lifespan — inject mocks directly into app.state @@ -608,7 +608,7 @@ async def _mock_post_callback( return True monkeypatch.setattr( - "areal.experimental.inference_service.data_proxy.app._post_online_ready_callback", + "areal.v2.inference_service.data_proxy.app._post_online_ready_callback", _mock_post_callback, ) diff --git a/tests/experimental/inference_service/test_data_proxy_integration.py b/tests/v2/inference_service/test_data_proxy_integration.py similarity index 97% rename from tests/experimental/inference_service/test_data_proxy_integration.py rename to tests/v2/inference_service/test_data_proxy_integration.py index 8ce11f900c..0d68fc98a2 100644 --- a/tests/experimental/inference_service/test_data_proxy_integration.py +++ b/tests/v2/inference_service/test_data_proxy_integration.py @@ -1,7 +1,7 @@ """Integration tests for data proxy with a real SGLang server. Requires GPU and a model. Run with: - uv run pytest tests/experimental/inference_service/test_data_proxy_integration.py -v -s + uv run pytest tests/v2/inference_service/test_data_proxy_integration.py -v -s The test launches an SGLang server subprocess, starts the data proxy FastAPI app, and exercises the /chat/completions endpoint (full session lifecycle) and @@ -103,7 +103,7 @@ def vllm_server(): if not _has_gpu(): pytest.skip("GPU required for vLLM server") - from tests.experimental.inference_service.integration_utils import ( + from tests.v2.inference_service.integration_utils import ( launch_vllm_server, ) @@ -126,16 +126,16 @@ def model_path() -> str: def _create_data_proxy_app_with_sessions(sglang_server, model_path): """Create a fully-wired data proxy app with session support.""" - from areal.experimental.inference_service.data_proxy.app import create_app - from areal.experimental.inference_service.data_proxy.config import DataProxyConfig - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.data_proxy.session import SessionStore - from areal.experimental.inference_service.data_proxy.tokenizer_proxy import ( + from areal.experimental.openai.client import ArealOpenAI + from areal.v2.inference_service.data_proxy.app import create_app + from areal.v2.inference_service.data_proxy.config import DataProxyConfig + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.data_proxy.session import SessionStore + from areal.v2.inference_service.data_proxy.tokenizer_proxy import ( TokenizerProxy, ) - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend - from areal.experimental.openai.client import ArealOpenAI + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend config = DataProxyConfig( host="127.0.0.1", @@ -171,16 +171,16 @@ def _create_data_proxy_app_with_sessions(sglang_server, model_path): def _create_data_proxy_app_vllm(vllm_server, model_path): """Create a data proxy app backed by vLLM with session support.""" - from areal.experimental.inference_service.data_proxy.app import create_app - from areal.experimental.inference_service.data_proxy.config import DataProxyConfig - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.data_proxy.session import SessionStore - from areal.experimental.inference_service.data_proxy.tokenizer_proxy import ( + from areal.experimental.openai.client import ArealOpenAI + from areal.v2.inference_service.data_proxy.app import create_app + from areal.v2.inference_service.data_proxy.config import DataProxyConfig + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.data_proxy.session import SessionStore + from areal.v2.inference_service.data_proxy.tokenizer_proxy import ( TokenizerProxy, ) - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.vllm.bridge import VLLMBridgeBackend - from areal.experimental.openai.client import ArealOpenAI + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.vllm.bridge import VLLMBridgeBackend config = DataProxyConfig( host="127.0.0.1", diff --git a/tests/experimental/inference_service/test_data_proxy_pause.py b/tests/v2/inference_service/test_data_proxy_pause.py similarity index 94% rename from tests/experimental/inference_service/test_data_proxy_pause.py rename to tests/v2/inference_service/test_data_proxy_pause.py index 330702032e..32fea7781d 100644 --- a/tests/experimental/inference_service/test_data_proxy_pause.py +++ b/tests/v2/inference_service/test_data_proxy_pause.py @@ -8,10 +8,10 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.data_proxy.app import create_app -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.pause import PauseState -from areal.experimental.inference_service.data_proxy.session import SessionStore +from areal.v2.inference_service.data_proxy.app import create_app +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.pause import PauseState +from areal.v2.inference_service.data_proxy.session import SessionStore # ============================================================================= # Fixtures @@ -99,8 +99,8 @@ async def _mock_create(*, areal_cache=None, **kwargs): @pytest_asyncio.fixture async def app_client(config, mock_tokenizer, mock_areal_client): """Create an ASGI test client with all app.state attributes injected.""" - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend app = create_app(config) diff --git a/tests/experimental/inference_service/test_data_proxy_rtensor.py b/tests/v2/inference_service/test_data_proxy_rtensor.py similarity index 97% rename from tests/experimental/inference_service/test_data_proxy_rtensor.py rename to tests/v2/inference_service/test_data_proxy_rtensor.py index 306cc769bd..2dd5923043 100644 --- a/tests/experimental/inference_service/test_data_proxy_rtensor.py +++ b/tests/v2/inference_service/test_data_proxy_rtensor.py @@ -18,11 +18,11 @@ import pytest_asyncio import torch -from areal.experimental.inference_service.data_proxy.app import create_app -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.session import SessionStore from areal.infra.rpc import rtensor as rtensor_storage from areal.infra.rpc.serialization import deserialize_value, serialize_value +from areal.v2.inference_service.data_proxy.app import create_app +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.session import SessionStore # ============================================================================= # Fixtures @@ -51,7 +51,7 @@ def config(): @pytest_asyncio.fixture async def client(config): - from areal.experimental.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.data_proxy.pause import PauseState app = create_app(config) pause_state = PauseState() diff --git a/tests/experimental/inference_service/test_data_proxy_standalone.py b/tests/v2/inference_service/test_data_proxy_standalone.py similarity index 91% rename from tests/experimental/inference_service/test_data_proxy_standalone.py rename to tests/v2/inference_service/test_data_proxy_standalone.py index 0a29516dee..3dd723b4f1 100644 --- a/tests/experimental/inference_service/test_data_proxy_standalone.py +++ b/tests/v2/inference_service/test_data_proxy_standalone.py @@ -13,9 +13,9 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.data_proxy.app import create_app -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.session import SessionStore +from areal.v2.inference_service.data_proxy.app import create_app +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.session import SessionStore # ============================================================================= # Fixtures @@ -85,9 +85,9 @@ async def _mock_create(*, areal_cache=None, **kwargs): @pytest_asyncio.fixture async def client(config, mock_tokenizer, mock_areal_client): """Create app with mocked deps and yield an httpx async client (no auth header).""" - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend app = create_app(config) pause_state = PauseState() @@ -123,11 +123,11 @@ def admin_headers(): class TestStandaloneChat: def test_config_can_select_vllm_backend(self): """backend_type=vllm creates a vLLM-backed InfBridge.""" - from areal.experimental.inference_service.data_proxy.app import ( + from areal.v2.inference_service.data_proxy.app import ( _create_inf_bridge, ) - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.vllm.bridge import VLLMBridgeBackend + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.vllm.bridge import VLLMBridgeBackend config = DataProxyConfig( host="127.0.0.1", diff --git a/tests/experimental/inference_service/test_data_proxy_version.py b/tests/v2/inference_service/test_data_proxy_version.py similarity index 93% rename from tests/experimental/inference_service/test_data_proxy_version.py rename to tests/v2/inference_service/test_data_proxy_version.py index 6a65d1cc01..c5031b33a9 100644 --- a/tests/experimental/inference_service/test_data_proxy_version.py +++ b/tests/v2/inference_service/test_data_proxy_version.py @@ -8,10 +8,10 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.data_proxy.app import create_app -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.pause import PauseState -from areal.experimental.inference_service.data_proxy.session import SessionStore +from areal.v2.inference_service.data_proxy.app import create_app +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.pause import PauseState +from areal.v2.inference_service.data_proxy.session import SessionStore # ============================================================================= # Fixtures @@ -99,8 +99,8 @@ async def _mock_create(*, areal_cache=None, **kwargs): @pytest_asyncio.fixture async def app_client(config, mock_tokenizer, mock_areal_client): """Create an ASGI test client with all app.state attributes injected.""" - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend app = create_app(config) diff --git a/tests/experimental/inference_service/test_examples.py b/tests/v2/inference_service/test_examples.py similarity index 100% rename from tests/experimental/inference_service/test_examples.py rename to tests/v2/inference_service/test_examples.py diff --git a/tests/experimental/inference_service/test_external_model.py b/tests/v2/inference_service/test_external_model.py similarity index 96% rename from tests/experimental/inference_service/test_external_model.py rename to tests/v2/inference_service/test_external_model.py index 8a6430f592..93dc8bd580 100644 --- a/tests/experimental/inference_service/test_external_model.py +++ b/tests/v2/inference_service/test_external_model.py @@ -8,28 +8,28 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.data_proxy.app import ( +from areal.v2.inference_service.data_proxy.app import ( create_app as create_data_proxy_app, ) -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.session import SessionStore -from areal.experimental.inference_service.gateway.app import ( +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.session import SessionStore +from areal.v2.inference_service.gateway.app import ( create_app as create_gateway_app, ) -from areal.experimental.inference_service.gateway.config import GatewayConfig -from areal.experimental.inference_service.gateway.streaming import ( +from areal.v2.inference_service.gateway.config import GatewayConfig +from areal.v2.inference_service.gateway.streaming import ( RouterKeyRejectedError, ) -from areal.experimental.inference_service.router.app import ( +from areal.v2.inference_service.router.app import ( create_app as create_router_app, ) -from areal.experimental.inference_service.router.config import RouterConfig -from areal.experimental.inference_service.router.state import ModelRegistry +from areal.v2.inference_service.router.config import RouterConfig +from areal.v2.inference_service.router.state import ModelRegistry ADMIN_KEY = "test-admin-key" SESSION_KEY = "session-key-abc123" WORKER_ADDR = "http://worker-1:18082" -ROUTER_MODULE = "areal.experimental.inference_service.gateway.app" +ROUTER_MODULE = "areal.v2.inference_service.gateway.app" def admin_headers() -> dict[str, str]: @@ -411,9 +411,9 @@ def mock_areal_client(): @pytest_asyncio.fixture async def data_proxy_app(data_proxy_config, mock_tokenizer, mock_areal_client): - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend app = create_data_proxy_app(data_proxy_config) pause_state = PauseState() @@ -555,7 +555,7 @@ def stream(self, method, url, json=None, headers=None): return _FakeStreamResponse() monkeypatch.setattr( - "areal.experimental.inference_service.data_proxy.app.httpx.AsyncClient", + "areal.v2.inference_service.data_proxy.app.httpx.AsyncClient", _FakeClient, ) diff --git a/tests/experimental/inference_service/test_external_model_integration.py b/tests/v2/inference_service/test_external_model_integration.py similarity index 94% rename from tests/experimental/inference_service/test_external_model_integration.py rename to tests/v2/inference_service/test_external_model_integration.py index 9d2b7ad048..b8a13eb2cc 100644 --- a/tests/experimental/inference_service/test_external_model_integration.py +++ b/tests/v2/inference_service/test_external_model_integration.py @@ -9,26 +9,26 @@ from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -from areal.experimental.inference_service.data_proxy.app import ( +from areal.v2.inference_service.data_proxy.app import ( create_app as create_data_proxy_app, ) -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.session import SessionStore -from areal.experimental.inference_service.gateway.app import ( +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.session import SessionStore +from areal.v2.inference_service.gateway.app import ( create_app as create_gateway_app, ) -from areal.experimental.inference_service.gateway.config import GatewayConfig -from areal.experimental.inference_service.gateway.streaming import ( +from areal.v2.inference_service.gateway.config import GatewayConfig +from areal.v2.inference_service.gateway.streaming import ( RouterKeyRejectedError, ) -from areal.experimental.inference_service.router.app import ( +from areal.v2.inference_service.router.app import ( create_app as create_router_app, ) -from areal.experimental.inference_service.router.config import RouterConfig +from areal.v2.inference_service.router.config import RouterConfig ADMIN_KEY = "test-admin-key" WORKER_ADDR = "http://worker-1:18082" -ROUTER_MODULE = "areal.experimental.inference_service.gateway.app" +ROUTER_MODULE = "areal.v2.inference_service.gateway.app" def admin_headers() -> dict[str, str]: @@ -339,7 +339,7 @@ async def post(self, url, json=None, headers=None): new=AsyncMock(side_effect=_forward_request), ), patch( - "areal.experimental.inference_service.data_proxy.app.httpx.AsyncClient", + "areal.v2.inference_service.data_proxy.app.httpx.AsyncClient", _ExternalClient, ), ): diff --git a/tests/experimental/inference_service/test_gateway.py b/tests/v2/inference_service/test_gateway.py similarity index 98% rename from tests/experimental/inference_service/test_gateway.py rename to tests/v2/inference_service/test_gateway.py index 5f9830a48b..a57f209914 100644 --- a/tests/experimental/inference_service/test_gateway.py +++ b/tests/v2/inference_service/test_gateway.py @@ -12,9 +12,9 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.gateway.app import create_app -from areal.experimental.inference_service.gateway.config import GatewayConfig -from areal.experimental.inference_service.gateway.streaming import ( +from areal.v2.inference_service.gateway.app import create_app +from areal.v2.inference_service.gateway.config import GatewayConfig +from areal.v2.inference_service.gateway.streaming import ( RouterKeyRejectedError, RouterUnreachableError, ) @@ -29,7 +29,7 @@ WORKER_ADDR_2 = "http://worker-2:18082" WORKER_ADDR_3 = "http://worker-3:18082" -MODULE = "areal.experimental.inference_service.gateway.app" +MODULE = "areal.v2.inference_service.gateway.app" @pytest.fixture diff --git a/tests/experimental/inference_service/test_gateway_integration.py b/tests/v2/inference_service/test_gateway_integration.py similarity index 97% rename from tests/experimental/inference_service/test_gateway_integration.py rename to tests/v2/inference_service/test_gateway_integration.py index 52be4b1444..cad2b28a24 100644 --- a/tests/experimental/inference_service/test_gateway_integration.py +++ b/tests/v2/inference_service/test_gateway_integration.py @@ -1,7 +1,7 @@ """Full-stack integration test: client → Gateway → Router → Data Proxy → SGLang. Requires GPU and a model. Run with: - uv run pytest tests/experimental/inference_service/test_gateway_integration.py -v -s + uv run pytest tests/v2/inference_service/test_gateway_integration.py -v -s The test launches: 1. A real SGLang server (GPU subprocess) @@ -136,7 +136,7 @@ def vllm_server(): if not _has_gpu(): pytest.skip("GPU required for vLLM server") - from tests.experimental.inference_service.integration_utils import ( + from tests.v2.inference_service.integration_utils import ( launch_vllm_server, ) @@ -169,10 +169,10 @@ def gateway_stack(sglang_server, model_path): gateway_addr = f"http://{bind_host}:{gateway_port}" # --- Create Data Proxy app --- - from areal.experimental.inference_service.data_proxy.app import ( + from areal.v2.inference_service.data_proxy.app import ( create_app as create_dp_app, ) - from areal.experimental.inference_service.data_proxy.config import DataProxyConfig + from areal.v2.inference_service.data_proxy.config import DataProxyConfig dp_config = DataProxyConfig( host=bind_host, @@ -185,10 +185,10 @@ def gateway_stack(sglang_server, model_path): dp_app = create_dp_app(dp_config) # --- Create Router app --- - from areal.experimental.inference_service.router.app import ( + from areal.v2.inference_service.router.app import ( create_app as create_router_app, ) - from areal.experimental.inference_service.router.config import RouterConfig + from areal.v2.inference_service.router.config import RouterConfig router_config = RouterConfig( host=bind_host, @@ -201,10 +201,10 @@ def gateway_stack(sglang_server, model_path): router_app = create_router_app(router_config) # --- Create Gateway app --- - from areal.experimental.inference_service.gateway.app import ( + from areal.v2.inference_service.gateway.app import ( create_app as create_gw_app, ) - from areal.experimental.inference_service.gateway.config import GatewayConfig + from areal.v2.inference_service.gateway.config import GatewayConfig gw_config = GatewayConfig( host=bind_host, @@ -798,18 +798,18 @@ async def pause_then_resume(): @pytest.fixture(scope="module") def gateway_stack_vllm(vllm_server, model_path): """Launch the full gateway stack backed by vLLM on free ports.""" - from areal.experimental.inference_service.data_proxy.app import ( + from areal.v2.inference_service.data_proxy.app import ( create_app as create_dp_app, ) - from areal.experimental.inference_service.data_proxy.config import DataProxyConfig - from areal.experimental.inference_service.gateway.app import ( + from areal.v2.inference_service.data_proxy.config import DataProxyConfig + from areal.v2.inference_service.gateway.app import ( create_app as create_gw_app, ) - from areal.experimental.inference_service.gateway.config import GatewayConfig - from areal.experimental.inference_service.router.app import ( + from areal.v2.inference_service.gateway.config import GatewayConfig + from areal.v2.inference_service.router.app import ( create_app as create_router_app, ) - from areal.experimental.inference_service.router.config import RouterConfig + from areal.v2.inference_service.router.config import RouterConfig bind_host = "127.0.0.1" data_proxy_port = _find_free_port() diff --git a/tests/experimental/inference_service/test_guard.py b/tests/v2/inference_service/test_guard.py similarity index 97% rename from tests/experimental/inference_service/test_guard.py rename to tests/v2/inference_service/test_guard.py index 8922ba9e54..805a8340ef 100644 --- a/tests/experimental/inference_service/test_guard.py +++ b/tests/v2/inference_service/test_guard.py @@ -1,4 +1,4 @@ -"""Unit tests for RPCGuard Flask app (areal.experimental.inference_service.guard.app). +"""Unit tests for RPCGuard Flask app (areal.v2.inference_service.guard.app). Tests all 4 endpoints (/health, /alloc_ports, /fork, /kill_forked_worker) and the cleanup_forked_children() function using Flask test client with @@ -12,8 +12,8 @@ import pytest -from areal.experimental.inference_service.guard import app as guard_module -from areal.experimental.inference_service.guard.app import app, cleanup_forked_children +from areal.v2.inference_service.guard import app as guard_module +from areal.v2.inference_service.guard.app import app, cleanup_forked_children GUARD_APP = "areal.infra.rpc.guard.app" @@ -133,7 +133,7 @@ def test_fork_raw_cmd_passes_command_as_is(self, mock_run, client): raw = [ "python", "-m", - "areal.experimental.inference_service.sglang.launch_server", + "areal.v2.inference_service.sglang.launch_server", "--model", "test-model", ] diff --git a/tests/experimental/inference_service/test_image_input.py b/tests/v2/inference_service/test_image_input.py similarity index 98% rename from tests/experimental/inference_service/test_image_input.py rename to tests/v2/inference_service/test_image_input.py index 1088c7b6e7..ce0821fc0a 100644 --- a/tests/experimental/inference_service/test_image_input.py +++ b/tests/v2/inference_service/test_image_input.py @@ -19,14 +19,14 @@ from areal.api.cli_args import GenerationHyperparameters from areal.api.io_struct import ModelRequest -from areal.experimental.inference_service.data_proxy.pause import PauseState -from areal.experimental.inference_service.inf_bridge import InfBridge -from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend -from areal.experimental.inference_service.vllm.bridge import VLLMBridgeBackend from areal.experimental.openai.client import ( _build_messages_list, _extract_images_from_messages, ) +from areal.v2.inference_service.data_proxy.pause import PauseState +from areal.v2.inference_service.inf_bridge import InfBridge +from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend +from areal.v2.inference_service.vllm.bridge import VLLMBridgeBackend # --------------------------------------------------------------------------- # Real image fixtures @@ -663,9 +663,9 @@ async def _mock_create(*, areal_cache=None, **kwargs): @pytest_asyncio.fixture async def image_test_client(): """Data proxy test client wired with an image-capturing mock.""" - from areal.experimental.inference_service.data_proxy.app import create_app - from areal.experimental.inference_service.data_proxy.config import DataProxyConfig - from areal.experimental.inference_service.data_proxy.session import SessionStore + from areal.v2.inference_service.data_proxy.app import create_app + from areal.v2.inference_service.data_proxy.config import DataProxyConfig + from areal.v2.inference_service.data_proxy.session import SessionStore config = DataProxyConfig( host="127.0.0.1", diff --git a/tests/experimental/inference_service/test_inf_bridge.py b/tests/v2/inference_service/test_inf_bridge.py similarity index 98% rename from tests/experimental/inference_service/test_inf_bridge.py rename to tests/v2/inference_service/test_inf_bridge.py index 2216b92d1e..aad70d546b 100644 --- a/tests/experimental/inference_service/test_inf_bridge.py +++ b/tests/v2/inference_service/test_inf_bridge.py @@ -10,10 +10,10 @@ from areal.api.cli_args import GenerationHyperparameters from areal.api.io_struct import ModelRequest, ModelResponse, get_versioned_lora_name -from areal.experimental.inference_service.data_proxy.pause import PauseState -from areal.experimental.inference_service.inf_bridge import InfBridge -from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend -from areal.experimental.inference_service.vllm.bridge import VLLMBridgeBackend +from areal.v2.inference_service.data_proxy.pause import PauseState +from areal.v2.inference_service.inf_bridge import InfBridge +from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend +from areal.v2.inference_service.vllm.bridge import VLLMBridgeBackend # ============================================================================= # Helpers diff --git a/tests/experimental/inference_service/test_ipv6_entrypoints.py b/tests/v2/inference_service/test_ipv6_entrypoints.py similarity index 94% rename from tests/experimental/inference_service/test_ipv6_entrypoints.py rename to tests/v2/inference_service/test_ipv6_entrypoints.py index d382042c06..2b7d9b37dd 100644 --- a/tests/experimental/inference_service/test_ipv6_entrypoints.py +++ b/tests/v2/inference_service/test_ipv6_entrypoints.py @@ -5,7 +5,7 @@ def test_data_proxy_main_formats_ipv6_serving_addr(): - from areal.experimental.inference_service.data_proxy import ( + from areal.v2.inference_service.data_proxy import ( __main__ as data_proxy_main, ) @@ -45,7 +45,7 @@ def test_data_proxy_main_formats_ipv6_serving_addr(): def test_guard_main_registers_ipv6_worker_addr(): - from areal.experimental.inference_service.guard import __main__ as guard_main + from areal.v2.inference_service.guard import __main__ as guard_main args = argparse.Namespace( port=0, diff --git a/tests/experimental/inference_service/test_online_stack.py b/tests/v2/inference_service/test_online_stack.py similarity index 92% rename from tests/experimental/inference_service/test_online_stack.py rename to tests/v2/inference_service/test_online_stack.py index c8350c8efd..d39b0c1a5f 100644 --- a/tests/experimental/inference_service/test_online_stack.py +++ b/tests/v2/inference_service/test_online_stack.py @@ -13,21 +13,21 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.data_proxy.app import ( +from areal.v2.inference_service.data_proxy.app import ( create_app as create_dp_app, ) -from areal.experimental.inference_service.data_proxy.config import DataProxyConfig -from areal.experimental.inference_service.data_proxy.session import SessionStore -from areal.experimental.inference_service.gateway.app import create_app as create_gw_app -from areal.experimental.inference_service.gateway.config import GatewayConfig -from areal.experimental.inference_service.gateway.streaming import ( +from areal.v2.inference_service.data_proxy.config import DataProxyConfig +from areal.v2.inference_service.data_proxy.session import SessionStore +from areal.v2.inference_service.gateway.app import create_app as create_gw_app +from areal.v2.inference_service.gateway.config import GatewayConfig +from areal.v2.inference_service.gateway.streaming import ( RouterKeyRejectedError, RouterUnreachableError, ) -from areal.experimental.inference_service.router.app import ( +from areal.v2.inference_service.router.app import ( create_app as create_router_app, ) -from areal.experimental.inference_service.router.config import RouterConfig +from areal.v2.inference_service.router.config import RouterConfig ADMIN_KEY = "areal-admin-key" DATA_PROXY_ADDR = "http://data-proxy" @@ -88,10 +88,10 @@ async def _mock_create(*, areal_cache=None, **kwargs): @pytest_asyncio.fixture async def online_stack(monkeypatch): - import areal.experimental.inference_service.gateway.app as gateway_app_module - from areal.experimental.inference_service.data_proxy.pause import PauseState - from areal.experimental.inference_service.inf_bridge import InfBridge - from areal.experimental.inference_service.sglang.bridge import SGLangBridgeBackend + import areal.v2.inference_service.gateway.app as gateway_app_module + from areal.v2.inference_service.data_proxy.pause import PauseState + from areal.v2.inference_service.inf_bridge import InfBridge + from areal.v2.inference_service.sglang.bridge import SGLangBridgeBackend dp_config = DataProxyConfig( host="127.0.0.1", diff --git a/tests/experimental/inference_service/test_router.py b/tests/v2/inference_service/test_router.py similarity index 98% rename from tests/experimental/inference_service/test_router.py rename to tests/v2/inference_service/test_router.py index e03c35f442..0199b842eb 100644 --- a/tests/experimental/inference_service/test_router.py +++ b/tests/v2/inference_service/test_router.py @@ -10,14 +10,14 @@ import pytest import pytest_asyncio -from areal.experimental.inference_service.router.app import create_app -from areal.experimental.inference_service.router.config import RouterConfig -from areal.experimental.inference_service.router.state import ( +from areal.v2.inference_service.router.app import create_app +from areal.v2.inference_service.router.config import RouterConfig +from areal.v2.inference_service.router.state import ( SessionRegistry, WorkerInfo, WorkerRegistry, ) -from areal.experimental.inference_service.router.strategies import ( +from areal.v2.inference_service.router.strategies import ( RoundRobinStrategy, get_strategy, ) diff --git a/tests/experimental/inference_service/test_sglang_multinode.py b/tests/v2/inference_service/test_sglang_multinode.py similarity index 100% rename from tests/experimental/inference_service/test_sglang_multinode.py rename to tests/v2/inference_service/test_sglang_multinode.py diff --git a/tests/experimental/inference_service/test_vllm_multinode.py b/tests/v2/inference_service/test_vllm_multinode.py similarity index 100% rename from tests/experimental/inference_service/test_vllm_multinode.py rename to tests/v2/inference_service/test_vllm_multinode.py diff --git a/tests/experimental/training_service/__init__.py b/tests/v2/training_service/__init__.py similarity index 100% rename from tests/experimental/training_service/__init__.py rename to tests/v2/training_service/__init__.py diff --git a/tests/experimental/training_service/fake_train_engine.py b/tests/v2/training_service/fake_train_engine.py similarity index 100% rename from tests/experimental/training_service/fake_train_engine.py rename to tests/v2/training_service/fake_train_engine.py diff --git a/tests/experimental/training_service/test_controller_integration.py b/tests/v2/training_service/test_controller_integration.py similarity index 99% rename from tests/experimental/training_service/test_controller_integration.py rename to tests/v2/training_service/test_controller_integration.py index 33b5286778..a6edc605b7 100644 --- a/tests/experimental/training_service/test_controller_integration.py +++ b/tests/v2/training_service/test_controller_integration.py @@ -16,11 +16,11 @@ TrainEngineConfig, ) from areal.api.io_struct import FinetuneSpec, SaveLoadMeta -from areal.experimental.training_service.controller.controller import ( - GatewayTrainController, -) from areal.infra.platforms import current_platform from areal.infra.scheduler.local import LocalScheduler +from areal.v2.training_service.controller.controller import ( + GatewayTrainController, +) LOCAL_MODEL_PATH = "/storage/openpsi/models/Qwen__Qwen3-0.6B/" LOCAL_MOE_MODEL_PATH = "/storage/openpsi/models/Qwen__Qwen3-30B-A3B/" diff --git a/tests/experimental/training_service/test_controller_unit.py b/tests/v2/training_service/test_controller_unit.py similarity index 97% rename from tests/experimental/training_service/test_controller_unit.py rename to tests/v2/training_service/test_controller_unit.py index 4495fd942d..a489e0c613 100644 --- a/tests/experimental/training_service/test_controller_unit.py +++ b/tests/v2/training_service/test_controller_unit.py @@ -6,11 +6,11 @@ import pytest from areal.api.cli_args import SchedulingSpec, TrainEngineConfig -from areal.experimental.training_service.controller.controller import ( +from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) -MODULE = "areal.experimental.training_service.controller.controller" +MODULE = "areal.v2.training_service.controller.controller" def _make_response(method: str, url: str, *, json=None) -> httpx.Response: diff --git a/tests/experimental/training_service/test_data_proxy_unit.py b/tests/v2/training_service/test_data_proxy_unit.py similarity index 98% rename from tests/experimental/training_service/test_data_proxy_unit.py rename to tests/v2/training_service/test_data_proxy_unit.py index 5a209dbdd9..2be179225d 100644 --- a/tests/experimental/training_service/test_data_proxy_unit.py +++ b/tests/v2/training_service/test_data_proxy_unit.py @@ -10,15 +10,15 @@ import pytest_asyncio import torch -from areal.experimental.training_service.data_proxy.app import create_app -from areal.experimental.training_service.data_proxy.config import TrainDataProxyConfig -from areal.experimental.training_service.data_proxy.dispatcher import Dispatcher -from areal.experimental.training_service.data_proxy.topology import ( +from areal.infra.controller.train_controller import _dispatch_tensors +from areal.infra.rpc.serialization import deserialize_value, serialize_value +from areal.v2.training_service.data_proxy.app import create_app +from areal.v2.training_service.data_proxy.config import TrainDataProxyConfig +from areal.v2.training_service.data_proxy.dispatcher import Dispatcher +from areal.v2.training_service.data_proxy.topology import ( WorkerInfo, WorkerTopology, ) -from areal.infra.controller.train_controller import _dispatch_tensors -from areal.infra.rpc.serialization import deserialize_value, serialize_value ADMIN_KEY = "dp-admin-key" diff --git a/tests/experimental/training_service/test_gateway_unit.py b/tests/v2/training_service/test_gateway_unit.py similarity index 97% rename from tests/experimental/training_service/test_gateway_unit.py rename to tests/v2/training_service/test_gateway_unit.py index 84c55dbcc7..0e314e0156 100644 --- a/tests/experimental/training_service/test_gateway_unit.py +++ b/tests/v2/training_service/test_gateway_unit.py @@ -8,16 +8,16 @@ import pytest import pytest_asyncio -from areal.experimental.training_service.gateway.app import create_app -from areal.experimental.training_service.gateway.config import GatewayConfig -from areal.experimental.training_service.gateway.streaming import ( +from areal.v2.training_service.gateway.app import create_app +from areal.v2.training_service.gateway.config import GatewayConfig +from areal.v2.training_service.gateway.streaming import ( RouterKeyRejectedError, RouterUnreachableError, forward_request, query_router, ) -MODULE = "areal.experimental.training_service.gateway.app" +MODULE = "areal.v2.training_service.gateway.app" ADMIN_KEY = "test-admin-key" SESSION_KEY = "session-key" WORKER_ADDR = "http://mock-worker:18082" diff --git a/tests/experimental/training_service/test_router_unit.py b/tests/v2/training_service/test_router_unit.py similarity index 97% rename from tests/experimental/training_service/test_router_unit.py rename to tests/v2/training_service/test_router_unit.py index 2f9bae4295..ad172d465d 100644 --- a/tests/experimental/training_service/test_router_unit.py +++ b/tests/v2/training_service/test_router_unit.py @@ -8,11 +8,11 @@ import pytest import pytest_asyncio -from areal.experimental.training_service.router.app import ( +from areal.v2.training_service.router.app import ( _probe_model_health, create_app, ) -from areal.experimental.training_service.router.config import RouterConfig +from areal.v2.training_service.router.config import RouterConfig ADMIN_KEY = "router-admin-key" diff --git a/tests/experimental/training_service/test_worker_unit.py b/tests/v2/training_service/test_worker_unit.py similarity index 87% rename from tests/experimental/training_service/test_worker_unit.py rename to tests/v2/training_service/test_worker_unit.py index 8d3c71e7aa..69b080625c 100644 --- a/tests/experimental/training_service/test_worker_unit.py +++ b/tests/v2/training_service/test_worker_unit.py @@ -6,15 +6,15 @@ import pytest -from areal.experimental.training_service.worker.config import TrainWorkerConfig from areal.infra.rpc.serialization import deserialize_value, serialize_value +from areal.v2.training_service.worker.config import TrainWorkerConfig -MODULE = "areal.experimental.training_service.worker.app" +MODULE = "areal.v2.training_service.worker.app" @pytest.fixture(autouse=True) def reset_worker_state(): - import areal.experimental.training_service.worker.app as worker_app + import areal.v2.training_service.worker.app as worker_app if worker_app._engine_work_queue is not None: worker_app._engine_work_queue.put(None) @@ -41,7 +41,7 @@ def reset_worker_state(): @pytest.fixture def client(): - from areal.experimental.training_service.worker.app import create_app + from areal.v2.training_service.worker.app import create_app app = create_app( TrainWorkerConfig( @@ -66,7 +66,7 @@ def test_create_engine_success(self, client): resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, @@ -86,7 +86,7 @@ def test_train_batch_after_create_engine(self, client): create_resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, @@ -111,7 +111,7 @@ def test_topology_after_create_engine(self, client): create_resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, @@ -134,7 +134,7 @@ def test_ppo_endpoints_return_400_when_engine_method_missing(self, client): create_resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, @@ -162,7 +162,7 @@ def test_forward_batch_after_initialize_succeeds_without_distributed_group( create_resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, @@ -198,7 +198,7 @@ def test_sft_route_succeeds_without_distributed_group_for_single_worker( create_resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, @@ -221,7 +221,7 @@ def test_sft_route_ignores_rpc_meta_override_for_single_worker(self, client): create_resp = client.post( "/create_engine", json={ - "engine_class": "tests.experimental.training_service.fake_train_engine.FakeTrainEngine", + "engine_class": "tests.v2.training_service.fake_train_engine.FakeTrainEngine", "init_args": serialize_value([]), "init_kwargs": serialize_value({"world_size": 1}), }, diff --git a/tests/experimental/weight_update/torchrun/__init__.py b/tests/v2/weight_update/__init__.py similarity index 100% rename from tests/experimental/weight_update/torchrun/__init__.py rename to tests/v2/weight_update/__init__.py diff --git a/tests/experimental/weight_update/conftest.py b/tests/v2/weight_update/conftest.py similarity index 86% rename from tests/experimental/weight_update/conftest.py rename to tests/v2/weight_update/conftest.py index 56ed08be50..0223d78345 100644 --- a/tests/experimental/weight_update/conftest.py +++ b/tests/v2/weight_update/conftest.py @@ -3,7 +3,7 @@ The top-level ``areal/__init__.py`` imports from modules that use PEP 695 syntax (``def func[T](...)``) which is only valid on Python 3.12+. When running tests on Python 3.10/3.11 we register lightweight stubs for the ``areal`` namespace packages -so that importing ``areal.experimental.weight_update.*`` never triggers the +so that importing ``areal.v2.weight_update.*`` never triggers the problematic top-level init. """ @@ -18,9 +18,10 @@ _STUB_PACKAGES = [ ("areal", os.path.join(_REPO_ROOT, "areal")), ("areal.experimental", os.path.join(_REPO_ROOT, "areal", "experimental")), + ("areal.v2", os.path.join(_REPO_ROOT, "areal", "v2")), ( - "areal.experimental.weight_update", - os.path.join(_REPO_ROOT, "areal", "experimental", "weight_update"), + "areal.v2.weight_update", + os.path.join(_REPO_ROOT, "areal", "v2", "weight_update"), ), ] diff --git a/tests/experimental/weight_update/test_disk_integration.py b/tests/v2/weight_update/test_disk_integration.py similarity index 96% rename from tests/experimental/weight_update/test_disk_integration.py rename to tests/v2/weight_update/test_disk_integration.py index 7f400526f6..b5b7f2d225 100644 --- a/tests/experimental/weight_update/test_disk_integration.py +++ b/tests/v2/weight_update/test_disk_integration.py @@ -9,18 +9,18 @@ import pytest import torch -from tests.experimental.weight_update.test_nccl_integration import ( +from tests.v2.weight_update.test_nccl_integration import ( _get_test_model_path, _make_local_scheduler, _validate_weight_update_correctness, ) -from areal.experimental.weight_update.gateway.app import create_app -from areal.experimental.weight_update.gateway.config import ( +from areal.infra.platforms import current_platform +from areal.v2.weight_update.gateway.app import create_app +from areal.v2.weight_update.gateway.config import ( PairInfo, WeightUpdateConfig, ) -from areal.infra.platforms import current_platform @pytest.fixture() @@ -308,13 +308,13 @@ def test_disk_e2e_weight_update(n_gpus, tmp_path_factory): SchedulingSpec, TrainEngineConfig, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.training_service.controller.controller import ( + from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) - from areal.experimental.weight_update.controller import ( + from areal.v2.weight_update.controller import ( WeightUpdateController, WeightUpdateControllerConfig, ) @@ -332,7 +332,7 @@ def test_disk_e2e_weight_update(n_gpus, tmp_path_factory): scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), consumer_batch_size=8, @@ -353,7 +353,7 @@ def test_disk_e2e_weight_update(n_gpus, tmp_path_factory): scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.training_service.guard", + cmd="python -m areal.v2.training_service.guard", env_vars=dict(NCCL_CUMEM_ENABLE="0", NCCL_NVLS_ENABLE="0"), ), ), diff --git a/tests/experimental/weight_update/test_kv_store.py b/tests/v2/weight_update/test_kv_store.py similarity index 97% rename from tests/experimental/weight_update/test_kv_store.py rename to tests/v2/weight_update/test_kv_store.py index 1e1f1576f8..f3f61b4ac4 100644 --- a/tests/experimental/weight_update/test_kv_store.py +++ b/tests/v2/weight_update/test_kv_store.py @@ -5,7 +5,7 @@ import pytest -from areal.experimental.weight_update.gateway.kv_store import WeightMetaStore +from areal.v2.weight_update.gateway.kv_store import WeightMetaStore @pytest.fixture() diff --git a/tests/experimental/weight_update/test_nccl_integration.py b/tests/v2/weight_update/test_nccl_integration.py similarity index 96% rename from tests/experimental/weight_update/test_nccl_integration.py rename to tests/v2/weight_update/test_nccl_integration.py index 8b9751b9fb..481df2c2bb 100644 --- a/tests/experimental/weight_update/test_nccl_integration.py +++ b/tests/v2/weight_update/test_nccl_integration.py @@ -34,7 +34,7 @@ def _run_weight_update_test(n_gpus: int, test_type: str, output: str): "--nnodes=1", "--master-addr=localhost", f"--master_port={port}", - "tests/experimental/weight_update/torchrun/run_nccl_weight_transfer.py", + "tests/v2/weight_update/torchrun/run_nccl_weight_transfer.py", f"--test_type={test_type}", f"--output={output}", ], @@ -374,13 +374,13 @@ def test_awex_fsdp_e2e_weight_update(n_gpus, tmp_path_factory): SchedulingSpec, TrainEngineConfig, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.training_service.controller.controller import ( + from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) - from areal.experimental.weight_update.controller import ( + from areal.v2.weight_update.controller import ( WeightUpdateController, WeightUpdateControllerConfig, ) @@ -397,7 +397,7 @@ def test_awex_fsdp_e2e_weight_update(n_gpus, tmp_path_factory): scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), consumer_batch_size=8, @@ -418,7 +418,7 @@ def test_awex_fsdp_e2e_weight_update(n_gpus, tmp_path_factory): scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.training_service.guard", + cmd="python -m areal.v2.training_service.guard", env_vars=dict(NCCL_CUMEM_ENABLE="0", NCCL_NVLS_ENABLE="0"), ), ), @@ -577,13 +577,13 @@ def _run_megatron_awex_e2e( SchedulingSpec, TrainEngineConfig, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.training_service.controller.controller import ( + from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) - from areal.experimental.weight_update.controller import ( + from areal.v2.weight_update.controller import ( WeightUpdateController, WeightUpdateControllerConfig, ) @@ -599,7 +599,7 @@ def _run_megatron_awex_e2e( scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), consumer_batch_size=8, @@ -621,7 +621,7 @@ def _run_megatron_awex_e2e( scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.training_service.guard", + cmd="python -m areal.v2.training_service.guard", env_vars=dict(NCCL_CUMEM_ENABLE="0", NCCL_NVLS_ENABLE="0"), ), ), @@ -923,13 +923,13 @@ def _run_megatron_colocate_e2e( SchedulingSpec, TrainEngineConfig, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.training_service.controller.controller import ( + from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) - from areal.experimental.weight_update.controller import ( + from areal.v2.weight_update.controller import ( WeightUpdateController, WeightUpdateControllerConfig, ) @@ -945,7 +945,7 @@ def _run_megatron_colocate_e2e( scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), consumer_batch_size=8, @@ -966,7 +966,7 @@ def _run_megatron_colocate_e2e( scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.training_service.guard", + cmd="python -m areal.v2.training_service.guard", env_vars=dict(NCCL_CUMEM_ENABLE="0", NCCL_NVLS_ENABLE="0"), ), ), @@ -1093,13 +1093,13 @@ def test_awex_megatron_colocate_dp_multi_version_e2e(tmp_path_factory): SchedulingSpec, TrainEngineConfig, ) - from areal.experimental.inference_service.controller.controller import ( + from areal.v2.inference_service.controller.controller import ( RolloutControllerV2, ) - from areal.experimental.training_service.controller.controller import ( + from areal.v2.training_service.controller.controller import ( GatewayTrainController, ) - from areal.experimental.weight_update.controller import ( + from areal.v2.weight_update.controller import ( WeightUpdateController, WeightUpdateControllerConfig, ) @@ -1115,7 +1115,7 @@ def test_awex_megatron_colocate_dp_multi_version_e2e(tmp_path_factory): scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.inference_service.guard", + cmd="python -m areal.v2.inference_service.guard", ), ), consumer_batch_size=8, @@ -1136,7 +1136,7 @@ def test_awex_megatron_colocate_dp_multi_version_e2e(tmp_path_factory): scheduling_spec=( SchedulingSpec( gpu=1, - cmd="python -m areal.experimental.training_service.guard", + cmd="python -m areal.v2.training_service.guard", env_vars=dict(NCCL_CUMEM_ENABLE="0", NCCL_NVLS_ENABLE="0"), ), ), diff --git a/tests/experimental/weight_update/test_pair_registry.py b/tests/v2/weight_update/test_pair_registry.py similarity index 94% rename from tests/experimental/weight_update/test_pair_registry.py rename to tests/v2/weight_update/test_pair_registry.py index 89b9009250..dc4fd56ff5 100644 --- a/tests/experimental/weight_update/test_pair_registry.py +++ b/tests/v2/weight_update/test_pair_registry.py @@ -3,8 +3,8 @@ import pytest -from areal.experimental.weight_update.gateway.config import PairInfo -from areal.experimental.weight_update.gateway.pair_registry import PairRegistry +from areal.v2.weight_update.gateway.config import PairInfo +from areal.v2.weight_update.gateway.pair_registry import PairRegistry @pytest.fixture diff --git a/tests/experimental/weight_update/test_sglang_integration.py b/tests/v2/weight_update/test_sglang_integration.py similarity index 96% rename from tests/experimental/weight_update/test_sglang_integration.py rename to tests/v2/weight_update/test_sglang_integration.py index 0cf1b19e92..e5851950a3 100644 --- a/tests/experimental/weight_update/test_sglang_integration.py +++ b/tests/v2/weight_update/test_sglang_integration.py @@ -4,7 +4,7 @@ Requires GPU. Marked @pytest.mark.slow and @pytest.mark.sglang to exclude from default CI. Run manually: - uv run pytest tests/experimental/weight_update/test_sglang_server_integration.py -v -s + uv run pytest tests/v2/weight_update/test_sglang_server_integration.py -v -s """ from __future__ import annotations @@ -52,7 +52,7 @@ def sglang_server(): [ "python", "-m", - "areal.experimental.inference_service.sglang.launch_server", + "areal.v2.inference_service.sglang.launch_server", "--model-path", model_path, "--port", diff --git a/tests/experimental/weight_update/test_wu_controller.py b/tests/v2/weight_update/test_wu_controller.py similarity index 96% rename from tests/experimental/weight_update/test_wu_controller.py rename to tests/v2/weight_update/test_wu_controller.py index f1aca5531e..ba029d3bf2 100644 --- a/tests/experimental/weight_update/test_wu_controller.py +++ b/tests/v2/weight_update/test_wu_controller.py @@ -7,13 +7,13 @@ import pytest import requests -from areal.experimental.weight_update.controller.config import ( +from areal.v2.weight_update.controller.config import ( WeightUpdateControllerConfig, ) -from areal.experimental.weight_update.controller.controller import ( +from areal.v2.weight_update.controller.controller import ( WeightUpdateController, ) -from areal.experimental.weight_update.gateway.config import WeightUpdateResult +from areal.v2.weight_update.gateway.config import WeightUpdateResult GATEWAY_URL = "http://localhost:7080" diff --git a/tests/v2/weight_update/torchrun/__init__.py b/tests/v2/weight_update/torchrun/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/tests/v2/weight_update/torchrun/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/experimental/weight_update/torchrun/dist_utils.py b/tests/v2/weight_update/torchrun/dist_utils.py similarity index 100% rename from tests/experimental/weight_update/torchrun/dist_utils.py rename to tests/v2/weight_update/torchrun/dist_utils.py diff --git a/tests/experimental/weight_update/torchrun/run_nccl_weight_transfer.py b/tests/v2/weight_update/torchrun/run_nccl_weight_transfer.py similarity index 98% rename from tests/experimental/weight_update/torchrun/run_nccl_weight_transfer.py rename to tests/v2/weight_update/torchrun/run_nccl_weight_transfer.py index 7931988f27..abfcab0336 100644 --- a/tests/experimental/weight_update/torchrun/run_nccl_weight_transfer.py +++ b/tests/v2/weight_update/torchrun/run_nccl_weight_transfer.py @@ -7,16 +7,16 @@ import torch import torch.distributed as dist -from tests.experimental.weight_update.torchrun.dist_utils import ( +from tests.v2.weight_update.torchrun.dist_utils import ( print_rank0, write_result, ) -from areal.experimental.weight_update.nccl_group import ( +from areal.infra.platforms import current_platform +from areal.v2.weight_update.nccl_group import ( init_weights_update_group, setup_batch_isend_irecv, ) -from areal.infra.platforms import current_platform def run_nccl_group_init(output=None): From 62512eb93ff483d66ad417857072f5cb0211c033 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:45:20 +0800 Subject: [PATCH 15/36] feat: support v2 weight update disk mode for lora RL (#1450) Support LoRA RL training under actor._version=v2 / rollout._version=v2 --- areal/trainer/rl_trainer.py | 34 ++++++---- .../training_service/controller/controller.py | 65 ++++++++++++------- .../v2/weight_update/controller/controller.py | 8 ++- areal/v2/weight_update/gateway/app.py | 54 ++++++++++++++- areal/v2/weight_update/gateway/config.py | 4 ++ 5 files changed, 127 insertions(+), 38 deletions(-) diff --git a/areal/trainer/rl_trainer.py b/areal/trainer/rl_trainer.py index 1dce676809..ef89734813 100644 --- a/areal/trainer/rl_trainer.py +++ b/areal/trainer/rl_trainer.py @@ -323,18 +323,30 @@ def __init__( # Proxy worker initialization (lazy, for AgentWorkflow support) self._proxy_started = False - # Prepare weight update meta and connect to inference engine + # Prepare weight update meta and connect to inference engine. + # v2 controllers pick transport from use_lora: LoRA must go through + # disk (P2P transports cannot carry PEFT-wrapped tensors); non-LoRA + # uses awex. v1 keeps the legacy weight_update_mode dispatch. if self.config.actor._version == "v2": - awex_kwargs: dict[str, Any] = {} if config.actor.use_lora: - awex_kwargs.update( - { - "use_lora": config.actor.use_lora, - "lora_name": config.gconfig.lora_name, - "base_model_name": config.actor.path, - } - ) - self.weight_update_meta = WeightUpdateMeta.from_awex(**awex_kwargs) + disk_kwargs: dict[str, Any] = { + "experiment_name": config.experiment_name, + "trial_name": config.trial_name, + "file_root": config.cluster.fileroot, + "name": "default", + "clear_checkpoint_after_load": True, + "use_lora": config.actor.use_lora, + "lora_name": config.gconfig.lora_name, + "base_model_name": config.actor.path, + # Keep enough recent adapter versions for off-policy + # rollouts (max_head_offpolicyness) plus a safety margin; + # older versions are unloaded to bound sglang VRAM and + # avoid the adapter-accumulation hang. + "lora_keep_versions": config.rollout.max_head_offpolicyness + 2, + } + self.weight_update_meta = WeightUpdateMeta.from_disk(**disk_kwargs) + else: + self.weight_update_meta = WeightUpdateMeta.from_awex() elif self.config.actor.weight_update_mode == "disk": disk_kwargs = { "experiment_name": config.experiment_name, @@ -358,7 +370,7 @@ def __init__( ) self.weight_update_meta = WeightUpdateMeta.from_disk(**disk_kwargs) elif self.config.actor.weight_update_mode == "xccl": - # NCCL/XCCL weight update + # NCCL/XCCL weight update (v1 only) xccl_kwargs: dict[str, Any] = { "gen_allocation": self.rollout_alloc, } diff --git a/areal/v2/training_service/controller/controller.py b/areal/v2/training_service/controller/controller.py index 613153b88b..16f136095e 100644 --- a/areal/v2/training_service/controller/controller.py +++ b/areal/v2/training_service/controller/controller.py @@ -17,7 +17,7 @@ from areal.infra.utils.concurrent import get_executor, run_async_task from areal.infra.utils.http import create_httpx_client from areal.utils import logging -from areal.utils.network import format_hostport +from areal.utils.network import format_hostport, gethostip if TYPE_CHECKING: from areal.api import ParallelStrategy, TrainEngine @@ -955,13 +955,18 @@ def connect_engine(self, rollout: Any, meta: Any) -> None: self.rollout = rollout - if meta.type != "awex": + if meta.type not in ("awex", "disk"): raise ValueError( - f"GatewayTrainController only supports 'awex' weight updates, got '{meta.type}'" + f"GatewayTrainController supports 'awex' or 'disk' weight " + f"updates, got '{meta.type}'" ) ctrl = WeightUpdateController( WeightUpdateControllerConfig( + # Bind gateway to this node's outbound IP so cross-host + # train/inf workers can reach the kv_store_url the gateway + # publishes. Default "127.0.0.1" only works single-machine. + host=gethostip(), admin_api_key=self.config.admin_api_key, log_level=self.config.log_level, ) @@ -969,29 +974,39 @@ def connect_engine(self, rollout: Any, meta: Any) -> None: ctrl.initialize() inference_urls: list[str] = rollout.inference_worker_urls - - # NCCL rendezvous master must live on the rank-0 process's node. - # awex assigns rank 0 to inference[0], so allocate on the inference - # rank-0 guard rather than a train guard. - inf_guard_addrs = rollout.inference_guard_addrs - resp = requests.post( - f"{inf_guard_addrs[0]}/alloc_ports", - json={"count": 1}, - timeout=30, - ) - resp.raise_for_status() - port_data = resp.json() - nccl_master_addr = port_data["host"] - nccl_master_port = port_data["ports"][0] - pair_name = f"{self._role}-rollout" - ctrl.connect( - pair_name=pair_name, - train_worker_urls=self._worker_addrs, - inference_worker_urls=inference_urls, - nccl_master_addr=nccl_master_addr, - nccl_master_port=nccl_master_port, - ) + + if meta.type == "awex": + # NCCL rendezvous master must live on the rank-0 process's node. + # awex assigns rank 0 to inference[0], so allocate on the inference + # rank-0 guard rather than a train guard. + inf_guard_addrs = rollout.inference_guard_addrs + resp = requests.post( + f"{inf_guard_addrs[0]}/alloc_ports", + json={"count": 1}, + timeout=30, + ) + resp.raise_for_status() + port_data = resp.json() + ctrl.connect( + pair_name=pair_name, + train_worker_urls=self._worker_addrs, + inference_worker_urls=inference_urls, + mode="awex", + nccl_master_addr=port_data["host"], + nccl_master_port=port_data["ports"][0], + ) + else: # disk + ctrl.connect( + pair_name=pair_name, + train_worker_urls=self._worker_addrs, + inference_worker_urls=inference_urls, + mode="disk", + save_path=meta.path or "", + use_lora=meta.use_lora, + lora_name=meta.lora_name, + lora_keep_versions=meta.lora_keep_versions, + ) self._weight_update_ctrl = ctrl logger.info( "WeightUpdateController connected (pair=%s, train=%d, inf=%d)", diff --git a/areal/v2/weight_update/controller/controller.py b/areal/v2/weight_update/controller/controller.py index c880cac129..457cc5d273 100644 --- a/areal/v2/weight_update/controller/controller.py +++ b/areal/v2/weight_update/controller/controller.py @@ -112,6 +112,7 @@ def connect( save_path: str = "", use_lora: bool = False, lora_name: str = "", + lora_keep_versions: int = 0, colocate: bool = False, nccl_master_addr: str = "", nccl_master_port: int = 0, @@ -125,6 +126,7 @@ def connect( "save_path": save_path, "use_lora": use_lora, "lora_name": lora_name, + "lora_keep_versions": lora_keep_versions, "colocate": colocate, "nccl_master_addr": nccl_master_addr, "nccl_master_port": nccl_master_port, @@ -136,7 +138,11 @@ def connect( ) resp.raise_for_status() logger.info( - "Connected pair '%s' (mode=%s, colocate=%s)", pair_name, mode, colocate + "Connected pair '%s' (mode=%s, colocate=%s, use_lora=%s)", + pair_name, + mode, + colocate, + use_lora, ) def update_weights(self, version: int) -> WeightUpdateResult: diff --git a/areal/v2/weight_update/gateway/app.py b/areal/v2/weight_update/gateway/app.py index d37392cbca..a381b8211c 100644 --- a/areal/v2/weight_update/gateway/app.py +++ b/areal/v2/weight_update/gateway/app.py @@ -38,6 +38,7 @@ class ConnectRequest(BaseModel): save_path: str = "" use_lora: bool = False lora_name: str = "" + lora_keep_versions: int = 0 colocate: bool = False @@ -229,13 +230,35 @@ async def connect(request: Request, body: ConnectRequest) -> ConnectResponse: save_path=body.save_path, use_lora=body.use_lora, lora_name=body.lora_name, + lora_keep_versions=body.lora_keep_versions, ) registry.register(pair_info) logger.info( - "Connected disk pair '%s' (save_path=%s)", pair_name, body.save_path + "Connected disk pair '%s' (save_path=%s, use_lora=%s, " + "lora_keep_versions=%d)", + pair_name, + body.save_path, + body.use_lora, + body.lora_keep_versions, ) return ConnectResponse(pair_name=pair_name) + # awex mode -- LoRA is unsupported because the NCCL P2P transfer plan + # assumes train/infer parameter names match the HF layout, but PEFT + # exposes ``base_model.model.*.{base_layer,lora_A,lora_B}.weight`` on + # the train side. Fail fast with an actionable error rather than + # bubbling up a cryptic TransferPlanBuilder key-mismatch at init. + if body.use_lora: + return JSONResponse( + status_code=400, + content={ + "error": ( + "awex weight update does not support LoRA; set " + "actor.weight_update_mode=disk in your config." + ) + }, + ) + session = request.app.state.http_session init_timeout_s = config.init_timeout_s @@ -633,6 +656,35 @@ async def _disk_transfer_weights( for url in pair_info.inference_worker_urls ] ) + # Unload the version that fell outside the retention window so + # sglang does not accumulate one adapter per train step (leaks + # VRAM and eventually hangs). Best-effort: the stale adapter may + # already have been evicted or never loaded on this worker. + keep = pair_info.lora_keep_versions + if keep > 0 and version - keep >= 0: + stale_name = get_versioned_lora_name( + pair_info.lora_name, version - keep + ) + + async def _unload(url: str) -> None: + try: + await _post_json( + session, + f"{url}/unload_lora_adapter", + timeout_s, + json_data={"lora_name": stale_name}, + ) + except Exception as e: + logger.warning( + "unload_lora_adapter(%s) on %s failed (best-effort): %s", + stale_name, + url, + e, + ) + + await asyncio.gather( + *[_unload(url) for url in pair_info.inference_worker_urls] + ) else: await asyncio.gather( *[ diff --git a/areal/v2/weight_update/gateway/config.py b/areal/v2/weight_update/gateway/config.py index db564d8c77..34c40b4f59 100644 --- a/areal/v2/weight_update/gateway/config.py +++ b/areal/v2/weight_update/gateway/config.py @@ -59,6 +59,10 @@ class PairInfo: save_path: str = "" use_lora: bool = False lora_name: str = "" + # Number of recent LoRA adapter versions to keep loaded on each inference + # worker. Older versions are unloaded after a successful load_lora_adapter + # to bound VRAM and avoid the adapter-accumulation hang. 0 disables cleanup. + lora_keep_versions: int = 0 # Colocated mode (training and inference share GPUs) colocate: bool = False From 2824f7208006f30a234ef90fa83f3fbdb6c09c32 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:46:38 +0800 Subject: [PATCH 16/36] feat(cli): add training service cli (#1446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: integrate CLI shared files Co-Authored-By: Claude Opus 4.7 * feat(cli): training experiment launcher — `areal train run` Wraps a user-supplied driver entry point (`module.path:func`) into a uniform CLI invocation driven by yaml + hydra overrides. Same lifecycle as `python -m examples.math.gsm8k_rl ...` — the CLI process is the driver process, attached, all the way down. Single verb, permanently. No ps / stop / status / logs: a training job is a process, not a service; OS tooling (`ps`, `kill`) and the cluster scheduler (`squeue`, `kubectl get`) already cover that, and logs live at `{fileroot}/logs/...` for `tail -F`. Service-shaped verbs belong with `areal inf`, whose state is daemon-backed and invisible to OS tooling. Usage: areal train run --config experiments/grpo.yaml \ --driver examples.math.gsm8k_rl:main \ actor.lr=1e-5 trial_name=lr-sweep-3 +actor._version=v2 `--driver MOD:FN` is required; we deliberately don't peek a `driver:` field out of the yaml — keeps the contract obvious and avoids parsing yaml in the CLI layer. Anything after the flags is forwarded verbatim as a hydra override to the driver (click's `nargs=-1 + UNPROCESSED + ignore_unknown_options`). Backgrounding is out of scope: users wrap the command in nohup/tmux/sbatch as appropriate. Adding `--detach` would force state files, heartbeat, pid tracking — none of which anything else here needs. Conflicts with `feat/inference-service-cli` are isolated to main.py + pyproject.toml's [cli] extra; trivial 3-way merge after the inf branch lands. * refactor(experimental): split training CLI commands Move the training CLI entrypoint into a root CLI module and a training command package so future training commands can be added without growing a monolithic command file. * docs(cli/train): add training service CLI guide Document the `areal train` subcommand group: basic concepts, the driver function contract, hydra override conventions, and exit code behaviour. Co-Authored-By: Claude Opus 4.7 * chore(experimental): rebase train CLI onto cli scaffold v2 Train doesn't carry a service / daemon / state, so the scaffold's ServiceLifecycle / BaseHTTPClient / StatusReporter components don't apply — train run is a synchronous importlib dispatcher. The change here is just plumbing alignment: - Rebase onto feat/experimental-cli-scaffold; scaffold now owns cli/__main__.py / cli/cli.py. - Drop the redundant cli/main.py shim (cli/__main__.py covers it). - Point [project.scripts] areal entry at cli.cli:cli in both pyproject manifests, matching the agent / inf branches. - Re-flow training/cli_guide.md through mdformat. * docs(cli/train): drop the evaluation example from the guide * docs(cli/train): drop the BOBA-GRPO example from the guide * fix(cli): resolve config merge leftovers --------- Co-authored-by: Claude Opus 4.7 --- areal/v2/cli/cli.py | 5 +- areal/v2/cli/training/__init__.py | 15 +++ areal/v2/cli/training/cli_guide.md | 139 +++++++++++++++++++++ areal/v2/cli/training/commands/__init__.py | 1 + areal/v2/cli/training/commands/run.py | 57 +++++++++ 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 areal/v2/cli/training/__init__.py create mode 100644 areal/v2/cli/training/cli_guide.md create mode 100644 areal/v2/cli/training/commands/__init__.py create mode 100644 areal/v2/cli/training/commands/run.py diff --git a/areal/v2/cli/cli.py b/areal/v2/cli/cli.py index 44373c542b..65e41c09fd 100644 --- a/areal/v2/cli/cli.py +++ b/areal/v2/cli/cli.py @@ -4,6 +4,7 @@ import click +from areal.v2.cli.training import train from areal.version import __version__ @@ -16,6 +17,4 @@ def cli() -> None: pass -# Subcommand groups (inf / train / agent / ...) attach themselves to ``cli`` -# from their own modules — see e.g. ``areal.v2.cli.inference``, -# ``areal.v2.cli.training``, ``areal.v2.cli.agent``. +cli.add_command(train) diff --git a/areal/v2/cli/training/__init__.py b/areal/v2/cli/training/__init__.py new file mode 100644 index 0000000000..1d392ca050 --- /dev/null +++ b/areal/v2/cli/training/__init__.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.training.commands.run import run_cmd + + +@click.group(help="Run AReaL training experiments.") +def train() -> None: + pass + + +train.add_command(run_cmd) diff --git a/areal/v2/cli/training/cli_guide.md b/areal/v2/cli/training/cli_guide.md new file mode 100644 index 0000000000..3455da8704 --- /dev/null +++ b/areal/v2/cli/training/cli_guide.md @@ -0,0 +1,139 @@ +# AReaL Training Service CLI + +`areal train` is the training subcommand group under the top-level `areal` CLI. It wires +up an AReaL training driver function and an experiment config file into a single +command. It does not manage the training process lifecycle (unlike `areal inf`, which +maintains service state); it only "finds the driver, loads the config, and passes +hydra-style overrides through." + +## Basic concepts + +The minimum execution unit of a training job is a **driver function** —typically a +`main(args: list[str])` in some script under `examples/`. The CLI does exactly three +things: + +1. Resolve the driver from `module.path:func` +1. Resolve `--config ` to an absolute path and prepend it to argv +1. Append every trailing argument unchanged to argv (typically hydra overrides) + +The driver function's return value is used as the exit code if it returns `int`; +anything else (including `None`) is treated as 0. + +## Usage + +```bash +areal train run \ + --config \ + --driver : \ + [ ...] +``` + +| flag / arg | required | description | +| ------------------------ | -------- | ----------------------------------------------------------------------------- | +| `--config` | yes | Experiment YAML path; the file must exist (the CLI runs an `exists` check) | +| `--driver` | yes | Driver entry point in `module.path:func` form (colon-separated) | +| trailing positional args | no | Forwarded to the driver verbatim; typically hydra-style `key=value` overrides | + +Note that `run_cmd` is configured with +`context_settings={"ignore_unknown_options": True}` — this means trailing positional +args **can include `--xxx` flags**; the CLI does not try to parse them and forwards them +as-is to the driver. + +## Examples + +Run GSM8K GRPO (the most common baseline): + +```bash +areal train run \ + --config examples/math/gsm8k_grpo.yaml \ + --driver examples.math.gsm8k_rl:main \ + experiment_name=gsm8k_grpo_test \ + trial_name=t1 +``` + +Run SFT: + +```bash +areal train run \ + --config examples/math/gsm8k_sft.yaml \ + --driver examples.math.gsm8k_sft:main +``` + +## Driver function conventions + +The CLI calls the driver with a single argument `argv: list[str]`, so the driver must +look like: + +```python +def main(args: list[str]) -> int | None: + config, _ = load_expr_config(args, GRPOConfig) # or any other *Config dataclass + ... + return 0 +``` + +`load_expr_config` lives in `areal.api.cli_args` and consumes `args` itself: it +recognises the YAML path after `--config`, and treats every remaining `key=value` as a +hydra override merged into the config dataclass. In other words, hydra parsing is **done +by the driver**, not the CLI. + +Minimum template for writing a new driver: + +```python +from areal.api.cli_args import GRPOConfig, load_expr_config +from areal import PPOTrainer + +def main(args): + config, _ = load_expr_config(args, GRPOConfig) + with PPOTrainer(config, train_dataset=..., valid_dataset=...) as trainer: + trainer.train(workflow="...", workflow_kwargs={...}) + return 0 +``` + +## Hydra overrides + +Any driver that uses `load_expr_config` to parse args supports hydra-style overrides. +Common override targets: + +```bash +# experiment / trial naming +experiment_name=my_run trial_name=t1 + +# cluster size +cluster.n_nodes=4 cluster.n_gpus_per_node=8 + +# training hyperparameters +actor.optimizer.lr=5e-6 +total_train_epochs=20 + +# rollout backend +rollout.backend=sglang:d2p1t2 +rollout.max_concurrent_rollouts=128 + +# datasets +train_dataset.batch_size=256 +``` + +The CLI does not validate whether these keys are legal; unknown fields will be reported +by hydra when the driver loads the config. + +## Exit codes + +| Scenario | exit code | +| ------------------------------------------------------ | ----------------------------------------- | +| Driver returns `int` | Returned value used directly as exit code | +| Driver returns `None` / other | 0 | +| `--driver` does not contain `:` | UsageError (click default 2) | +| Module referenced by `--driver` cannot be imported | ClickException (1) | +| Function referenced by `--driver` is not on the module | ClickException (1) | +| `--config` path does not exist | Caught by click `exists=True` (2) | + +Exceptions raised inside the driver **are not caught by the CLI** — the default Python +behaviour applies (traceback printed, process exits). + +## Not implemented yet + +`areal train` currently only implements `run`. The following are reasonable future +extensions but are **not** in this version: + +- `areal train ps` / `status` / `stop` — lifecycle management for training jobs + (requires a training service state concept first) diff --git a/areal/v2/cli/training/commands/__init__.py b/areal/v2/cli/training/commands/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/areal/v2/cli/training/commands/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/areal/v2/cli/training/commands/run.py b/areal/v2/cli/training/commands/run.py new file mode 100644 index 0000000000..104e5df72b --- /dev/null +++ b/areal/v2/cli/training/commands/run.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +from pathlib import Path + +import click + + +@click.command( + name="run", + help="Invoke a training driver with the given config and hydra overrides.", + context_settings={"ignore_unknown_options": True}, +) +@click.option( + "--config", + "config_path", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + required=True, + help="Path to the experiment yaml.", +) +@click.option( + "--driver", + "driver_spec", + required=True, + help="Driver entry point as 'module.path:func', e.g. examples.math.gsm8k_rl:main.", +) +@click.argument("overrides", nargs=-1, type=click.UNPROCESSED) +def run_cmd(config_path: Path, driver_spec: str, overrides: tuple[str, ...]) -> None: + raise SystemExit(do_run(config_path, driver_spec, list(overrides)) or 0) + + +def do_run(config_path: Path, driver_spec: str, overrides: list[str]) -> int: + if ":" not in driver_spec: + raise click.UsageError( + f"--driver must be in 'module.path:func' form, got: {driver_spec!r}" + ) + mod_path, func_name = driver_spec.split(":", 1) + try: + module = importlib.import_module(mod_path) + except ImportError as e: + raise click.ClickException( + f"failed to import driver module {mod_path!r}: {e}" + ) from e + try: + fn = getattr(module, func_name) + except AttributeError as e: + raise click.ClickException( + f"module {mod_path!r} has no attribute {func_name!r}" + ) from e + + argv = ["--config", str(config_path.resolve()), *overrides] + result = fn(argv) + if isinstance(result, int): + return result + return 0 From 0ca30f19b6edd78167e865a8578a193afb329bde Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:29:19 +0800 Subject: [PATCH 17/36] feat(cli): add agent service cli (#1447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): add experimental cli scaffold for service-style subcommands Prepares the ground for feat/inference-service-cli, feat/training-service-cli, and feat/agent-service-cli to land on top — each subcommand PR only adds its own click group and one cli.add_command call, instead of duplicating the same plumbing. Key pieces: - empty top-level `cli` click group with version + --help wiring; subcommand modules attach themselves via cli.add_command(...) - `state.areal_home()` + `atomic_write_json()` for AREAL_HOME-rooted local state files (atomic .tmp + os.replace) - `process` module: pid_alive / pick_free_port / signal_pid / kill_pids + spawn_process with start_new_session=True so detached worker processes survive parent SIGHUP and a graceful SIGTERM→grace→SIGKILL teardown sequence is one helper away - pyproject: `[project.scripts] areal = ...cli.main:cli`, optional `[cli]` deps (click, colorlog) Co-Authored-By: Claude Opus 4.7 * refactor(cli): unify terminate + reuse kill_process_tree, drop pick_free_port Replace the scaffold's bespoke process primitives with shared utilities already in the codebase: - process: kill_pids now delegates to areal.infra.utils.proc.kill_process_tree, which walks the descendant tree via psutil. This fixes the bug TaoZex raised on signal_pid's inconsistent PermissionError handling — signal_pid is gone entirely. - process: drop pick_free_port. Callers should use areal.utils.network.find_free_ports, which draws from a non-ephemeral port range and supports exclude_ports — addresses the TOCTOU race guozhihao-224 flagged on the naive bind(0) implementation. - scheduler: new module with a stateless top-level terminate(ref, *, backend, grace_s) that subcommand CLIs can funnel teardown through. Only the local backend is implemented; future backends extend the dispatch in-module. The free-function shape keeps the dispatcher from drifting into stateful scheduler instances. Co-Authored-By: Claude Opus 4.7 * chore: integrate CLI shared files Co-Authored-By: Claude Opus 4.7 * refactor(cli): collect namespace path helpers into NamespacedStateStore state.py previously exposed 13 free functions (namespace_root, services_dir, logs_dir, service_state_path, …, recover_pids_from_raw_state) that all took ``namespace`` as their first argument. Promote them to methods on a new NamespacedStateStore class so subcommand CLIs construct one instance per namespace and stop threading the namespace string through every call. areal_home, atomic_write_json, SupportsComponentProbe, and ServiceStateBase remain at module level. ServiceLifecycle and ConfigLoader each hold a store instance built from their namespace at __init__ time; LogsCommand resolves log paths via ``lifecycle.store.logs_dir``. No behavior change. Note: downstream branches (inf / agent / train) will need to swap their imports from ``state.service_state_path``/``state.clear_current_service``/ etc. to ``store.`` on the next rebase. * feat(experimental): add agent service CLI * refactor(experimental): align agent CLI with Click * refactor(experimental): remove agent rl negotiated flag Use the existing RL session id and API key as the source of truth instead of keeping a derived boolean in agent session state. Key changes: - Drop rl_negotiated from SessionState and tolerate legacy state files - Route agent CLI and demo output through loggers instead of print - Update CLI state tests for the revised session schema * refactor(experimental): avoid creating agent session on run Agent service startup should only launch runtime components. Sessions are now created explicitly through new_session. Key changes: - Remove initial session creation from areal agent run - Drop the run --session-key option - Add CLI tests for session-free startup * refactor(experimental): align agent CLI with inference; drop session verbs Pulled the agent CLI in line with feat/inference-service-cli and removed surface the controller-replacement scope does not need. Style alignment with inference CLI: - adopt 2-step *_cmd -> do_* (drop the redundant handle layer) - table / JSON output via click.echo; hard failures via click.ClickException - Click default_map driven by load_click_default_map(_BINDINGS); drop the hand-rolled cfg_get / resolve_* helpers - single AgentCli logger (registered color in areal/utils/logging.py), exported from new agent/common.py alongside running_state / load_running_state / wait_http_health - AgentCLIHTTPError / AgentCLIUnreachable renamed to AgentHTTPError / AgentUnreachable; per-server clients renamed (GatewayClient, RouterClient, DataProxyClient) to mirror inference/client.py Removed surface: - interactive REPL stub (interactive.py + --interactive / --stop-on-exit / --history-file on run); chat / reward placeholders never shipped - destroy / health aliases of stop / status - new_session / switch_session and session_ops; SessionState / SessionsState / InferenceClient go with them. Session lifecycle is moving to a gateway /v1/sessions REST endpoint; CLI no longer owns it - legacy rl_negotiated migration in state.py (the flag was removed two commits ago; no production state needs the shim) Net diff: -830 lines, 6 tests green, pre-commit clean. * refactor(experimental): drop cli scaffold overlap pending #1440 The shared cli/cli.py + cli/main.py entry points and the pyproject [project.scripts] + the example invocation tweak duplicated plumbing that the cli scaffold PR (#1440) now provides. Drop them here so this branch can rebase cleanly on top of #1440 once that lands; the agent CLI itself (cli/agent/*) is untouched and continues to live in this PR. Co-Authored-By: Claude Opus 4.7 * refactor(experimental): rename agent http.py to client.py for parity with inf inf-cli puts the same kind of HTTP client wrappers in cli/inference/client.py; rename the agent counterpart to match so the two sibling subcommand packages have parallel layouts. Pure rename + three import updates, no behavior change. Co-Authored-By: Claude Opus 4.7 * refactor(experimental): adopt cli scaffold v2 in agent service Rebase onto feat/experimental-cli-scaffold and replace the per-CLI duplicates with the shared base classes/utilities: - state.py now keeps only the agent dataclasses; ServiceState satisfies ServiceStateBase and gateway_alive() is anchored on the gateway PID only (fixes the prior "any pid alive" check that kept reporting running after the gateway died). - config.py / client.py / lifecycle.py shrink to thin subclasses of ConfigLoader / BaseHTTPClient / ServiceLifecycle. - run.py and stop.py route through ServiceLifecycle for double-start refusal, force-replace, and state cleanup. - status.py renders via StatusReporter + ColumnSpec; ps.py emits via json_or_table; the bespoke logs subcommand is dropped in favor of LogsCommand(lifecycle=...).build(). - launcher.py reserves all ports up front through find_free_ports (non-ephemeral, no TOCTOU) and uses the scaffold spawn/wait helpers. Net ~360 LOC dropped from the agent module while every behavioral guarantee carries over. * refactor(experimental): drop empty Gateway/DataProxy client subclasses Both classes only inherited BaseHTTPClient.health() and were never instantiated after the scaffold refactor (StatusReporter probes addrs directly, launcher only uses RouterClient). Callers that ever need a gateway/data-proxy health probe can do BaseHTTPClient(addr) — the variable name carries the role. RouterClient stays because register_proxy is a real method. * fix(experimental): point areal CLI script at cli.py entry main.py was dropped earlier when scaffold landed (its single-line shim was redundant with __main__.py), but the [project.scripts] entry still referenced it, leaving the installed ``areal`` console script broken. Update both pyproject manifests to ``cli.cli:cli``. * docs(cli/agent): add agent service CLI guide Document the `areal agent` subcommand group: launching the gateway/router with N worker/data-proxy pairs, inspecting service state, log management, two-phase shutdown, configuration precedence, and the relationship with `areal inf`. Co-Authored-By: Claude Opus 4.7 * style(docs): apply mdformat to agent CLI guide * refactor(experimental): adopt scaffold NamespacedStateStore in agent scaffold's state.py promoted the namespace-aware free functions onto NamespacedStateStore. Update agent accordingly: hold a module-level ``store = NamespacedStateStore(AGENT_NAMESPACE)`` and route every service_state_path / set_current_service / clear_current_service / logs_dir call through it. No subclassing needed — agent has no extra state files (vs inf's two-file split). Behavior unchanged; 6 tests still pass. * style: sort v2 CLI imports * refactor(v2/cli/agent): drop dead --inf-* options The --inf-addr / --inf-api-key / --inf-model surface was a placeholder for a session-api-key negotiation that never landed: the values were written into ServiceState but no downstream code (launcher, gateway, worker, data_proxy, /v1/responses bridge) reads them. Keeping the options advertises functionality the CLI does not provide. - run.py: drop the three click options and do_run params - launcher.py: drop the inf_* kwargs on launch_agent_stack - state.py: drop inf_* fields from ServiceState; pop them on load() for forward-compat with state files written by older revisions - config.py: drop the [inference] -> run.inf_* TOML bindings - cli_guide.md: drop the --inf-* example, the "Relationship with areal inf" section, and the [inference] TOML block; also drop the "Multiple pairs" example and fix the agent import path syntax from the package:Class colon form to the module.Class dot form (the worker resolves the path via import_from_string which requires dots) - tests: drop inf_* kwargs from test_agent_cli_run; rewrite the config default_map test to assert on a non-inference key Will be reintroduced (with real wiring) when /v1/sessions lands on the gateway and the gateway process needs inf credentials to negotiate session_api_key for RL trajectory association. --------- Co-authored-by: Claude Opus 4.7 --- areal/utils/logging.py | 1 + areal/v2/cli/agent/__init__.py | 35 ++++ areal/v2/cli/agent/cli_guide.md | 180 ++++++++++++++++++ areal/v2/cli/agent/client.py | 12 ++ areal/v2/cli/agent/commands/__init__.py | 1 + areal/v2/cli/agent/commands/ps.py | 61 +++++++ areal/v2/cli/agent/commands/run.py | 82 +++++++++ areal/v2/cli/agent/commands/status.py | 119 ++++++++++++ areal/v2/cli/agent/commands/stop.py | 57 ++++++ areal/v2/cli/agent/config.py | 29 +++ areal/v2/cli/agent/launcher.py | 232 ++++++++++++++++++++++++ areal/v2/cli/agent/lifecycle.py | 12 ++ areal/v2/cli/agent/state.py | 104 +++++++++++ areal/v2/cli/cli.py | 2 + tests/v2/cli/__init__.py | 1 + tests/v2/cli/test_agent_cli_parser.py | 34 ++++ tests/v2/cli/test_agent_cli_run.py | 74 ++++++++ tests/v2/cli/test_agent_cli_state.py | 58 ++++++ 18 files changed, 1094 insertions(+) create mode 100644 areal/v2/cli/agent/__init__.py create mode 100644 areal/v2/cli/agent/cli_guide.md create mode 100644 areal/v2/cli/agent/client.py create mode 100644 areal/v2/cli/agent/commands/__init__.py create mode 100644 areal/v2/cli/agent/commands/ps.py create mode 100644 areal/v2/cli/agent/commands/run.py create mode 100644 areal/v2/cli/agent/commands/status.py create mode 100644 areal/v2/cli/agent/commands/stop.py create mode 100644 areal/v2/cli/agent/config.py create mode 100644 areal/v2/cli/agent/launcher.py create mode 100644 areal/v2/cli/agent/lifecycle.py create mode 100644 areal/v2/cli/agent/state.py create mode 100644 tests/v2/cli/__init__.py create mode 100644 tests/v2/cli/test_agent_cli_parser.py create mode 100644 tests/v2/cli/test_agent_cli_run.py create mode 100644 tests/v2/cli/test_agent_cli_state.py diff --git a/areal/utils/logging.py b/areal/utils/logging.py index 94e5839a6e..1c67860138 100644 --- a/areal/utils/logging.py +++ b/areal/utils/logging.py @@ -53,6 +53,7 @@ "TrainController": "white", "RolloutController": "white", "WorkflowExecutor": "white", + "AgentCli": "white", # Stats/Perf - green "StatsLogger": "light_green", "StatsTracker": "light_green", diff --git a/areal/v2/cli/agent/__init__.py b/areal/v2/cli/agent/__init__.py new file mode 100644 index 0000000000..724728eb7d --- /dev/null +++ b/areal/v2/cli/agent/__init__.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path + +import click + +from areal.v2.cli.agent.commands.ps import ps_cmd +from areal.v2.cli.agent.commands.run import run_cmd +from areal.v2.cli.agent.commands.status import status_cmd +from areal.v2.cli.agent.commands.stop import stop_cmd +from areal.v2.cli.agent.config import load_click_default_map +from areal.v2.cli.agent.lifecycle import agent_lifecycle +from areal.v2.cli.commands.logs import LogsCommand + + +@click.group(help="Manage agent services.") +@click.option( + "--config", + "config_file", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + default=None, + help="Extra TOML file merged on top of ~/.areal/agent/config.toml.", +) +@click.pass_context +def agent(ctx: click.Context, config_file: Path | None) -> None: + ctx.default_map = load_click_default_map(extra=config_file) + + +agent.add_command(run_cmd) +agent.add_command(stop_cmd) +agent.add_command(status_cmd) +agent.add_command(ps_cmd) +agent.add_command(LogsCommand(lifecycle=agent_lifecycle).build()) diff --git a/areal/v2/cli/agent/cli_guide.md b/areal/v2/cli/agent/cli_guide.md new file mode 100644 index 0000000000..1524d6f698 --- /dev/null +++ b/areal/v2/cli/agent/cli_guide.md @@ -0,0 +1,180 @@ +# AReaL Agent Service CLI + +`areal agent` is the agent subcommand group under the top-level `areal` CLI. It launches +a set of agent service processes on the local machine (gateway / router + N +worker/data-proxy pairs) so that an upstream application can interact with the agent +over HTTP. Its shape is very similar to `areal inf`, but it serves an agent (multi-turn +interaction with session state) rather than stateless inference. + +## Basic concepts + +A running agent service has the following components: + +- `gateway`: exposes session and agent APIs to the outside. One per service. +- `router`: routes requests to a data-proxy based on load. One per service. +- `worker[i]`: the process that actually runs user agent code. The agent class is + imported from the `module.path` form passed via `--agent`. +- `data-proxy[i]`: the session-management / accounting layer in front of `worker[i]`. + Each worker is paired with one proxy to form a **pair**.`--num-pairs` controls the + number of replicas. + +Request flow: + +``` +client → gateway → router → data-proxy[i] → worker[i] → agent code + └ session lifecycle / record ┘ +``` + +The CLI's local state is stored under `~/.areal/agent/` by default. The root directory +can be overridden via `AREAL_HOME`: + +```bash +export AREAL_HOME=/path/to/areal-home +``` + +## Launching the service + +Minimum launch — a single (worker, proxy) pair: + +```bash +areal agent run \ + --service default \ + --agent my_package.my_agent.MyAgent \ + --num-pairs 1 \ + --admin-api-key areal-agent-admin +``` + +`--agent` is required; it is the import path the worker process uses to load the agent +class. + +Force-start by clearing stale state: + +```bash +areal agent run --service default --agent ... --force +``` + +## Inspecting service state + +List all agent services on the local machine: + +```bash +areal agent ps +areal agent ps --all # include stale rows +areal agent ps --json +``` + +Output columns: `SERVICE / STATUS / GATEWAY / AGENT`. + +Drill into a single service for per-component health: + +```bash +areal agent status --service default +``` + +The output includes the gateway, router, and each pair's worker + proxy. `--watch` mode +refreshes on an interval (default 2 seconds): + +```bash +areal agent status --service default --watch --interval 1 +``` + +JSON mode plays well with jq: + +```bash +areal agent status --service default --json | jq '.pairs[].worker' +``` + +## Talking to the service + +The CLI **does not** manage how an application talks to the service —applications hit +the gateway HTTP endpoints directly. The status command tells you the gateway URL: + +```bash +GATEWAY_URL=$(areal agent status --service default --json | jq -r '.gateway.url') +echo "gateway at $GATEWAY_URL" +``` + +The application then sends requests against that URL with `--admin-api-key` (or a +session key obtained from the gateway). + +## Logs + +Each component writes a separate log file: + +```bash +areal agent logs --service default --component gateway -f +areal agent logs --service default --component router -f +areal agent logs --service default --component worker-0 -f +areal agent logs --service default --component proxy-0 -f +``` + +Naming convention: + +- `gateway` / `router`: service-level singletons +- `worker-` / `proxy-`: the worker / data-proxy of the i-th pair (istarts from 0) + +If `--component` is wrong, the CLI prints the available names. `-f` uses `tail -F` +semantics. + +## Stopping + +```bash +areal agent stop --service default +``` + +The default is a two-phase shutdown: SIGTERM, wait `--grace-period`(10s), then SIGKILL. +Immediate SIGKILL: + +```bash +areal agent stop --service default --force +``` + +`--keep-state` preserves the state file (kills processes but leaves the on-disk +`.json` alone): + +```bash +areal agent stop --service default --keep-state +``` + +## Configuration file + +`areal agent` reads `~/.areal/agent/config.toml` as defaults on startup;additional +config files can be passed in: + +```bash +areal agent --config ./my-agent.toml run --service default --agent ... +``` + +Example: + +```toml +[default] +service = "default" +admin_api_key = "areal-agent-admin" +log_level = "info" + +[run] +agent = "my_package.my_agent.MyAgent" +num_pairs = 2 +setup_timeout = 120 +health_poll_interval = 5 +drain_timeout = 30 +session_timeout = 1800 +``` + +Precedence: \*\*CLI flag > TOML passed via `--config` > `~/.areal/agent/config.toml` + +> hardcoded defaults\*\*. + +## Not implemented yet + +The current `areal agent` does **not** include: + +- Session-level CLI operations (start session / set reward / export trajectory) — these + are tightly coupled with the application and are handled by the application talking + directly to the gateway HTTP +- Automatic failure recovery / heartbeat monitoring — `status` is on-demand; it does not + continuously observe component health. If a worker dies, users have to discover it by + running `status` or checking logs. +- Distributed scheduling — only local processes on the local machine; k8s / slurm and + friends are out of scope for the current CLI. diff --git a/areal/v2/cli/agent/client.py b/areal/v2/cli/agent/client.py new file mode 100644 index 0000000000..da22c3a04b --- /dev/null +++ b/areal/v2/cli/agent/client.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any + +from areal.v2.cli.client import BaseHTTPClient + + +class RouterClient(BaseHTTPClient): + def register_proxy(self, addr: str, *, timeout: float = 10.0) -> dict[str, Any]: + return self._post("/register", payload={"addr": addr}, timeout=timeout) diff --git a/areal/v2/cli/agent/commands/__init__.py b/areal/v2/cli/agent/commands/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/areal/v2/cli/agent/commands/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/areal/v2/cli/agent/commands/ps.py b/areal/v2/cli/agent/commands/ps.py new file mode 100644 index 0000000000..b2efe73f2b --- /dev/null +++ b/areal/v2/cli/agent/commands/ps.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.agent.lifecycle import agent_lifecycle +from areal.v2.cli.process import pid_alive +from areal.v2.cli.utils import json_or_table + + +@click.command(name="ps", help="List locally known agent services.") +@click.option("--json", "as_json", is_flag=True) +@click.option("--all", "include_all", is_flag=True, help="Include stale services.") +def ps_cmd(as_json: bool, include_all: bool) -> None: + raise SystemExit(do_ps(as_json=as_json, include_all=include_all) or 0) + + +def do_ps(*, as_json: bool, include_all: bool) -> int: + rows: list[dict] = [] + for service in agent_lifecycle.list_services(): + try: + state = agent_lifecycle.load_state(service) + except Exception: + if include_all: + rows.append({"service": service, "status": "stale"}) + continue + running = pid_alive(state.gateway.pid) + if running or include_all: + rows.append( + { + "service": service, + "status": "running" if running else "stale", + "gateway_url": state.gateway.url, + "agent": state.agent, + } + ) + + json_or_table(rows, as_json=as_json, table_renderer=_print_table) + return 0 + + +def _print_table(rows: list[dict]) -> None: + if not rows: + click.echo("no agent services") + return + cols = ("SERVICE", "STATUS", "GATEWAY", "AGENT") + table = [ + ( + row["service"], + row["status"], + row.get("gateway_url", ""), + row.get("agent", ""), + ) + for row in rows + ] + widths = [max(len(str(r[i])) for r in (cols, *table)) for i in range(4)] + fmt = " ".join(f"{{:<{w}}}" for w in widths) + click.echo(fmt.format(*cols)) + for row in table: + click.echo(fmt.format(*row)) diff --git a/areal/v2/cli/agent/commands/run.py b/areal/v2/cli/agent/commands/run.py new file mode 100644 index 0000000000..7e8ca6628f --- /dev/null +++ b/areal/v2/cli/agent/commands/run.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.agent.launcher import launch_agent_stack +from areal.v2.cli.agent.lifecycle import agent_lifecycle +from areal.v2.cli.agent.state import ServiceState +from areal.v2.cli.client import ServiceHTTPError, ServiceUnreachable +from areal.v2.cli.process import kill_pids +from areal.v2.cli.state import DEFAULT_SERVICE +from areal.v2.cli.utils import register_cli_logger + +logger = register_cli_logger("AgentCli") + + +@click.command(name="run", help="Launch an agent service.") +@click.option("--service", default=DEFAULT_SERVICE, show_default=True) +@click.option("--agent", default=None, help="Agent import path.") +@click.option("--num-pairs", type=int, default=1, show_default=True) +@click.option("--admin-api-key", default="areal-agent-admin", show_default=True) +@click.option("--setup-timeout", type=float, default=120.0, show_default=True) +@click.option("--health-poll-interval", type=float, default=5.0, show_default=True) +@click.option("--drain-timeout", type=float, default=30.0, show_default=True) +@click.option("--session-timeout", type=float, default=1800.0, show_default=True) +@click.option( + "--log-level", + type=click.Choice(["debug", "info", "warning", "error"]), + default="info", + show_default=True, +) +@click.option("--force", is_flag=True, help="Replace stale or running service state.") +def run_cmd(**opts) -> None: + raise SystemExit(do_run(**opts) or 0) + + +def do_run( + *, + service: str, + agent: str | None, + num_pairs: int, + admin_api_key: str, + setup_timeout: float, + health_poll_interval: float, + drain_timeout: float, + session_timeout: float, + log_level: str, + force: bool, +) -> int: + if not agent: + raise click.UsageError("--agent is required") + + if force: + agent_lifecycle.force_replace_slot(service, grace_s=5.0) + else: + agent_lifecycle.refuse_if_running(service) + + launched: ServiceState | None = None + try: + launched = launch_agent_stack( + service=service, + agent=agent, + admin_api_key=admin_api_key, + num_pairs=num_pairs, + setup_timeout=setup_timeout, + session_timeout=session_timeout, + health_poll_interval=health_poll_interval, + drain_timeout=drain_timeout, + log_level=log_level, + ) + launched.save() + except (ServiceHTTPError, ServiceUnreachable, RuntimeError, ValueError) as exc: + if launched is not None: + kill_pids( + [pid for _, handle in launched.components() if (pid := handle.pid) > 0], + grace_s=5.0, + ) + raise click.ClickException(f"failed to launch agent service: {exc}") from exc + + logger.info("service=%s gateway=%s", service, launched.gateway.url) + return 0 diff --git a/areal/v2/cli/agent/commands/status.py b/areal/v2/cli/agent/commands/status.py new file mode 100644 index 0000000000..60b5ad596b --- /dev/null +++ b/areal/v2/cli/agent/commands/status.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import time + +import click + +from areal.v2.cli.agent.lifecycle import agent_lifecycle +from areal.v2.cli.process import pid_alive +from areal.v2.cli.state import SupportsComponentProbe +from areal.v2.cli.status import ColumnSpec, StatusReporter +from areal.v2.cli.utils import json_or_table + + +@click.command(name="status", help="Show agent service health.") +@click.option("--service", default=None) +@click.option("--watch", is_flag=True) +@click.option("--interval", type=float, default=2.0, show_default=True) +@click.option("--json", "as_json", is_flag=True) +def status_cmd( + service: str | None, watch: bool, interval: float, as_json: bool +) -> None: + raise SystemExit( + do_status(service=service, watch=watch, interval=interval, as_json=as_json) or 0 + ) + + +def do_status( + *, service: str | None, watch: bool, interval: float, as_json: bool +) -> int: + name = agent_lifecycle.resolve_service_name(service) + while True: + _emit_once(name, as_json=as_json) + if not watch: + return 0 + time.sleep(interval) + + +def _emit_once(service: str, *, as_json: bool) -> None: + path = agent_lifecycle.state_path(service) + if not path.exists(): + payload = {"service": service, "running": False, "components": []} + json_or_table( + payload, + as_json=as_json, + table_renderer=lambda p: click.echo( + f"service {p['service']!r} is not running" + ), + ) + return + try: + state = agent_lifecycle.load_state(service) + except Exception as exc: + payload = { + "service": service, + "running": False, + "error": f"failed to read state: {exc}", + "components": [], + } + json_or_table( + payload, + as_json=as_json, + table_renderer=lambda p: click.echo( + f"service {p['service']!r}: {p['error']}" + ), + ) + return + + components = list(state.components()) + reporter = StatusReporter(components, _columns(service)) + alive = reporter.probe_all() + + if as_json: + payload = { + "service": service, + "running": pid_alive(state.gateway.pid), + "gateway_url": state.gateway.url, + "router_url": state.router.url, + "components": reporter.json_snapshot(alive), + } + click.echo(_json_indent(payload)) + return + reporter.print_table( + reporter.render_rows(alive), + header_line=f"service: {service} gateway: {state.gateway.url}", + ) + + +def _columns(service: str) -> list[ColumnSpec]: + def _service(label: str, _: SupportsComponentProbe, __: bool) -> str: + del label + return service + + def _component(label: str, _: SupportsComponentProbe, __: bool) -> str: + return label + + def _status(_: str, __: SupportsComponentProbe, alive: bool) -> str: + return "ok" if alive else "down" + + def _addr(_: str, handle: SupportsComponentProbe, __: bool) -> str: + return handle.addr or "-" + + def _pid(_: str, handle: SupportsComponentProbe, __: bool) -> str: + return str(handle.pid) if handle.pid > 0 else "-" + + return [ + ColumnSpec("SERVICE", _service), + ColumnSpec("COMPONENT", _component), + ColumnSpec("STATUS", _status), + ColumnSpec("ADDR", _addr), + ColumnSpec("PID", _pid), + ] + + +def _json_indent(payload: dict) -> str: + import json + + return json.dumps(payload, indent=2, default=str) diff --git a/areal/v2/cli/agent/commands/stop.py b/areal/v2/cli/agent/commands/stop.py new file mode 100644 index 0000000000..5cea6302c4 --- /dev/null +++ b/areal/v2/cli/agent/commands/stop.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.agent.lifecycle import agent_lifecycle +from areal.v2.cli.agent.state import ServiceState +from areal.v2.cli.process import kill_pids + + +@click.command(name="stop", help="Stop an agent service.") +@click.option("--service", default=None) +@click.option("--grace-period", type=float, default=10.0, show_default=True) +@click.option("--keep-state", is_flag=True) +@click.option("--force", is_flag=True, help="SIGKILL immediately.") +def stop_cmd( + service: str | None, grace_period: float, keep_state: bool, force: bool +) -> None: + raise SystemExit( + do_stop( + service=service, + grace_period=grace_period, + keep_state=keep_state, + force=force, + ) + or 0 + ) + + +def do_stop( + *, + service: str | None, + grace_period: float, + keep_state: bool, + force: bool, +) -> int: + name = agent_lifecycle.resolve_service_name(service) + path = agent_lifecycle.state_path(name) + if not path.exists(): + click.echo(f"service {name!r} is not running") + return 0 + + try: + state = agent_lifecycle.load_state(name) + except Exception: + if not keep_state: + ServiceState.remove(name) + click.echo(f"removed stale state for {name!r}") + return 0 + + pids = [pid for _, h in state.components() if (pid := h.pid) > 0] + kill_pids(pids, grace_s=0.0 if force else grace_period) + if not keep_state: + ServiceState.remove(name) + click.echo(f"service {name!r} stopped") + return 0 diff --git a/areal/v2/cli/agent/config.py b/areal/v2/cli/agent/config.py new file mode 100644 index 0000000000..d493604ea9 --- /dev/null +++ b/areal/v2/cli/agent/config.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path + +from areal.v2.cli.agent.state import AGENT_NAMESPACE +from areal.v2.cli.config import BindingMap, ConfigLoader + +# (section, key) -> (verb_or_verbs, click_option_name). +# Anything not listed in the agent's TOML is silently ignored. +AGENT_BINDINGS: BindingMap = { + ("default", "service"): (("run", "stop", "status", "ps", "logs"), "service"), + ("default", "admin_api_key"): ("run", "admin_api_key"), + ("default", "log_level"): ("run", "log_level"), + ("run", "agent"): ("run", "agent"), + ("run", "num_pairs"): ("run", "num_pairs"), + ("run", "setup_timeout"): ("run", "setup_timeout"), + ("run", "health_poll_interval"): ("run", "health_poll_interval"), + ("run", "drain_timeout"): ("run", "drain_timeout"), + ("run", "session_timeout"): ("run", "session_timeout"), +} + + +agent_config_loader = ConfigLoader(namespace=AGENT_NAMESPACE, bindings=AGENT_BINDINGS) + + +def load_click_default_map(extra: Path | None = None) -> dict: + return agent_config_loader.load_click_default_map(extra=extra) diff --git a/areal/v2/cli/agent/launcher.py b/areal/v2/cli/agent/launcher.py new file mode 100644 index 0000000000..2ce55e2d4f --- /dev/null +++ b/areal/v2/cli/agent/launcher.py @@ -0,0 +1,232 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path + +from areal.utils.network import find_free_ports +from areal.v2.cli.agent.client import RouterClient +from areal.v2.cli.agent.state import ( + PairState, + ProcessState, + ServiceState, + store, +) +from areal.v2.cli.process import spawn_process +from areal.v2.cli.utils import wait_http_health + + +def launch_agent_stack( + *, + service: str, + agent: str, + admin_api_key: str, + num_pairs: int, + setup_timeout: float, + session_timeout: float, + health_poll_interval: float, + drain_timeout: float, + log_level: str, +) -> ServiceState: + log_dir = store.logs_dir(service) + + # Reserve every port we need up front from the non-ephemeral pool to + # avoid TOCTOU collisions between successive bind(0) calls. + ports = find_free_ports(2 + 2 * num_pairs) + router_port = ports[0] + gateway_port = ports[1] + pair_ports = ports[2:] + + router_log = log_dir / "router.log" + router_pid = _spawn_router( + host="127.0.0.1", + port=router_port, + admin_api_key=admin_api_key, + log_level=log_level, + log_file=router_log, + ) + router_url = f"http://127.0.0.1:{router_port}" + wait_http_health(router_url, pid=router_pid, timeout=setup_timeout, label="router") + + pairs: list[PairState] = [] + router = RouterClient(router_url, admin_api_key) + for idx in range(num_pairs): + pair = _spawn_pair( + index=idx, + agent=agent, + worker_port=pair_ports[2 * idx], + proxy_port=pair_ports[2 * idx + 1], + session_timeout=session_timeout, + log_level=log_level, + log_dir=log_dir, + setup_timeout=setup_timeout, + ) + router.register_proxy(pair.data_proxy.url) + pairs.append(pair) + + gateway_log = log_dir / "gateway.log" + gateway_pid = _spawn_gateway( + host="127.0.0.1", + port=gateway_port, + router_url=router_url, + admin_api_key=admin_api_key, + log_level=log_level, + log_file=gateway_log, + ) + gateway_url = f"http://127.0.0.1:{gateway_port}" + wait_http_health( + gateway_url, pid=gateway_pid, timeout=setup_timeout, label="gateway" + ) + + return ServiceState( + service=service, + launch_mode="detached", + agent=agent, + admin_api_key=admin_api_key, + gateway=ProcessState( + component="gateway", + pid=gateway_pid, + url=gateway_url, + log_file=str(gateway_log), + ), + router=ProcessState( + component="router", + pid=router_pid, + url=router_url, + log_file=str(router_log), + ), + pairs=pairs, + session_timeout=session_timeout, + health_poll_interval=health_poll_interval, + drain_timeout=drain_timeout, + ) + + +def _spawn_router( + *, + host: str, + port: int, + admin_api_key: str, + log_level: str, + log_file: Path, +) -> int: + return spawn_process( + [ + sys.executable, + "-m", + "areal.v2.agent_service.router", + "--host", + host, + "--port", + str(port), + "--admin-api-key", + admin_api_key, + "--log-level", + log_level, + ], + log_file, + ) + + +def _spawn_gateway( + *, + host: str, + port: int, + router_url: str, + admin_api_key: str, + log_level: str, + log_file: Path, +) -> int: + return spawn_process( + [ + sys.executable, + "-m", + "areal.v2.agent_service.gateway", + "--host", + host, + "--port", + str(port), + "--router-addr", + router_url, + "--admin-api-key", + admin_api_key, + "--log-level", + log_level, + ], + log_file, + ) + + +def _spawn_pair( + *, + index: int, + agent: str, + worker_port: int, + proxy_port: int, + session_timeout: float, + log_level: str, + log_dir: Path, + setup_timeout: float, +) -> PairState: + worker_log = log_dir / f"worker-{index}.log" + worker_pid = spawn_process( + [ + sys.executable, + "-m", + "areal.v2.agent_service.worker", + "--agent", + agent, + "--host", + "127.0.0.1", + "--port", + str(worker_port), + "--log-level", + log_level, + ], + worker_log, + ) + worker_url = f"http://127.0.0.1:{worker_port}" + wait_http_health( + worker_url, pid=worker_pid, timeout=setup_timeout, label=f"worker-{index}" + ) + + proxy_log = log_dir / f"proxy-{index}.log" + proxy_pid = spawn_process( + [ + sys.executable, + "-m", + "areal.v2.agent_service.data_proxy", + "--worker-addr", + worker_url, + "--host", + "127.0.0.1", + "--port", + str(proxy_port), + "--session-timeout", + str(int(session_timeout)), + "--log-level", + log_level, + ], + proxy_log, + ) + proxy_url = f"http://127.0.0.1:{proxy_port}" + wait_http_health( + proxy_url, pid=proxy_pid, timeout=setup_timeout, label=f"proxy-{index}" + ) + + return PairState( + index=index, + worker=ProcessState( + component=f"worker-{index}", + pid=worker_pid, + url=worker_url, + log_file=str(worker_log), + ), + data_proxy=ProcessState( + component=f"proxy-{index}", + pid=proxy_pid, + url=proxy_url, + log_file=str(proxy_log), + ), + ) diff --git a/areal/v2/cli/agent/lifecycle.py b/areal/v2/cli/agent/lifecycle.py new file mode 100644 index 0000000000..cfd93b0076 --- /dev/null +++ b/areal/v2/cli/agent/lifecycle.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from areal.v2.cli.agent.state import AGENT_NAMESPACE, ServiceState +from areal.v2.cli.lifecycle import ServiceLifecycle + +agent_lifecycle = ServiceLifecycle( + namespace=AGENT_NAMESPACE, + state_class=ServiceState, + stop_command="areal agent stop", +) diff --git a/areal/v2/cli/agent/state.py b/areal/v2/cli/agent/state.py new file mode 100644 index 0000000000..42d7a73ace --- /dev/null +++ b/areal/v2/cli/agent/state.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +import time +from collections.abc import Iterable +from dataclasses import asdict, dataclass, field + +from areal.v2.cli.process import pid_alive +from areal.v2.cli.state import ( + NamespacedStateStore, + ServiceStateBase, + SupportsComponentProbe, + atomic_write_json, +) + +AGENT_NAMESPACE = "agent" + +# Module-level store — every save/load/remove routes paths through it, +# tests with ``AREAL_HOME`` work because the store re-resolves on each +# call. +store = NamespacedStateStore(AGENT_NAMESPACE) + + +@dataclass +class ProcessState: + component: str + pid: int + url: str + log_file: str + + @property + def addr(self) -> str: + return self.url + + +@dataclass +class PairState: + index: int + worker: ProcessState + data_proxy: ProcessState + + +@dataclass +class ServiceState(ServiceStateBase): + service: str + launch_mode: str + agent: str + admin_api_key: str + gateway: ProcessState + router: ProcessState + pairs: list[PairState] + session_timeout: float = 1800.0 + health_poll_interval: float = 5.0 + drain_timeout: float = 30.0 + started_at: float = field(default_factory=time.time) + + def save(self) -> None: + atomic_write_json(store.service_state_path(self.service), asdict(self)) + store.set_current_service(self.service) + + @classmethod + def load(cls, service: str) -> ServiceState: + with open(store.service_state_path(service)) as f: + raw = json.load(f) + raw["gateway"] = ProcessState(**raw["gateway"]) + raw["router"] = ProcessState(**raw["router"]) + raw["pairs"] = [ + PairState( + index=pair["index"], + worker=ProcessState(**pair["worker"]), + data_proxy=ProcessState(**pair["data_proxy"]), + ) + for pair in raw.get("pairs", []) + ] + # ``created_at`` is the legacy field name; accept it for forward + # compatibility with any state files written before the rename. + if "created_at" in raw and "started_at" not in raw: + raw["started_at"] = raw.pop("created_at") + raw.pop("created_at", None) + for legacy in ("inf_addr", "inf_api_key", "inf_model"): + raw.pop(legacy, None) + return cls(**raw) + + @classmethod + def remove(cls, service: str) -> None: + path = store.service_state_path(service) + if path.exists(): + path.unlink() + store.clear_current_service(service) + + def gateway_alive(self) -> bool: + # The CLI treats "running" as "gateway process still alive". The + # gateway is the entry point — without it, the rest of the stack + # is unreachable even if some children survived. + return pid_alive(self.gateway.pid) + + def components(self) -> Iterable[tuple[str, SupportsComponentProbe]]: + yield "gateway", self.gateway + yield "router", self.router + for pair in self.pairs: + yield pair.worker.component, pair.worker + yield pair.data_proxy.component, pair.data_proxy diff --git a/areal/v2/cli/cli.py b/areal/v2/cli/cli.py index 65e41c09fd..be8584aef9 100644 --- a/areal/v2/cli/cli.py +++ b/areal/v2/cli/cli.py @@ -4,6 +4,7 @@ import click +from areal.v2.cli.agent import agent from areal.v2.cli.training import train from areal.version import __version__ @@ -17,4 +18,5 @@ def cli() -> None: pass +cli.add_command(agent) cli.add_command(train) diff --git a/tests/v2/cli/__init__.py b/tests/v2/cli/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/tests/v2/cli/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/v2/cli/test_agent_cli_parser.py b/tests/v2/cli/test_agent_cli_parser.py new file mode 100644 index 0000000000..f2e013de63 --- /dev/null +++ b/tests/v2/cli/test_agent_cli_parser.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from click.testing import CliRunner + +from areal.v2.cli.cli import cli + + +def test_agent_help_contains_expected_commands(): + result = CliRunner().invoke(cli, ["agent", "--help"]) + + assert result.exit_code == 0 + assert "run" in result.output + assert "stop" in result.output + assert "status" in result.output + assert "ps" in result.output + assert "logs" in result.output + + +def test_agent_help_does_not_register_session_commands(): + runner = CliRunner() + + for verb in ("new_session", "switch_session", "chat", "reward"): + result = runner.invoke(cli, ["agent", verb]) + assert result.exit_code != 0 + assert "No such command" in result.output + + +def test_agent_run_does_not_accept_initial_session_key(): + result = CliRunner().invoke(cli, ["agent", "run", "--help"]) + + assert result.exit_code == 0 + assert "--session-key" not in result.output diff --git a/tests/v2/cli/test_agent_cli_run.py b/tests/v2/cli/test_agent_cli_run.py new file mode 100644 index 0000000000..b4502ac686 --- /dev/null +++ b/tests/v2/cli/test_agent_cli_run.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from areal.v2.cli.agent.commands import run +from areal.v2.cli.agent.state import ( + PairState, + ProcessState, + ServiceState, + store, +) + + +def test_run_persists_service_state(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + + def fake_launch_agent_stack(**kwargs): + service = kwargs["service"] + return ServiceState( + service=service, + launch_mode="detached", + agent=kwargs["agent"], + admin_api_key=kwargs["admin_api_key"], + gateway=ProcessState( + component="gateway", + pid=11, + url="http://127.0.0.1:1", + log_file="gateway.log", + ), + router=ProcessState( + component="router", + pid=12, + url="http://127.0.0.1:2", + log_file="router.log", + ), + pairs=[ + PairState( + index=0, + worker=ProcessState( + component="worker-0", + pid=13, + url="http://127.0.0.1:3", + log_file="worker.log", + ), + data_proxy=ProcessState( + component="proxy-0", + pid=14, + url="http://127.0.0.1:4", + log_file="proxy.log", + ), + ) + ], + session_timeout=kwargs["session_timeout"], + health_poll_interval=kwargs["health_poll_interval"], + drain_timeout=kwargs["drain_timeout"], + ) + + monkeypatch.setattr(run, "launch_agent_stack", fake_launch_agent_stack) + + rc = run.do_run( + agent="pkg.Agent", + service="svc", + num_pairs=1, + admin_api_key="agent-admin", + setup_timeout=1.0, + health_poll_interval=1.0, + drain_timeout=1.0, + session_timeout=60.0, + log_level="info", + force=False, + ) + + assert rc == 0 + assert store.service_state_path("svc").exists() diff --git a/tests/v2/cli/test_agent_cli_state.py b/tests/v2/cli/test_agent_cli_state.py new file mode 100644 index 0000000000..e60e5119d5 --- /dev/null +++ b/tests/v2/cli/test_agent_cli_state.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from areal.v2.cli.agent.config import load_click_default_map +from areal.v2.cli.agent.state import ( + ProcessState, + ServiceState, + store, +) + + +def test_service_state_round_trip(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + state = ServiceState( + service="svc", + launch_mode="detached", + agent="pkg.Agent", + admin_api_key="key", + gateway=ProcessState( + component="gateway", + pid=11, + url="http://127.0.0.1:1", + log_file="gateway.log", + ), + router=ProcessState( + component="router", + pid=12, + url="http://127.0.0.1:2", + log_file="router.log", + ), + pairs=[], + ) + + state.save() + + loaded = ServiceState.load("svc") + assert loaded.service == "svc" + assert loaded.gateway.pid == 11 + assert [pid for _, h in loaded.components() if (pid := h.pid) > 0] == [11, 12] + assert store.service_state_path("svc").exists() + + +def test_config_maps_toml_sections_to_click_defaults(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path / "home")) + config_dir = tmp_path / "home" / "agent" + config_dir.mkdir(parents=True) + (config_dir / "config.toml").write_text( + "[default]\nadmin_api_key = 'base'\n[run]\nagent = 'pkg.Agent'\nnum_pairs = 4\n" + ) + extra = tmp_path / "extra.toml" + extra.write_text("[default]\nadmin_api_key = 'override'\n") + + default_map = load_click_default_map(extra=extra) + + assert default_map["run"]["admin_api_key"] == "override" + assert default_map["run"]["agent"] == "pkg.Agent" + assert default_map["run"]["num_pairs"] == 4 From b8917415990cee1b1b73603f12416944e4f7ec26 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:12:27 +0800 Subject: [PATCH 18/36] feat(cli): add inference service cli (#1434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): inference service CLI — daemon + 4 verbs `areal inf` is an ollama-style operator console for the local inference service. One daemon per user, one OpenAI-compatible gateway endpoint, models registered against it. Verbs: inf run start daemon (gateway + router); inline --model registers an external (--api-url) or internal (--backend / --model-path) model in the same call inf ps list registered models inf status daemon health + model count inf stop SIGTERM gateway+router, grace period, then SIGKILL State is a single ~/.areal/inf/state.json (pid + url + admin key + started_at). Everything else lives in the gateway/router process memory; CLI is otherwise stateless. Layout under areal/experimental/cli/: main.py / state.py shared scaffold (areal_home, pid_alive, atomic_write_json), to be reused by `areal train` in a separate PR commands/inf/__init__.py all four verbs + register helpers in a single file (one place to scan what the user can do) commands/inf/state.py DaemonState dataclass + paths commands/inf/launcher.py subprocess spawn helpers, including base_gpu_id support for sglang dp>1 commands/inf/client.py urllib gateway + router HTTP client Heavy imports (sglang/vllm/torch via areal.api.cli_args) stay lazy — inside register helpers only — so `areal inf -h` and `areal -h` parse the click tree without paying for them. * fix(cli/inf): persist model worker pids in state.json so stop kills them `_register_internal` returns the list of sglang+data-proxy pids it spawned, but `_do_run` discarded the return value. state.json then only held gateway+router pids, so `areal inf stop` left sglang and data-proxy processes orphaned. Add a `worker_pids: list[int]` field to DaemonState, save the list right after register_internal succeeds, and include those pids in the stop kill set. Also fix _do_stop short-circuiting on a dead gateway pid — even when the daemon front-end is dead, model worker pids in state may still be alive (the original bug pattern this commit fixes). Always try to kill every pid we know about, then drop the state file. * fix(cli/inf): route startup messages through getLogger; align admin key default Startup-time prints (router/gateway pids, replica spawn lines, daemon ready, foreground / shutdown notices) were going through click.echo, so they bypassed the AReaL log formatter — no timestamps, no log level, no PascalCase tag. Switch all of these to `getLogger("InfCli").info(...)` so output looks like the rest of the project: 20260611-09:14:02.157 InfCli INFO: starting inference daemon ... 20260611-09:14:02.342 InfCli INFO: router pid=304116 http://... 20260611-09:14:02.910 InfCli INFO: gateway pid=304118 http://... Register InfCli in LOGGER_COLORS_EXACT under the launcher (blue) group, since `areal inf run` is conceptually a process launcher. Reads kept on click.echo on purpose: `inf ps` / `inf status` plain-text output is structured (tabular or single-field-per-line) and meant to be consumed by jq / awk in scripts; piping that through the colored log formatter would break it. Also flip `--admin-api-key` default from "areal-admin-key" to "admin-api-key" — matches the existing inference_service convention. * feat(cli/inf): standalone `register` and `deregister` verbs Phase 2 — model lifecycle separated from daemon lifecycle. areal inf register [external | internal flags] Register a new model against a running daemon. Same flags as `inf run --model ...`, just attached to an existing service. areal inf deregister [--grace] [--force] Drop the model from the router, unregister its proxy workers, and SIGTERM/SIGKILL the spawned sglang+data-proxy procs. DaemonState gains a `models: dict[str, ModelEntry]` mapping each registered model to its (pids, proxy_addrs). This replaces the previous flat `worker_pids: list[int]` so deregister can target one model's processes without touching the others. `inf stop` flattens across all entries and kills the whole set; the foreground / failure cleanup paths use the same flatten. `_register_internal` now returns `(pids, proxy_addrs)` rather than just pids — both are needed at deregister time (router unregister takes the proxy addr; SIGTERM takes the pid). External models also get a state entry now (empty pids/addrs) so `inf deregister` can find them and drop them from the router. phase 1 verbs unchanged. * feat(cli/inf): phase 3 — reward + collect verbs Two verbs that round out the RL data path: areal inf reward [--model X] thin wrapper around POST /rl/set_reward. set_reward is the only thing that flips an active conversation into a ready trajectory, so any data-collection flow needs to call it (even with a dummy reward=0 just to "flush"). CLI verb is for users whose agent is in another language / shell / human raters; agent authors writing python are free to POST directly. areal inf collect --batch-size N \ [--sessions-out FILE] [--output FILE] \ [--timeout T] [--poll-interval S] \ [--discount D] [--style individual|concat] client-side batch orchestrator. start_session(group_size=N) -> hand sessions to the agent (via --sessions-out FILE) -> poll /export_trajectories every poll-interval seconds, accumulating unique trajectories until N are collected or timeout fires -> one final export with remove_session=True for cleanup -> dump JSONL. Mirrors the controller's rollout_batch path but moves the wait loop to the client so gateway / router stay stateless. Agent lifecycle is intentionally NOT inside collect (no --agent-cmd): agent runs in its own process and just reads sessions_out. JSONL is the only output format on purpose -- the gateway already serializes trajectories to JSON at HTTP boundary, so .pt would mean re-decoding tensors only to re-encode them; trainers consuming the output can torch.tensor(x) when they need it. 8 verbs total (run / ps / status / stop / register / deregister / reward / collect). * feat(cli/inf): enrich ps/status with model kind / backend / addrs Phase 2.5 — bring `inf ps` and `inf status` to the design_inf.md fidelity (sections 11.5 / 11.6) without bringing back the multi-service concept. ModelEntry gains four fields: kind: 'internal' | 'external' backend: spec string ('sglang:tp=2,dp=2') for internal, '' for external api_url: external upstream URL, '' for internal inference_server_addrs: per-replica sglang/vllm URLs (internal) `_register_internal` returns (pids, proxy_addrs, inf_addrs); _do_run and _do_register both fill in the new ModelEntry fields. inf ps now reads state.models (CLI-side truth) instead of polling gateway /v1/models. Output: NAME KIND BACKEND WORKERS qwen3 internal sglang:tp=2,dp=2 2 gpt-4o external - - inf status switches to a multi-row table per design 11.5: COMPONENT STATUS ADDR DETAILS gateway ok http://127.0.0.1:8080 models=2 router ok http://127.0.0.1:.. qwen3 registered internal backend=sglang:tp=2 workers=2 gpt-4o registered external api_url=https://... JSON output of both verbs follows suit. Backwards-compat: ModelEntry's new fields all have defaults, so an old state.json still loads cleanly. * feat(cli/inf): add `inf models` verb `inf ps` currently lists registered models (table: NAME / KIND / BACKEND / WORKERS). Add `inf models` as a more explicit alias — docker-style "different verb for different resource". `ps` keeps its current behavior; both call the same _print_models helper so output is identical. This isn't part of the multi-service rollback (which we decided not to do). It's a small ergonomic addition for the single-daemon shape. * feat(cli/inf): add `logs` verb + TOML config support + help text for proxy/engine args Three related additions: 1. `inf logs --component NAME [-f] [-n LINES]` Tail a log under ~/.areal/inf/logs/. Component defaults to 'gateway'; can be 'router' or a full model log basename like 'qwen3-inf-0'. -f follows (tail -F), -n sets initial line count (default 200). Exec's tail directly for stream fidelity. 2. TOML config support (design 12) - Group-level option `areal inf --config FILE` merges FILE on top of ~/.areal/inf/config.toml (both optional). - config.py loads TOML via tomllib (py3.11+), maps [default] / [launch] / [register.internal] / [collect] sections to CLI option defaults via click's default_map mechanism. - Precedence: CLI flag > --config > ~/.areal/inf/config.toml > hardcoded default. 3. Detailed help for --engine-args and --proxy-args Users couldn't guess what to pass. Now both flags show inline hints (common sglang knobs / data-proxy flags + defaults). Help is a single paragraph so click's wrap_text handles terminal width. 10 verbs total (run / stop / ps / status / models / register / deregister / reward / collect / logs). * fix(cli/inf): align `collect` flags with design 11.9 Rename + add + drop options on `inf collect` so the surface matches the design spec exactly: rename --discount -> --turn-discount rename --style -> --export-style add --format json|jsonl (default jsonl) add --json progress-events flag (placeholder; not implemented) drop --task-id (always 'cli-collect' internally) drop --sessions-out (agents query gateway directly) drop --poll-interval (always 2.0s internally) JSON output (--format json) emits {tid: interaction, ...} pretty-printed. JSONL output (default) emits one trajectory per line, each prefixed with trajectory_id. config.toml [collect] keys renamed to match. * fix(cli/inf): three bugs (admin key, gpu collision, sglang request log) P1. --admin-api-key default reverts from "admin-api-key" to "areal-admin-key" — matches the v2 inference_service convention used everywhere else in the codebase. P2. Registering a second internal model collided with the first on GPUs 0..tp-1. Cause: base_gpu_id was always r * tp, computed only against the current model's dp index, ignoring GPUs already used by previously-registered models. Fix: track a monotonic `next_gpu_id` cursor on DaemonState and pass it into `_register_internal` as `base_gpu_id`. Each ModelEntry now records its (base_gpu_id, gpu_count) so deregister can roll back the cursor when removing the *last* registered model (preserves contiguous allocation; doesn't try to coalesce holes in the middle, which is fine for the v1 use case). P3. sglang server logs only had model-load output, no chat requests. Cause: SGLangConfig.log_requests defaults to False. Fix: spawn sglang with log_requests=True so /chat/completions traffic shows up under ~/.areal/inf/logs/-inf-N.log. (data-proxy access logs are off via uvicorn config inside the data-proxy package itself; that's not under inf CLI's control.) * refactor(experimental): split inference CLI commands Move the inference service CLI out of the monolithic commands package and into command-specific modules. Align collect with the session/export flow by returning session keys, polling exports without revoking the router group, and cleaning up at the end. * feat(experimental): support multiple inference services * fix(experimental): harden inference CLI lifecycle Protect inference service state transitions so register and run do not race or leave orphaned processes behind. Key changes: - Track engine and proxy PIDs separately for phased shutdown - Lock model state during register and startup model setup - Clean up foreground services on SIGTERM and SIGHUP - Recover raw PIDs before forced service replacement - Align inference CLI model and session option names with the design * feat(cli/inf): add scheduler abstraction for worker placement Co-Authored-By: Claude Opus 4.7 * fix(cli/inf): widen probe timeout, parallelize status, drop default_model Co-Authored-By: Claude Opus 4.7 * refactor(experimental): adopt cli scaffold v2 in inference service Rebase onto feat/experimental-cli-scaffold and replace the per-CLI duplicates with the shared base classes/utilities: - state.py keeps the inf-specific dataclasses and the two-file recover_pids_from_raw_state; RuntimeState now satisfies ServiceStateBase (gateway_alive + components + .load classmethod). - config.py / client.py / lifecycle.py shrink to thin subclasses of ConfigLoader / BaseHTTPClient / ServiceLifecycle. The old GatewayHTTPError / GatewayUnreachable names are kept as aliases of ServiceHTTPError / ServiceUnreachable so subcommands swap mechanically. - InferenceLifecycle overrides force_replace_slot to walk the inf raw-state helper (which knows about the secondary model-state file) and to remove both files on cleanup. - common.py drops scaffold-replaced helpers (running_state / load_running_state / refuse_if_running / wait_http_health / wait_client_health / print_services / print_models / probe_http_health); keeps backend-spec parsing, model registration, TaskHandle formatters, and terminate_runtime_state (data-flow order is inf-specific). - commands/run.py uses ServiceLifecycle for refuse / force-replace and ForegroundWatcher for the SIGINT/SIGTERM/SIGHUP handling. - commands/stop / status / ps / models / register / deregister / reward / collect route through inf_lifecycle; status.py emits via StatusReporter + ColumnSpec; ps/models via json_or_table. - launcher.py and scheduler/local.py replace pick_free_port with find_free_ports (non-ephemeral, no TOCTOU); LocalScheduler tracks allocated ports across submits. - commands/logs.py is removed; LogsCommand(lifecycle=inf_lifecycle) wires the verb in __init__.py. Net ~290 LOC dropped while every behavioral guarantee carries over. * docs(cli/inf): add inference service CLI guide Document the `areal inf` subcommand group: launching the gateway/router, registering models, RL session flow with rewards, trajectory collection, log management, and configuration file precedence. Co-Authored-By: Claude Opus 4.7 * style(docs): apply mdformat to inference CLI guide * refactor(experimental): address PR review on inference CLI Address garrett4wade's review on #1435: 1+2) Drop the unfinished `collect` verb and the gateway RPCs it depended on. `commands/collect.py` is removed entirely; the gateway client no longer exposes `start_session` / `export_trajectories` (only `set_reward` remains, used by the surviving `reward` verb). The cli group, config bindings, cli_guide section, and parser tests are cleaned up accordingly. 3) Replace the bespoke `sglang:tp=2,dp=2` mini-DSL in `parse_backend_spec` with `ModelAllocation.from_str`, so the CLI accepts the same grammar as `InferenceEngineConfig.backend` in YAML configs (`sglang:d4`, `vllm:d2t4`). Help text, doc examples, and test fixtures are updated to the new form. 4) Collect the free functions in `state.py` (`models_dir`, `models_state_path`, `models_lock_path`, `locked_model_state`, `recover_pids_from_raw_state`) into an `InferenceStateStore` class and route every caller through a module-level `store` instance. The dataclasses now obtain paths via `store.<...>`, keeping on-disk-layout responsibilities in one place. Net: 13 files, +170 / -524 (mostly from dropping the collect verb). * style(tests): drop trailing blank lines after removed collect case * refactor(experimental): subclass scaffold NamespacedStateStore in inference scaffold's state.py promoted the namespace-aware free functions onto NamespacedStateStore. Update inference accordingly: - InferenceStateStore now subclasses NamespacedStateStore, gaining service_state_path / set_current_service / clear_current_service / current_service_path / resolve_service_name from the parent. It keeps the inf-specific models_*, lock_model_state, and overrides recover_pids_from_raw_state to walk both state files. - ServiceState.save / .remove route through ``store.set_current_service`` / ``store.clear_current_service`` instead of the deleted free functions; commands/run.py / register.py / tests resolve log paths via ``store.logs_dir`` and the service-state path via ``store.service_state_path``. Behavior unchanged. * style: sort v2 CLI imports * refactor(experimental): drop reward verb + RL session flow docs Address PR #1434 review: - Remove ``areal inf reward`` (commands/reward.py + the ``reward_cmd`` wiring in __init__.py) and the corresponding GatewayClient.set_reward RPC. The reward flow is server-side only for now; the CLI does not need to wrap it. - Drop the "RL session flow" and "Setting reward" sections from cli_guide.md plus the trailing "For plain inference there is no need to call /rl/start_session" note. - Drop the ``reward`` entry from the [default].service binding tuple in config.py and the reward-related cases in the parser test. --------- Co-authored-by: Claude Opus 4.7 --- areal/utils/logging.py | 2 + areal/v2/cli/cli.py | 2 + areal/v2/cli/inference/__init__.py | 41 +++ areal/v2/cli/inference/cli_guide.md | 177 +++++++++ areal/v2/cli/inference/client.py | 36 ++ areal/v2/cli/inference/commands/__init__.py | 1 + areal/v2/cli/inference/commands/deregister.py | 62 ++++ areal/v2/cli/inference/commands/models.py | 41 +++ areal/v2/cli/inference/commands/ps.py | 70 ++++ areal/v2/cli/inference/commands/register.py | 71 ++++ areal/v2/cli/inference/commands/run.py | 245 ++++++++++++ areal/v2/cli/inference/commands/status.py | 120 ++++++ areal/v2/cli/inference/commands/stop.py | 48 +++ areal/v2/cli/inference/common.py | 335 +++++++++++++++++ areal/v2/cli/inference/config.py | 66 ++++ areal/v2/cli/inference/launcher.py | 187 ++++++++++ areal/v2/cli/inference/lifecycle.py | 47 +++ areal/v2/cli/inference/scheduler/__init__.py | 21 ++ areal/v2/cli/inference/scheduler/base.py | 75 ++++ areal/v2/cli/inference/scheduler/local.py | 111 ++++++ areal/v2/cli/inference/state.py | 348 ++++++++++++++++++ tests/v2/cli/test_inference_cli_lifecycle.py | 165 +++++++++ tests/v2/cli/test_inference_cli_parser.py | 120 ++++++ tests/v2/cli/test_inference_cli_state.py | 164 +++++++++ 24 files changed, 2555 insertions(+) create mode 100644 areal/v2/cli/inference/__init__.py create mode 100644 areal/v2/cli/inference/cli_guide.md create mode 100644 areal/v2/cli/inference/client.py create mode 100644 areal/v2/cli/inference/commands/__init__.py create mode 100644 areal/v2/cli/inference/commands/deregister.py create mode 100644 areal/v2/cli/inference/commands/models.py create mode 100644 areal/v2/cli/inference/commands/ps.py create mode 100644 areal/v2/cli/inference/commands/register.py create mode 100644 areal/v2/cli/inference/commands/run.py create mode 100644 areal/v2/cli/inference/commands/status.py create mode 100644 areal/v2/cli/inference/commands/stop.py create mode 100644 areal/v2/cli/inference/common.py create mode 100644 areal/v2/cli/inference/config.py create mode 100644 areal/v2/cli/inference/launcher.py create mode 100644 areal/v2/cli/inference/lifecycle.py create mode 100644 areal/v2/cli/inference/scheduler/__init__.py create mode 100644 areal/v2/cli/inference/scheduler/base.py create mode 100644 areal/v2/cli/inference/scheduler/local.py create mode 100644 areal/v2/cli/inference/state.py create mode 100644 tests/v2/cli/test_inference_cli_lifecycle.py create mode 100644 tests/v2/cli/test_inference_cli_parser.py create mode 100644 tests/v2/cli/test_inference_cli_state.py diff --git a/areal/utils/logging.py b/areal/utils/logging.py index 1c67860138..d0aa166378 100644 --- a/areal/utils/logging.py +++ b/areal/utils/logging.py @@ -40,10 +40,12 @@ "LocalScheduler": "blue", "RayScheduler": "blue", "SlurmScheduler": "blue", + "InfLocalScheduler": "blue", # Launchers - blue "LocalLauncher": "blue", "RayLauncher": "blue", "SlurmLauncher": "blue", + "InfCli": "blue", # Workflows - purple "RLVRWorkflow": "light_purple", "VisionRLVRWorkflow": "light_purple", diff --git a/areal/v2/cli/cli.py b/areal/v2/cli/cli.py index be8584aef9..d40d39c121 100644 --- a/areal/v2/cli/cli.py +++ b/areal/v2/cli/cli.py @@ -5,6 +5,7 @@ import click from areal.v2.cli.agent import agent +from areal.v2.cli.inference import inf from areal.v2.cli.training import train from areal.version import __version__ @@ -19,4 +20,5 @@ def cli() -> None: cli.add_command(agent) +cli.add_command(inf) cli.add_command(train) diff --git a/areal/v2/cli/inference/__init__.py b/areal/v2/cli/inference/__init__.py new file mode 100644 index 0000000000..510e550c93 --- /dev/null +++ b/areal/v2/cli/inference/__init__.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path + +import click + +from areal.v2.cli.commands.logs import LogsCommand +from areal.v2.cli.inference.commands.deregister import deregister_cmd +from areal.v2.cli.inference.commands.models import models_cmd +from areal.v2.cli.inference.commands.ps import ps_cmd +from areal.v2.cli.inference.commands.register import register_cmd +from areal.v2.cli.inference.commands.run import run_cmd +from areal.v2.cli.inference.commands.status import status_cmd +from areal.v2.cli.inference.commands.stop import stop_cmd +from areal.v2.cli.inference.config import load_click_default_map +from areal.v2.cli.inference.lifecycle import inf_lifecycle + + +@click.group(help="Manage local AReaL inference services.") +@click.option( + "--config", + "config_file", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + default=None, + help="Extra TOML file merged on top of ~/.areal/inf/config.toml.", +) +@click.pass_context +def inf(ctx: click.Context, config_file: Path | None) -> None: + ctx.default_map = load_click_default_map(extra=config_file) + + +inf.add_command(run_cmd) +inf.add_command(stop_cmd) +inf.add_command(status_cmd) +inf.add_command(ps_cmd) +inf.add_command(register_cmd) +inf.add_command(deregister_cmd) +inf.add_command(models_cmd) +inf.add_command(LogsCommand(lifecycle=inf_lifecycle).build()) diff --git a/areal/v2/cli/inference/cli_guide.md b/areal/v2/cli/inference/cli_guide.md new file mode 100644 index 0000000000..5e5e9f078a --- /dev/null +++ b/areal/v2/cli/inference/cli_guide.md @@ -0,0 +1,177 @@ +# AReaL Inference Service CLI + +`areal inf` launches and manages an AReaL inference service on the local machine. It +starts the gateway/router, registers models, inspects service state, and manages logs. + +## Basic concepts + +An inference service typically contains the following components: + +- `gateway`: exposes OpenAI-compatible API and RL API to the outside. +- `router`: maintains the model → worker/data-proxy routing. +- `model worker`: the actual inference backend, e.g. SGLang. +- `data proxy`: records interactions and rewards, and supports trajectory export. + +The CLI's local state is stored under `~/.areal/inf/` by default. The root directory can +be overridden via `AREAL_HOME`: + +```bash +export AREAL_HOME=/path/to/areal-home +``` + +## Launching the service + +Launch an empty inference service: + +```bash +areal inf run \ + --service default \ + --host 127.0.0.1 \ + --port 8080 \ + --admin-api-key areal-admin-key \ + --scheduler local \ + --detach +``` + +`--scheduler` selects the scheduling backend for workers / data-proxies. Only `local` is +supported today (and is the default). Once the service starts, this value is pinned into +the service state, so subsequent `register` / `stop` / `status` calls read it from state +and do not need `--scheduler` again. + +List services known to the local machine: + +```bash +areal inf ps +areal inf status --service default +``` + +`ps` shows the service list; `status` drills into the state of the gateway, router, +data-proxy, workers, etc. + +List registered models: + +```bash +areal inf models --service default +``` + +## Registering a model + +`register` makes the CLI launch a local inference backend together with a data-proxy: + +```bash +areal inf register \ + --service default \ + --model-name qwen-local \ + --backend sglang:d1 \ + --model-path Qwen/Qwen2.5-7B-Instruct \ + --tokenizer-path Qwen/Qwen2.5-7B-Instruct \ + --engine-args "--mem-fraction-static 0.8" \ + --proxy-args "--request-timeout 120 --chat-template-type hf" +``` + +`--engine-args` is a shell-style string forwarded verbatim to the sglang / vllm worker +process; `--proxy-args` is the analogous flag for the data-proxy process. Available +data-proxy flags include `--request-timeout`, `--set-reward-finish-timeout`, +`--tool-call-parser`, `--reasoning-parser`, `--engine-max-tokens`, and +`--chat-template-type {hf|concat}`. + +A model can also be registered directly at `run` time: + +```bash +areal inf run \ + --service default \ + --port 8080 \ + --admin-api-key areal-admin-key \ + --model qwen-local \ + --backend sglang:d1 \ + --model-path Qwen/Qwen2.5-7B-Instruct \ + --engine-args "--mem-fraction-static 0.8" \ + --proxy-args "--request-timeout 120 --chat-template-type hf" \ + --detach +``` + +## Plain inference requests + +Once a model is registered, the gateway's OpenAI-compatible endpoint can be called +directly: + +```bash +curl -sS http://127.0.0.1:8080/v1/chat/completions \ + -H "Authorization: Bearer areal-admin-key" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "qwen-local", + "messages": [ + {"role": "user", "content": "Hi, give me a quick intro to AReaL."} + ], + "max_tokens": 128 + }' +``` + +## Logs and cleanup + +View logs: + +```bash +areal inf logs --service default --component gateway -f +areal inf logs --service default --component router -f +areal inf logs --service default --component qwen-local-worker-0 -f +areal inf logs --service default --component qwen-local-data-proxy-0 -f +``` + +Each model's worker / data-proxy log file is named `-worker-` and +`-data-proxy-`. If `--component` is wrong or the file does not exist, +the CLI prints the available names. + +Deregister a model: + +```bash +areal inf deregister --service default --model-name qwen-local +``` + +Stop the service: + +```bash +areal inf stop --service default +``` + +Force stop: + +```bash +areal inf stop --service default --force +``` + +## Configuration file + +`areal inf` reads a default config from: + +```bash +~/.areal/inf/config.toml +``` + +Additional config files can be passed in: + +```bash +areal inf --config ./inf.toml run --service default --detach +``` + +Example: + +```toml +[default] +service = "default" + +[launch] +gateway_host = "127.0.0.1" +gateway_port = 8080 +routing_strategy = "round_robin" + +[scheduler] +type = "local" + +[register.internal] +backend = "sglang:d1" +model_health_timeout = 600 +engine_args = "--mem-fraction-static 0.8" +proxy_args = "--request-timeout 120 --chat-template-type hf" +``` diff --git a/areal/v2/cli/inference/client.py b/areal/v2/cli/inference/client.py new file mode 100644 index 0000000000..3cb073bdec --- /dev/null +++ b/areal/v2/cli/inference/client.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any + +from areal.v2.cli.client import ( + BaseHTTPClient, + ServiceHTTPError, + ServiceUnreachable, +) + +# Legacy aliases — preserve the old names imported by inference subcommands +# (and downstream callers) so this swap is mechanical. New code should use +# the scaffold names directly. +GatewayHTTPError = ServiceHTTPError +GatewayUnreachable = ServiceUnreachable + + +class GatewayClient(BaseHTTPClient): + def list_models(self, *, timeout: float = 5.0) -> dict[str, Any]: + return self._get("/models", timeout=timeout) + + def register_model(self, payload: dict, *, timeout: float = 30.0) -> dict[str, Any]: + return self._post("/register_model", payload=payload, timeout=timeout) + + +class RouterClient(BaseHTTPClient): + def register_worker(self, addr: str, *, timeout: float = 10.0) -> dict[str, Any]: + return self._post("/register", payload={"worker_addr": addr}, timeout=timeout) + + def unregister_worker(self, addr: str, *, timeout: float = 10.0) -> dict[str, Any]: + return self._post("/unregister", payload={"worker_addr": addr}, timeout=timeout) + + def remove_model(self, name: str, *, timeout: float = 10.0) -> dict[str, Any]: + return self._post("/remove_model", payload={"name": name}, timeout=timeout) diff --git a/areal/v2/cli/inference/commands/__init__.py b/areal/v2/cli/inference/commands/__init__.py new file mode 100644 index 0000000000..9881313609 --- /dev/null +++ b/areal/v2/cli/inference/commands/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 diff --git a/areal/v2/cli/inference/commands/deregister.py b/areal/v2/cli/inference/commands/deregister.py new file mode 100644 index 0000000000..332ba7d5f3 --- /dev/null +++ b/areal/v2/cli/inference/commands/deregister.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.client import ServiceHTTPError, ServiceUnreachable +from areal.v2.cli.inference.client import RouterClient +from areal.v2.cli.inference.common import logger +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.process import kill_pids + + +@click.command(name="deregister", help="Deregister a model and tear down its workers.") +@click.option("--model-name", required=True, help="Model name to deregister.") +@click.option("--service", default=None, help="Target service instance.") +@click.option("--grace", type=float, default=10.0, show_default=True) +@click.option("--force", is_flag=True, help="SIGKILL workers immediately.") +def deregister_cmd( + model_name: str, service: str | None, grace: float, force: bool +) -> None: + raise SystemExit(do_deregister(model_name, grace, force, service=service) or 0) + + +def do_deregister( + model_name: str, grace: float, force: bool, *, service: str | None = None +) -> int: + state = inf_lifecycle.load_running_state(service) + if model_name not in state.models: + raise click.ClickException( + f"model {model_name!r} is not registered in service {state.service!r}" + ) + entry = state.models[model_name] + router = RouterClient(state.router_url, state.admin_api_key) + + try: + router.remove_model(model_name) + except ServiceHTTPError as exc: + if exc.status != 404: + logger.warning("router remove_model %s returned %d", model_name, exc.status) + except ServiceUnreachable as exc: + logger.warning("router unreachable while removing %s: %s", model_name, exc) + + # Router unregister → kill data-proxies → kill workers (same data-flow + # order as terminate_runtime_state). + for r in entry.replicas: + try: + router.unregister_worker(r.data_proxy.addr) + except (ServiceHTTPError, ServiceUnreachable) as exc: + logger.warning("router unregister %s failed: %s", r.data_proxy.addr, exc) + + proxy_pids = [r.data_proxy.pid for r in entry.replicas if r.data_proxy.pid > 0] + worker_pids = [r.worker.pid for r in entry.replicas if r.worker.pid > 0] + effective_grace = 0.0 if force else grace + for pids in (proxy_pids, worker_pids): + if pids: + kill_pids(pids, grace_s=effective_grace) + + del state.model_state.models[model_name] + state.model_state.save() + logger.info("deregistered model %r from service %r", model_name, state.service) + return 0 diff --git a/areal/v2/cli/inference/commands/models.py b/areal/v2/cli/inference/commands/models.py new file mode 100644 index 0000000000..4c44f2b313 --- /dev/null +++ b/areal/v2/cli/inference/commands/models.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import asdict + +import click + +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.utils import json_or_table + + +@click.command(name="models", help="List registered models.") +@click.option("--service", default=None, help="Target service instance.") +@click.option("--json", "as_json", is_flag=True, help="Emit JSON.") +def models_cmd(service: str | None, as_json: bool) -> None: + raise SystemExit(do_models(as_json, service=service) or 0) + + +def do_models(as_json: bool, *, service: str | None = None) -> int: + state = inf_lifecycle.running_state(service) + if state is None: + click.echo("[]" if as_json else "service not running") + return 0 + + payload = [{"name": name, **asdict(entry)} for name, entry in state.models.items()] + json_or_table(payload, as_json=as_json, table_renderer=_print_models_table) + return 0 + + +def _print_models_table(rows: list[dict]) -> None: + if not rows: + click.echo("no models registered") + return + cols = ("NAME", "BACKEND", "WORKERS") + table = [(row["name"], row["backend"], str(len(row["replicas"]))) for row in rows] + widths = [max(len(r[i]) for r in (cols, *table)) for i in range(len(cols))] + fmt = " ".join(f"{{:<{w}}}" for w in widths) + click.echo(fmt.format(*cols)) + for row in table: + click.echo(fmt.format(*row)) diff --git a/areal/v2/cli/inference/commands/ps.py b/areal/v2/cli/inference/commands/ps.py new file mode 100644 index 0000000000..16f003b0b4 --- /dev/null +++ b/areal/v2/cli/inference/commands/ps.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.utils import json_or_table + + +@click.command(name="ps", help="List locally known inference services.") +@click.option("--json", "as_json", is_flag=True, help="Emit JSON.") +@click.option("--all", "include_all", is_flag=True, help="Include stale services.") +def ps_cmd(as_json: bool, include_all: bool) -> None: + raise SystemExit(do_ps(as_json, include_all) or 0) + + +def do_ps(as_json: bool, include_all: bool) -> int: + rows: list[dict] = [] + for service in inf_lifecycle.list_services(): + try: + state = inf_lifecycle.load_state(service) + except Exception: + if include_all: + rows.append({"service": service, "status": "stale"}) + continue + running = state.gateway_alive() + if running or include_all: + rows.append( + { + "service": service, + "status": "running" if running else "stale", + "backend": state.backend, + "gateway_url": state.gateway_url, + "gateway_pid": state.gateway_pid, + "router_url": state.router_url, + "models": len(state.models), + } + ) + + json_or_table(rows, as_json=as_json, table_renderer=_print_table) + return 0 + + +def _print_table(rows: list[dict]) -> None: + if not rows: + click.echo("no inference services") + return + + def _pid_cell(row: dict) -> str: + pid = row.get("gateway_pid", 0) + return str(pid) if pid else "-" + + table = [ + ( + row["service"], + row["status"], + row.get("backend", "-"), + str(row.get("models", "")), + row.get("gateway_url", ""), + _pid_cell(row), + ) + for row in rows + ] + cols = ("SERVICE", "STATUS", "BACKEND", "MODELS", "GATEWAY", "PID") + widths = [max(len(str(r[i])) for r in (cols, *table)) for i in range(len(cols))] + fmt = " ".join(f"{{:<{w}}}" for w in widths) + click.echo(fmt.format(*cols)) + for row in table: + click.echo(fmt.format(*row)) diff --git a/areal/v2/cli/inference/commands/register.py b/areal/v2/cli/inference/commands/register.py new file mode 100644 index 0000000000..8961453433 --- /dev/null +++ b/areal/v2/cli/inference/commands/register.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.inference.client import GatewayClient, RouterClient +from areal.v2.cli.inference.common import ( + ENGINE_ARGS_HELP, + PROXY_ARGS_HELP, + logger, + register_model, +) +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.inference.state import store + + +@click.command(name="register", help="Register a model against a running service.") +@click.option("--model-name", required=True, help="Model name to register.") +@click.option("--service", default=None, help="Target service instance.") +@click.option( + "--backend", + default=None, + help="Backend spec; same grammar as InferenceEngineConfig.backend, " + "e.g. 'sglang:d4', 'vllm:d2t4'.", +) +@click.option("--model-path", default=None) +@click.option("--tokenizer-path", default=None) +@click.option("--engine-args", default="", help=ENGINE_ARGS_HELP) +@click.option("--proxy-args", default="", help=PROXY_ARGS_HELP) +@click.option("--model-health-timeout", type=float, default=600.0, show_default=True) +@click.option( + "--log-level", + type=click.Choice(["debug", "info", "warning", "error"]), + default="info", + show_default=True, +) +def register_cmd(model_name: str, service: str | None, **opts) -> None: + raise SystemExit(do_register(model_name, opts, service=service) or 0) + + +def do_register(model_name: str, opts: dict, *, service: str | None = None) -> int: + service_name = inf_lifecycle.resolve_service_name(service) + with store.lock_model_state(service_name): + state = inf_lifecycle.load_running_state(service_name) + if model_name in state.models: + raise click.ClickException( + f"model {model_name!r} already registered in service {state.service!r}" + ) + + entry = register_model( + model_name=model_name, + opts=opts, + gateway=GatewayClient(state.gateway_url, state.admin_api_key), + router=RouterClient(state.router_url, state.admin_api_key), + log_dir=store.logs_dir(state.service), + admin_api_key=state.admin_api_key, + scheduler_backend=state.backend, + occupied_gpus=state.model_state.occupied_gpus(), + ) + state.model_state.models[model_name] = entry + state.model_state.save() + gpus_used = sorted({g for r in entry.replicas for g in r.worker.gpu_devices}) + logger.info( + "registered model %r in service %r (%d replica(s), GPUs=%s)", + model_name, + state.service, + len(entry.replicas), + gpus_used, + ) + return 0 diff --git a/areal/v2/cli/inference/commands/run.py b/areal/v2/cli/inference/commands/run.py new file mode 100644 index 0000000000..5342abd2ad --- /dev/null +++ b/areal/v2/cli/inference/commands/run.py @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import time + +import click + +from areal.v2.cli.inference.client import GatewayClient, RouterClient +from areal.v2.cli.inference.common import ( + ENGINE_ARGS_HELP, + PROXY_ARGS_HELP, + logger, + register_model, + terminate_runtime_state, + validate_register_opts, +) +from areal.v2.cli.inference.launcher import spawn_gateway, spawn_router +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.inference.scheduler import TaskHandle +from areal.v2.cli.inference.state import ( + DEFAULT_SERVICE, + ModelState, + RuntimeState, + ServiceState, + store, +) +from areal.v2.cli.process import kill_pids +from areal.v2.cli.utils import wait_client_health +from areal.v2.cli.watcher import ForegroundWatcher + + +@click.command(name="run", help="Start an inference service (gateway + router).") +@click.option("--service", default=DEFAULT_SERVICE, show_default=True) +@click.option("--port", type=int, default=8080, show_default=True, help="Gateway port.") +@click.option( + "--host", default="127.0.0.1", show_default=True, help="Gateway bind host." +) +@click.option("--admin-api-key", default="areal-admin-key", show_default=True) +@click.option( + "--routing-strategy", + type=click.Choice(["round_robin", "least_busy"]), + default="round_robin", + show_default=True, +) +@click.option( + "--log-level", + type=click.Choice(["debug", "info", "warning", "error"]), + default="info", + show_default=True, +) +@click.option( + "--launch-timeout", + type=float, + default=30.0, + show_default=True, + help="Seconds to wait for gateway /health.", +) +@click.option("-d", "--detach", is_flag=True, help="Fork the daemon and exit.") +@click.option("--model", default=None, help="Register this model at startup.") +@click.option( + "--backend", + default=None, + help="Backend spec; same grammar as InferenceEngineConfig.backend, " + "e.g. 'sglang:d4', 'vllm:d2t4'.", +) +@click.option("--model-path", default=None) +@click.option("--tokenizer-path", default=None) +@click.option("--engine-args", default="", show_default=False, help=ENGINE_ARGS_HELP) +@click.option("--proxy-args", default="", show_default=False, help=PROXY_ARGS_HELP) +@click.option( + "--model-health-timeout", + type=float, + default=600.0, + show_default=True, + help="Seconds to wait for the model server to come up.", +) +@click.option( + "--scheduler", + type=click.Choice(["local"]), + default=None, + help=( + "Scheduler backend used to place workers and data-proxies. Pinned " + "for the service's lifetime — register / stop / status will read it " + "from saved state. Defaults to [scheduler].type in config.toml, " + "falling back to 'local'." + ), +) +@click.option("--force", is_flag=True, help="Replace stale or running service state.") +def run_cmd(**opts) -> None: + raise SystemExit(do_run(opts) or 0) + + +def do_run(opts: dict) -> int: + service = opts["service"] + if opts["force"]: + inf_lifecycle.force_replace_slot(service, grace_s=5.0) + else: + inf_lifecycle.refuse_if_running(service) + + if opts["model"]: + validate_register_opts(opts) + elif opts["backend"]: + raise click.UsageError("model registration flags require --model.") + + log_dir = store.logs_dir(service) + logger.info("starting inference service %r (logs: %s)", service, log_dir) + + router_pid, router_port = spawn_router( + host="127.0.0.1", + admin_api_key=opts["admin_api_key"], + routing_strategy=opts["routing_strategy"], + log_level=opts["log_level"], + log_file=log_dir / "router.log", + ) + router_url = f"http://127.0.0.1:{router_port}" + logger.info("router pid=%d %s", router_pid, router_url) + + time.sleep(0.3) + gateway_pid = spawn_gateway( + host=opts["host"], + port=opts["port"], + admin_api_key=opts["admin_api_key"], + router_url=router_url, + log_level=opts["log_level"], + log_file=log_dir / "gateway.log", + ) + host_for_url = "127.0.0.1" if opts["host"] in ("0.0.0.0", "::") else opts["host"] + gateway_url = f"http://{host_for_url}:{opts['port']}" + logger.info("gateway pid=%d %s", gateway_pid, gateway_url) + + gateway_client = GatewayClient(gateway_url, opts["admin_api_key"]) + router_client = RouterClient(router_url, opts["admin_api_key"]) + try: + wait_client_health( + gateway_client, timeout=opts["launch_timeout"], label="gateway" + ) + except BaseException: + kill_pids([gateway_pid, router_pid], grace_s=5.0) + raise + + backend = opts.get("scheduler") or "local" + + service_state = ServiceState( + service=service, + backend=backend, + gateway_handle=TaskHandle( + host=host_for_url, + ports=[opts["port"]], + gpu_devices=[], + ref={"pid": gateway_pid}, + ), + router_handle=TaskHandle( + host="127.0.0.1", + ports=[router_port], + gpu_devices=[], + ref={"pid": router_pid}, + ), + admin_api_key=opts["admin_api_key"], + started_at=time.time(), + ) + model_state = ModelState(service=service) + with store.lock_model_state(service): + service_state.save() + model_state.save() + + if opts["model"]: + try: + entry = register_model( + model_name=opts["model"], + opts=opts, + gateway=gateway_client, + router=router_client, + log_dir=log_dir, + admin_api_key=opts["admin_api_key"], + scheduler_backend=backend, + occupied_gpus=model_state.occupied_gpus(), + ) + model_state.models[opts["model"]] = entry + model_state.save() + except BaseException: + _cleanup_runtime(service, service_state, model_state, grace_s=5.0) + raise + + logger.info("service %r ready pid=%d url=%s", service, gateway_pid, gateway_url) + if opts["model"]: + logger.info("default model: %s (%s)", opts["model"], opts["backend"]) + + if opts["detach"]: + return 0 + + logger.info( + "foreground watcher running. Ctrl+C / SIGTERM tears down the service; " + "closing the terminal (SIGHUP) detaches the watcher and leaves it " + "running — use `areal inf stop --service %s` to terminate.", + service, + ) + watcher = ForegroundWatcher( + is_alive=lambda: inf_lifecycle.gateway_alive( + RuntimeState(service_state=service_state, model_state=model_state) + ), + teardown=lambda: _cleanup_runtime( + service, service_state, model_state, grace_s=10.0 + ), + service_name=service, + ) + rc = watcher.watch() + # gateway died externally (is_alive flipped to False on its own) — the + # children are gone, just clean the state files. + if rc == 0 and not inf_lifecycle.gateway_alive( + RuntimeState(service_state=service_state, model_state=model_state) + ): + ServiceState.remove(service) + ModelState.remove(service) + return rc + + +def _cleanup_runtime( + service: str, + service_state: ServiceState, + model_state: ModelState, + *, + grace_s: float, +) -> None: + model_state = _latest_model_state_for_cleanup(service, model_state) + try: + terminate_runtime_state( + RuntimeState(service_state=service_state, model_state=model_state), + grace_s=grace_s, + ) + finally: + ServiceState.remove(service) + ModelState.remove(service) + + +def _latest_model_state_for_cleanup( + service: str, + fallback: ModelState, +) -> ModelState: + try: + latest = ModelState.load(service) + except Exception: + return fallback + latest.models.update(fallback.models) + return latest diff --git a/areal/v2/cli/inference/commands/status.py b/areal/v2/cli/inference/commands/status.py new file mode 100644 index 0000000000..ea9e691bd4 --- /dev/null +++ b/areal/v2/cli/inference/commands/status.py @@ -0,0 +1,120 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +from dataclasses import asdict + +import click + +from areal.v2.cli.inference.common import ( + format_gpu_count, + format_placement, + format_ref, +) +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.inference.scheduler import TaskHandle +from areal.v2.cli.inference.state import RuntimeState +from areal.v2.cli.state import SupportsComponentProbe +from areal.v2.cli.status import ColumnSpec, StatusReporter + + +@click.command( + name="status", + help="Show inference service status — per-component drill-down for one service.", +) +@click.option("--service", default=None, help="Target service instance.") +@click.option("--json", "as_json", is_flag=True, help="Emit JSON.") +def status_cmd(service: str | None, as_json: bool) -> None: + raise SystemExit(do_status(as_json, service=service) or 0) + + +def do_status(as_json: bool, *, service: str | None = None) -> int: + service_name = inf_lifecycle.resolve_service_name(service) + if not inf_lifecycle.state_path(service_name).exists(): + if as_json: + click.echo( + json.dumps({"service": service_name, "running": False}, indent=2) + ) + else: + click.echo(f"service {service_name!r} not running") + return 0 + + try: + state = inf_lifecycle.load_state(service_name) + except Exception as exc: + raise click.ClickException(f"failed to load state: {exc}") from exc + + components = list(state.components()) + reporter = StatusReporter(components, _columns(state.backend)) + alive = reporter.probe_all() + + if as_json: + click.echo(json.dumps(_json_snapshot(state, reporter, alive), indent=2)) + return 0 + + gateway_addr = state.gateway_handle.addr or "-" + reporter.print_table( + reporter.render_rows(alive), + header_line=( + f"service: {state.service} " + f"backend: {state.backend} " + f"gateway: {gateway_addr} " + f"models: {len(state.models)} " + f"running: {'yes' if state.gateway_alive() else 'no'}" + ), + ) + return 0 + + +def _columns(backend: str) -> list[ColumnSpec]: + def _component(label: str, _: SupportsComponentProbe, __: bool) -> str: + return label + + def _placement(_: str, handle: SupportsComponentProbe, __: bool) -> str: + # SupportsComponentProbe doesn't promise the TaskHandle surface, + # but in practice every inf component handle IS a TaskHandle. + return format_placement(backend, handle) or "-" # type: ignore[arg-type] + + def _gpus(_: str, handle: SupportsComponentProbe, __: bool) -> str: + return format_gpu_count(handle) # type: ignore[arg-type] + + def _addr(_: str, handle: SupportsComponentProbe, __: bool) -> str: + return handle.addr or "-" + + def _ref(_: str, handle: SupportsComponentProbe, __: bool) -> str: + return format_ref(backend, handle) # type: ignore[arg-type] + + def _alive(_: str, __: SupportsComponentProbe, alive: bool) -> str: + return "yes" if alive else "no" + + return [ + ColumnSpec("COMPONENT", _component), + ColumnSpec("PLACEMENT", _placement), + ColumnSpec("GPUS", _gpus), + ColumnSpec("ADDR", _addr), + ColumnSpec("REF", _ref), + ColumnSpec("ALIVE", _alive), + ] + + +def _json_snapshot( + state: RuntimeState, reporter: StatusReporter, alive: list[bool] +) -> dict: + handle_pairs: list[tuple[str, TaskHandle]] = list(state.components()) # type: ignore[arg-type] + rows = [ + {**snap, "label": label} + for snap, (label, _) in zip( + reporter.json_snapshot(alive), handle_pairs, strict=True + ) + ] + return { + "service": state.service, + "backend": state.backend, + "running": state.gateway_alive(), + "gateway_handle": asdict(state.gateway_handle), + "router_handle": asdict(state.router_handle), + "started_at": state.started_at, + "models": {name: asdict(entry) for name, entry in state.models.items()}, + "components": rows, + } diff --git a/areal/v2/cli/inference/commands/stop.py b/areal/v2/cli/inference/commands/stop.py new file mode 100644 index 0000000000..88d0d78244 --- /dev/null +++ b/areal/v2/cli/inference/commands/stop.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import click + +from areal.v2.cli.inference.common import logger, terminate_runtime_state +from areal.v2.cli.inference.lifecycle import inf_lifecycle +from areal.v2.cli.inference.state import ModelState, ServiceState + + +@click.command(name="stop", help="Stop an inference service.") +@click.option("--service", default=None, help="Target service instance.") +@click.option("--grace", type=float, default=10.0, show_default=True) +@click.option("--force", is_flag=True, help="SIGKILL immediately.") +@click.option("--keep-state", is_flag=True, help="Keep service state after stopping.") +def stop_cmd(service: str | None, grace: float, force: bool, keep_state: bool) -> None: + raise SystemExit(do_stop(grace, force, service=service, keep_state=keep_state) or 0) + + +def do_stop( + grace: float, + force: bool, + *, + service: str | None = None, + keep_state: bool = False, +) -> int: + service_name = inf_lifecycle.resolve_service_name(service) + if not inf_lifecycle.state_path(service_name).exists(): + click.echo(f"service {service_name!r} not running") + return 0 + + try: + state = inf_lifecycle.load_state(service_name) + except Exception: + logger.warning("stale state; removing") + if not keep_state: + ServiceState.remove(service_name) + ModelState.remove(service_name) + return 0 + + terminate_runtime_state(state, grace_s=grace, force=force) + + if not keep_state: + ServiceState.remove(service_name) + ModelState.remove(service_name) + click.echo(f"service {service_name!r} stopped") + return 0 diff --git a/areal/v2/cli/inference/common.py b/areal/v2/cli/inference/common.py new file mode 100644 index 0000000000..72b9c3d203 --- /dev/null +++ b/areal/v2/cli/inference/common.py @@ -0,0 +1,335 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Domain-specific helpers for the inference CLI. + +Lifecycle predicates, HTTP polling, output dispatch, and logger setup +live in the shared scaffold (`areal.v2.cli.lifecycle`, +`.utils`, `.client`, `.status`). This module keeps only the +inference-specific pieces: backend spec parsing, model registration, +and TaskHandle column formatters. +""" + +from __future__ import annotations + +from pathlib import Path + +import click + +from areal.api.alloc_mode import ModelAllocation +from areal.v2.cli.client import ServiceHTTPError, ServiceUnreachable +from areal.v2.cli.inference.client import GatewayClient, RouterClient +from areal.v2.cli.inference.launcher import ( + build_data_proxy_task_spec, + build_sglang_task_spec, + build_vllm_task_spec, +) +from areal.v2.cli.inference.scheduler import ( + Scheduler, + TaskHandle, + build_scheduler, +) +from areal.v2.cli.inference.state import ( + ModelEntry, + ModelReplica, + RuntimeState, +) +from areal.v2.cli.process import kill_pids +from areal.v2.cli.utils import register_cli_logger, wait_http_health + +logger = register_cli_logger("InfCli") + + +ENGINE_ARGS_HELP = ( + "Shell-style string forwarded verbatim to the sglang / vllm process. " + "Common sglang knobs: --mem-fraction-static 0.85, " + "--max-running-requests 256, --chunked-prefill-size 4096, " + "--disable-radix-cache, --enable-torch-compile. " + "See the sglang / vllm CLI docs for the full surface." +) + +PROXY_ARGS_HELP = ( + "Shell-style string forwarded verbatim to the data-proxy process. " + "Available flags: --request-timeout, --set-reward-finish-timeout, " + "--tool-call-parser, --reasoning-parser, --engine-max-tokens, " + "--chat-template-type {hf|concat}." +) + + +def parse_backend_spec(spec: str) -> tuple[str, int, int, int]: + """Parse a backend spec into ``(engine, tp, dp, pp)``. + + Delegates to ``ModelAllocation.from_str`` so the CLI accepts the same + grammar as ``InferenceEngineConfig.backend`` in YAML configs — + ``"sglang:d4"``, ``"vllm:d2t4"``, etc. The CLI restricts the engine + set to the two inference backends it can spawn locally. + """ + + try: + alloc = ModelAllocation.from_str(spec) + except Exception as exc: + raise click.BadParameter(f"invalid --backend spec {spec!r}: {exc}") from exc + if alloc.backend not in ("sglang", "vllm"): + raise click.BadParameter( + f"backend must be one of: sglang, vllm; got {alloc.backend!r}" + ) + return ( + alloc.backend, + alloc.parallel.tensor_parallel_size, + alloc.parallel.data_parallel_size, + alloc.parallel.pipeline_parallel_size, + ) + + +def split_args(value: str) -> list[str]: + import shlex + + return shlex.split(value) if value else [] + + +def terminate_runtime_state( + state: RuntimeState, *, grace_s: float, force: bool = False +) -> None: + """Tear down the whole inference service in data-flow order: kill the + data-proxies first (so in-flight requests fail at the proxy boundary + rather than mid-worker), then workers, then gateway, then router. + + ``force=True`` skips the grace period entirely — equivalent to + ``kill_pids(pids, grace_s=0)`` for every phase. + """ + + def _pids(handles: list[TaskHandle]) -> list[int]: + return [h.pid for h in handles if h.pid > 0] + + phases = ( + _pids(state.model_state.all_data_proxies()), + _pids(state.model_state.all_workers()), + _pids([state.gateway_handle]), + _pids([state.router_handle]), + ) + effective_grace = 0.0 if force else grace_s + for pids in phases: + if not pids: + continue + kill_pids(pids, grace_s=effective_grace) + + +def format_placement(backend: str, handle: TaskHandle) -> str: + if backend == "local": + import socket + + return socket.gethostname() + if backend == "k8s": + node = handle.ref.get("node", "?") + pod = handle.ref.get("pod_name", "?") + return f"{node}/{pod}" + if backend == "slurm": + node = handle.ref.get("node", "?") + job = handle.ref.get("job_id", "?") + return f"{node}/job={job}" + return "-" + + +def format_ref(backend: str, handle: TaskHandle) -> str: + if backend == "local": + return f"pid={handle.pid}" if handle.pid > 0 else "-" + if backend == "k8s": + pod = handle.ref.get("pod_name", "") + return f"pod={pod}" if pod else "-" + if backend == "slurm": + job = handle.ref.get("job_id", "") + return f"job={job}" if job else "-" + return "-" + + +def format_gpu_count(handle: TaskHandle) -> str: + n = len(handle.gpu_devices) + return f"×{n}" if n > 0 else "-" + + +def register_internal( + *, + model: str, + backend: str, + model_path: str, + tokenizer_path: str, + engine_extra: list[str], + proxy_extra: list[str], + model_health_timeout: float, + log_level: str, + admin_api_key: str, + gateway: GatewayClient, + router: RouterClient, + log_dir: Path, + scheduler: Scheduler, +) -> list[ModelReplica]: + engine, tp, dp, pp = parse_backend_spec(backend) + if pp > 1: + raise click.ClickException( + "pp > 1 is not supported by `areal inf` (single-node only)." + ) + + replicas: list[ModelReplica] = [] + spawned_handles: list[TaskHandle] = [] + + try: + for rank in range(dp): + worker_log = log_dir / f"{model}-worker-{rank}.log" + if engine == "sglang": + worker_spec = build_sglang_task_spec( + name=f"worker/{model}/{rank}", + model_path=model_path, + tp=tp, + extra_args=engine_extra, + log_file=worker_log, + ) + else: + worker_spec = build_vllm_task_spec( + name=f"worker/{model}/{rank}", + model_path=model_path, + tp=tp, + pp=pp, + extra_args=engine_extra, + log_file=worker_log, + ) + worker_handle = scheduler.submit(worker_spec) + spawned_handles.append(worker_handle) + logger.info( + "spawned %s worker %d/%d pid=%d port=%d gpus=%s", + engine, + rank, + dp, + worker_handle.pid, + worker_handle.ports[0], + worker_handle.gpu_devices, + ) + wait_http_health( + worker_handle.addr, + pid=worker_handle.pid, + timeout=model_health_timeout, + label=f"{engine} worker {rank}", + poll_interval=1.0, + ) + + proxy_log = log_dir / f"{model}-data-proxy-{rank}.log" + proxy_spec = build_data_proxy_task_spec( + name=f"data_proxy/{model}/{rank}", + backend_addr=worker_handle.addr, + backend_type=engine, + tokenizer_path=tokenizer_path, + admin_api_key=admin_api_key, + log_level=log_level, + extra_args=proxy_extra, + log_file=proxy_log, + ) + proxy_handle = scheduler.submit(proxy_spec) + spawned_handles.append(proxy_handle) + logger.info( + "spawned data-proxy %d/%d pid=%d port=%d", + rank, + dp, + proxy_handle.pid, + proxy_handle.ports[0], + ) + wait_http_health( + proxy_handle.addr, + pid=proxy_handle.pid, + timeout=30.0, + label=f"data-proxy {rank}", + poll_interval=1.0, + ) + + replicas.append(ModelReplica(data_proxy=proxy_handle, worker=worker_handle)) + + proxy_addrs = [r.data_proxy.addr for r in replicas] + for addr in proxy_addrs: + try: + router.register_worker(addr) + except (ServiceUnreachable, ServiceHTTPError) as exc: + raise click.ClickException( + f"router register_worker {addr} failed: {exc}" + ) from exc + + try: + gateway.register_model( + { + "model": model, + "url": "", + "api_key": "", + "data_proxy_addrs": proxy_addrs, + } + ) + except (ServiceUnreachable, ServiceHTTPError) as exc: + raise click.ClickException(f"gateway register_model failed: {exc}") from exc + + except BaseException: + if spawned_handles: + logger.error( + "internal register failed; killing %d spawned worker(s)", + len(spawned_handles), + ) + pids = [h.pid for h in spawned_handles if h.pid > 0] + kill_pids(pids, grace_s=10.0) + raise + + return replicas + + +def validate_register_opts(opts: dict) -> None: + if not opts.get("backend"): + raise click.UsageError("--backend is required.") + if not opts.get("model_path"): + raise click.UsageError("--backend requires --model-path .") + + +def register_model( + *, + model_name: str, + opts: dict, + gateway: GatewayClient, + router: RouterClient, + log_dir: Path, + admin_api_key: str, + scheduler_backend: str, + occupied_gpus: set[int], +) -> ModelEntry: + """Validate registration opts and spawn the model's worker + data-proxy + fleet. The returned ModelEntry is meant to be slotted into model_state + by the caller — this helper does not touch model_state itself so the + caller can keep the file lock scope explicit.""" + + validate_register_opts(opts) + scheduler = build_scheduler(scheduler_backend, occupied_gpus=occupied_gpus) + replicas = register_internal( + model=model_name, + backend=opts["backend"], + model_path=opts["model_path"], + tokenizer_path=opts.get("tokenizer_path") or opts["model_path"], + engine_extra=split_args(opts.get("engine_args", "")), + proxy_extra=split_args(opts.get("proxy_args", "")), + model_health_timeout=opts.get("model_health_timeout", 600.0), + log_level=opts.get("log_level", "info"), + admin_api_key=admin_api_key, + gateway=gateway, + router=router, + log_dir=log_dir, + scheduler=scheduler, + ) + return ModelEntry(backend=opts["backend"], replicas=replicas) + + +# Imports kept at bottom: these are stdlib re-exports preserved for +# downstream callers that still import them via this module. +__all__ = [ + "ENGINE_ARGS_HELP", + "PROXY_ARGS_HELP", + "format_gpu_count", + "format_placement", + "format_ref", + "logger", + "parse_backend_spec", + "register_internal", + "register_model", + "split_args", + "terminate_runtime_state", + "validate_register_opts", +] diff --git a/areal/v2/cli/inference/config.py b/areal/v2/cli/inference/config.py new file mode 100644 index 0000000000..d28d325bb9 --- /dev/null +++ b/areal/v2/cli/inference/config.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""User config loader for ``areal inf``. + +~/.areal/inf/config.toml overrides built-in CLI defaults. Sections map +to verbs: + + [default] admin_api_key, log_level (applied to all verbs) + [launch] run-time gateway / router / strategy / timeouts + [scheduler] scheduler backend selection (applied to run only) + [register.internal] register / inline-register defaults + +Precedence (highest first): + 1. explicit CLI flag + 2. config.toml + 3. hard-coded default in the click @option + +Missing / malformed file is treated as empty — never crashes the CLI. +""" + +from __future__ import annotations + +from pathlib import Path + +from areal.v2.cli.config import BindingMap, ConfigLoader +from areal.v2.cli.inference.state import INF_NAMESPACE + +INF_BINDINGS: BindingMap = { + # [default] + ( + "default", + "service", + ): ( + ( + "run", + "stop", + "status", + "register", + "deregister", + "models", + "logs", + ), + "service", + ), + ("default", "admin_api_key"): ("run", "admin_api_key"), + ("default", "log_level"): ("run", "log_level"), + # [launch] — applied to `run` + ("launch", "gateway_host"): ("run", "host"), + ("launch", "gateway_port"): ("run", "port"), + ("launch", "routing_strategy"): ("run", "routing_strategy"), + ("launch", "launch_timeout"): ("run", "launch_timeout"), + # [scheduler] — applied to `run` only; other verbs read from ServiceState + ("scheduler", "type"): ("run", "scheduler"), + # [register.internal] — applied to `register` + ("register.internal", "backend"): ("register", "backend"), + ("register.internal", "model_health_timeout"): ("register", "model_health_timeout"), + ("register.internal", "engine_args"): ("register", "engine_args"), + ("register.internal", "proxy_args"): ("register", "proxy_args"), +} + + +inf_config_loader = ConfigLoader(namespace=INF_NAMESPACE, bindings=INF_BINDINGS) + + +def load_click_default_map(extra: Path | None = None) -> dict: + return inf_config_loader.load_click_default_map(extra=extra) diff --git a/areal/v2/cli/inference/launcher.py b/areal/v2/cli/inference/launcher.py new file mode 100644 index 0000000000..19a104a4db --- /dev/null +++ b/areal/v2/cli/inference/launcher.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path + +from areal.utils.network import find_free_ports +from areal.v2.cli.inference.scheduler import ( + TaskAllocation, + TaskResources, + TaskSpec, +) +from areal.v2.cli.process import spawn_process + +# Gateway and router are service-level singletons spawned directly via +# spawn_process — not submitted to a Scheduler — so their lifecycle is +# anchored to the CLI's state files regardless of scheduler backend. + + +def spawn_router( + *, + host: str, + admin_api_key: str, + routing_strategy: str, + log_level: str, + log_file: Path, +) -> tuple[int, int]: + port = find_free_ports(1)[0] + cmd = [ + sys.executable, + "-m", + "areal.v2.inference_service.router", + "--host", + host, + "--port", + str(port), + "--admin-api-key", + admin_api_key, + "--routing-strategy", + routing_strategy, + "--log-level", + log_level, + ] + return spawn_process(cmd, log_file), port + + +def spawn_gateway( + *, + host: str, + port: int, + admin_api_key: str, + router_url: str, + log_level: str, + log_file: Path, +) -> int: + cmd = [ + sys.executable, + "-m", + "areal.v2.inference_service.gateway", + "--host", + host, + "--port", + str(port), + "--admin-api-key", + admin_api_key, + "--router-addr", + router_url, + "--log-level", + log_level, + ] + return spawn_process(cmd, log_file) + + +# sglang / vllm builders always pass base_gpu_id=0; the scheduler masks +# physical GPUs via CUDA_VISIBLE_DEVICES so the launched process sees +# its devices as 0..N-1. + + +def build_sglang_task_spec( + *, + name: str, + model_path: str, + tp: int, + extra_args: list[str], + log_file: Path, +) -> TaskSpec: + def cmd_builder(alloc: TaskAllocation) -> list[str]: + from areal.api.cli_args import SGLangConfig + + cfg = SGLangConfig(model_path=model_path, log_requests=True) + cmd = list( + SGLangConfig.build_cmd( + sglang_config=cfg, + tp_size=tp, + base_gpu_id=0, + host=alloc.host, + port=alloc.ports[0], + n_nodes=1, + node_rank=0, + pp_size=1, + ) + ) + cmd.extend(extra_args) + return cmd + + return TaskSpec( + name=name, + cmd_builder=cmd_builder, + log_file=log_file, + resources=TaskResources(gpu=tp, ports=1), + ) + + +def build_vllm_task_spec( + *, + name: str, + model_path: str, + tp: int, + pp: int, + extra_args: list[str], + log_file: Path, +) -> TaskSpec: + def cmd_builder(alloc: TaskAllocation) -> list[str]: + from areal.api.cli_args import vLLMConfig + + cfg = vLLMConfig(model=model_path) + cmd = list( + vLLMConfig.build_cmd( + vllm_config=cfg, + tp_size=tp, + pp_size=pp, + host=alloc.host, + port=alloc.ports[0], + ) + ) + cmd.extend(extra_args) + return cmd + + return TaskSpec( + name=name, + cmd_builder=cmd_builder, + log_file=log_file, + resources=TaskResources(gpu=tp * pp, ports=1), + ) + + +def build_data_proxy_task_spec( + *, + name: str, + backend_addr: str, + backend_type: str, + tokenizer_path: str, + admin_api_key: str, + log_level: str, + extra_args: list[str], + log_file: Path, +) -> TaskSpec: + def cmd_builder(alloc: TaskAllocation) -> list[str]: + cmd = [ + sys.executable, + "-m", + "areal.v2.inference_service.data_proxy", + "--host", + alloc.host, + "--port", + str(alloc.ports[0]), + "--backend-addr", + backend_addr, + "--backend-type", + backend_type, + "--tokenizer-path", + tokenizer_path, + "--admin-api-key", + admin_api_key, + "--log-level", + log_level, + ] + cmd.extend(extra_args) + return cmd + + return TaskSpec( + name=name, + cmd_builder=cmd_builder, + log_file=log_file, + resources=TaskResources(gpu=0, ports=1), + ) diff --git a/areal/v2/cli/inference/lifecycle.py b/areal/v2/cli/inference/lifecycle.py new file mode 100644 index 0000000000..4856600374 --- /dev/null +++ b/areal/v2/cli/inference/lifecycle.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from areal.v2.cli.inference.state import ( + INF_NAMESPACE, + ModelState, + RuntimeState, + ServiceState, + store, +) +from areal.v2.cli.lifecycle import ServiceLifecycle +from areal.v2.cli.process import kill_pids + + +class InferenceLifecycle(ServiceLifecycle): + """Adds the inf-specific behavior on top of scaffold's lifecycle: + + - State spans two files (service + model registry). ``force_replace_slot`` + walks the inf raw-JSON helper which knows about both files, and + removes both on cleanup. + """ + + def force_replace_slot(self, service: str, *, grace_s: float = 5.0) -> None: + path = self.state_path(service) + if not path.exists(): + return + pids: list[int] = [] + try: + state = self.load_state(service) + pids = self._collect_pids(state) + except Exception: + try: + pids = store.recover_pids_from_raw_state(service) + except Exception: + pids = [] + if pids: + kill_pids(pids, grace_s=grace_s) + ServiceState.remove(service) + ModelState.remove(service) + + +inf_lifecycle = InferenceLifecycle( + namespace=INF_NAMESPACE, + state_class=RuntimeState, + stop_command="areal inf stop", +) diff --git a/areal/v2/cli/inference/scheduler/__init__.py b/areal/v2/cli/inference/scheduler/__init__.py new file mode 100644 index 0000000000..49c8d5cbb5 --- /dev/null +++ b/areal/v2/cli/inference/scheduler/__init__.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 + +from areal.v2.cli.inference.scheduler.base import ( + Scheduler, + SchedulerError, + TaskAllocation, + TaskHandle, + TaskResources, + TaskSpec, + build_scheduler, +) + +__all__ = [ + "Scheduler", + "SchedulerError", + "TaskAllocation", + "TaskHandle", + "TaskResources", + "TaskSpec", + "build_scheduler", +] diff --git a/areal/v2/cli/inference/scheduler/base.py b/areal/v2/cli/inference/scheduler/base.py new file mode 100644 index 0000000000..727fd37d9c --- /dev/null +++ b/areal/v2/cli/inference/scheduler/base.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable +from dataclasses import dataclass, field +from pathlib import Path + + +class SchedulerError(Exception): + pass + + +@dataclass +class TaskResources: + gpu: int = 0 + ports: int = 1 + + +@dataclass +class TaskAllocation: + host: str + ports: list[int] + gpu_devices: list[int] + + +@dataclass +class TaskSpec: + """cmd_builder receives the TaskAllocation so host/port/GPU can be filled + in at placement time — required for K8s/Slurm where the host is only + known after scheduling.""" + + name: str + cmd_builder: Callable[[TaskAllocation], list[str]] + log_file: Path + resources: TaskResources = field(default_factory=TaskResources) + env: dict[str, str] = field(default_factory=dict) + + +@dataclass +class TaskHandle: + """Backend-agnostic identity of a running task. + + ``ref`` carries backend-specific identity payload — ``{"pid": int}`` for + Local, ``{"pod_name", "namespace"}`` for K8s, ``{"job_id"}`` for Slurm. + """ + + host: str + ports: list[int] + gpu_devices: list[int] = field(default_factory=list) + ref: dict = field(default_factory=dict) + + @property + def addr(self) -> str: + if not self.host or not self.ports: + return "" + return f"http://{self.host}:{self.ports[0]}" + + @property + def pid(self) -> int: + return int(self.ref.get("pid", 0) or 0) + + +class Scheduler(ABC): + @abstractmethod + def submit(self, spec: TaskSpec) -> TaskHandle: ... + + +def build_scheduler(backend: str, **kwargs) -> Scheduler: + if backend == "local": + from areal.v2.cli.inference.scheduler.local import LocalScheduler + + return LocalScheduler(**kwargs) + raise SchedulerError(f"unknown scheduler backend: {backend!r}") diff --git a/areal/v2/cli/inference/scheduler/local.py b/areal/v2/cli/inference/scheduler/local.py new file mode 100644 index 0000000000..3a2c2dfc60 --- /dev/null +++ b/areal/v2/cli/inference/scheduler/local.py @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os + +from areal.utils.logging import getLogger +from areal.utils.network import find_free_ports +from areal.v2.cli.inference.scheduler.base import ( + Scheduler, + SchedulerError, + TaskAllocation, + TaskHandle, + TaskSpec, +) +from areal.v2.cli.process import spawn_process + +logger = getLogger("InfLocalScheduler") + + +def _detect_gpus() -> list[int]: + vis = os.environ.get("CUDA_VISIBLE_DEVICES") + if vis: + try: + return [int(x) for x in vis.split(",") if x.strip()] + except ValueError: + logger.warning( + "ignoring malformed CUDA_VISIBLE_DEVICES=%r, falling back to detection", + vis, + ) + try: + nvidia = [ + d + for d in os.listdir("/dev") + if d.startswith("nvidia") and d[len("nvidia") :].isdigit() + ] + except OSError: + return [0] + return sorted(int(d[len("nvidia") :]) for d in nvidia) or [0] + + +class LocalScheduler(Scheduler): + """Ephemeral local subprocess scheduler for ``areal inf``. + + The instance is discarded after each CLI command — Popen handles are + not retained, only the PID written into the returned TaskHandle. GPUs + are masked via ``CUDA_VISIBLE_DEVICES`` so the spawned process always + sees its devices as ``0..N-1``, matching K8s device-plugin semantics. + """ + + def __init__( + self, + *, + all_gpus: list[int] | None = None, + occupied_gpus: set[int] | None = None, + host: str = "127.0.0.1", + ) -> None: + self.all_gpus = list(all_gpus) if all_gpus is not None else _detect_gpus() + self.occupied_gpus: set[int] = set(occupied_gpus or ()) + self.host = host + # Ports already handed out by this scheduler instance — passed as + # exclude_ports so the next find_free_ports call cannot return the + # same port between subsequent submits within one CLI command. + self._allocated_ports: set[int] = set() + + def submit(self, spec: TaskSpec) -> TaskHandle: + gpus = self._allocate_gpus(spec.resources.gpu) + ports = self._allocate_ports(spec.resources.ports) + alloc = TaskAllocation(host=self.host, ports=ports, gpu_devices=gpus) + cmd = spec.cmd_builder(alloc) + + env = dict(spec.env) + if gpus: + env["CUDA_VISIBLE_DEVICES"] = ",".join(str(g) for g in gpus) + + try: + pid = spawn_process(cmd, spec.log_file, env=env) + except OSError as exc: + raise SchedulerError(f"failed to spawn task {spec.name!r}: {exc}") from exc + + self.occupied_gpus.update(gpus) + logger.info( + "submitted %s pid=%d host=%s ports=%s gpus=%s", + spec.name, + pid, + alloc.host, + ports, + gpus, + ) + return TaskHandle( + host=alloc.host, + ports=ports, + gpu_devices=list(gpus), + ref={"pid": pid}, + ) + + def _allocate_gpus(self, n: int) -> list[int]: + if n <= 0: + return [] + free = [g for g in self.all_gpus if g not in self.occupied_gpus] + if len(free) < n: + raise SchedulerError( + f"need {n} GPUs but only {len(free)} free " + f"(total={self.all_gpus}, occupied={sorted(self.occupied_gpus)})" + ) + return free[:n] + + def _allocate_ports(self, n: int) -> list[int]: + ports = find_free_ports(n, exclude_ports=self._allocated_ports) + self._allocated_ports.update(ports) + return ports diff --git a/areal/v2/cli/inference/state.py b/areal/v2/cli/inference/state.py new file mode 100644 index 0000000000..21d0f0d212 --- /dev/null +++ b/areal/v2/cli/inference/state.py @@ -0,0 +1,348 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +from collections.abc import Iterable, Iterator +from contextlib import contextmanager +from dataclasses import asdict, dataclass, field +from pathlib import Path + +from areal.v2.cli.inference.scheduler import TaskHandle +from areal.v2.cli.process import pid_alive +from areal.v2.cli.state import ( + DEFAULT_SERVICE, + NamespacedStateStore, + ServiceStateBase, + SupportsComponentProbe, + atomic_write_json, +) +from areal.v2.cli.utils import file_lock + +INF_NAMESPACE = "inf" + + +class InferenceStateStore(NamespacedStateStore): + """Inference-specific extension of the scaffold ``NamespacedStateStore``. + + The inference service splits its state across two files per service — + the service-state (gateway / router handles, inherited from + scaffold) and the model-state (model registry, locked for + register / deregister). This class adds the model-state file paths + + lock + overrides ``recover_pids_from_raw_state`` to walk both + files when ``run --force`` falls back to raw-JSON PID recovery. + """ + + def __init__(self, namespace: str = INF_NAMESPACE) -> None: + super().__init__(namespace) + + # --- model-state file layout ---------------------------------------- + + def models_dir(self) -> Path: + """``$AREAL_HOME//models`` — created lazily.""" + + d = self.root() / "models" + d.mkdir(parents=True, exist_ok=True) + return d + + def models_state_path(self, service: str) -> Path: + return self.models_dir() / f"{service}.json" + + def models_lock_path(self, service: str) -> Path: + return self.models_dir() / f"{service}.lock" + + # --- locking -------------------------------------------------------- + + @contextmanager + def lock_model_state(self, service: str) -> Iterator[None]: + """Per-service flock on the model-registry file. Held across + register / deregister so concurrent CLI calls cannot race the + JSON read-modify-write.""" + + with file_lock(self.models_lock_path(service)): + yield + + # --- best-effort recovery (override) -------------------------------- + + def recover_pids_from_raw_state(self, service: str) -> list[int]: + """Walk BOTH service-state and model-state JSON for ``pid`` / + ``pids`` keys. Inference overrides scaffold's single-file walker + because the second (model-state) file is invisible to it.""" + + pids: list[int] = [] + pid_keys = {"pid", "pids"} + + def add(value) -> None: + if isinstance(value, int) and value > 0: + pids.append(value) + elif isinstance(value, list): + for item in value: + add(item) + + def walk(value) -> None: + if isinstance(value, dict): + for key, item in value.items(): + if key in pid_keys: + add(item) + else: + walk(item) + elif isinstance(value, list): + for item in value: + walk(item) + + for path in (self.service_state_path(service), self.models_state_path(service)): + if not path.exists(): + continue + with open(path) as f: + walk(json.load(f)) + + seen: set[int] = set() + unique: list[int] = [] + for pid in pids: + if pid not in seen: + seen.add(pid) + unique.append(pid) + return unique + + +# Module-level singleton — every dataclass / verb routes path resolution +# through this. Tests use ``AREAL_HOME`` to isolate; the store re-resolves +# on every call so the env override takes effect without rebuilding. +store = InferenceStateStore() + + +# ``DEFAULT_SERVICE`` is re-exported above so existing subcommand imports +# (`from ...inference.state import DEFAULT_SERVICE`) keep working. +__all__ = [ + "DEFAULT_SERVICE", + "INF_NAMESPACE", + "InferenceStateStore", + "ModelEntry", + "ModelReplica", + "ModelState", + "RuntimeState", + "ServiceState", + "store", +] + + +def _handle_from_dict(raw: dict) -> TaskHandle: + return TaskHandle( + host=raw["host"], + ports=list(raw.get("ports", [])), + gpu_devices=list(raw.get("gpu_devices", [])), + ref=dict(raw.get("ref", {})), + ) + + +@dataclass +class ModelReplica: + """One DP replica: data-proxy in front, worker behind. Field order + matches the request-flow direction used by stop / status output.""" + + data_proxy: TaskHandle + worker: TaskHandle + + +@dataclass +class ModelEntry: + backend: str = "" + replicas: list[ModelReplica] = field(default_factory=list) + + def all_workers(self) -> list[TaskHandle]: + return [r.worker for r in self.replicas] + + def all_data_proxies(self) -> list[TaskHandle]: + return [r.data_proxy for r in self.replicas] + + def gpu_devices(self) -> list[int]: + return [g for w in self.all_workers() for g in w.gpu_devices] + + @classmethod + def from_dict(cls, raw: dict) -> ModelEntry: + replicas = [ + ModelReplica( + data_proxy=_handle_from_dict(r["data_proxy"]), + worker=_handle_from_dict(r["worker"]), + ) + for r in raw.get("replicas", []) + ] + return cls(backend=raw.get("backend", ""), replicas=replicas) + + +@dataclass +class ServiceState: + service: str + backend: str + gateway_handle: TaskHandle + router_handle: TaskHandle + admin_api_key: str + started_at: float + launch_mode: str = "detached" + + @property + def gateway_pid(self) -> int: + return self.gateway_handle.pid + + @property + def gateway_url(self) -> str: + return self.gateway_handle.addr + + @property + def router_pid(self) -> int: + return self.router_handle.pid + + @property + def router_url(self) -> str: + return self.router_handle.addr + + def save(self) -> None: + atomic_write_json(store.service_state_path(self.service), asdict(self)) + store.set_current_service(self.service) + + @classmethod + def load(cls, service: str) -> ServiceState: + p = store.service_state_path(service) + if not p.exists(): + raise FileNotFoundError(f"No service state at {p}") + with open(p) as f: + raw = json.load(f) + return cls( + service=raw.get("service", service), + backend=raw["backend"], + gateway_handle=_handle_from_dict(raw["gateway_handle"]), + router_handle=_handle_from_dict(raw["router_handle"]), + admin_api_key=raw["admin_api_key"], + started_at=float(raw["started_at"]), + launch_mode=raw.get("launch_mode", "detached"), + ) + + @classmethod + def remove(cls, service: str) -> None: + p = store.service_state_path(service) + if p.exists(): + p.unlink() + store.clear_current_service(service) + + +@dataclass +class ModelState: + service: str + models: dict[str, ModelEntry] = field(default_factory=dict) + + def save(self) -> None: + atomic_write_json(store.models_state_path(self.service), asdict(self)) + + @classmethod + def load(cls, service: str) -> ModelState: + p = store.models_state_path(service) + if not p.exists(): + return cls(service=service) + with open(p) as f: + raw = json.load(f) + models = { + name: ModelEntry.from_dict(entry) + for name, entry in (raw.pop("models", None) or {}).items() + } + return cls(service=raw.get("service", service), models=models) + + @classmethod + def remove(cls, service: str) -> None: + p = store.models_state_path(service) + if p.exists(): + p.unlink() + + def all_workers(self) -> list[TaskHandle]: + return [h for entry in self.models.values() for h in entry.all_workers()] + + def all_data_proxies(self) -> list[TaskHandle]: + return [h for entry in self.models.values() for h in entry.all_data_proxies()] + + def occupied_gpus(self) -> set[int]: + return {g for entry in self.models.values() for g in entry.gpu_devices()} + + +@dataclass +class RuntimeState(ServiceStateBase): + """Composite of ServiceState (gateway/router) and ModelState (model + registry). The single object the CLI verbs operate on; the lifecycle + contract is implemented against ``RuntimeState`` so scaffold helpers + enumerate every component in one pass.""" + + service_state: ServiceState + model_state: ModelState + + @property + def service(self) -> str: + return self.service_state.service + + @property + def backend(self) -> str: + return self.service_state.backend + + @property + def gateway_handle(self) -> TaskHandle: + return self.service_state.gateway_handle + + @property + def router_handle(self) -> TaskHandle: + return self.service_state.router_handle + + @property + def gateway_pid(self) -> int: + return self.service_state.gateway_pid + + @property + def gateway_url(self) -> str: + return self.service_state.gateway_url + + @property + def router_pid(self) -> int: + return self.service_state.router_pid + + @property + def router_url(self) -> str: + return self.service_state.router_url + + @property + def admin_api_key(self) -> str: + return self.service_state.admin_api_key + + @property + def started_at(self) -> float: + return self.service_state.started_at + + @property + def launch_mode(self) -> str: + return self.service_state.launch_mode + + @property + def models(self) -> dict[str, ModelEntry]: + return self.model_state.models + + def save(self) -> None: + self.service_state.save() + self.model_state.save() + + @classmethod + def load(cls, service: str) -> RuntimeState: + service_state = ServiceState.load(service) + return cls( + service_state=service_state, + model_state=ModelState.load(service_state.service), + ) + + # --- ServiceStateBase contract ---------------------------------------- + + def gateway_alive(self) -> bool: + # Non-local backends will need their own liveness probe — PID-based + # check returns False for them (no pid in handle.ref). + return pid_alive(self.gateway_pid) + + def components(self) -> Iterable[tuple[str, SupportsComponentProbe]]: + yield "gateway", self.gateway_handle + yield "router", self.router_handle + for name, entry in self.model_state.models.items(): + for i, replica in enumerate(entry.replicas): + yield f"data_proxy[{name}/{i}]", replica.data_proxy + yield f"worker[{name}/{i}]", replica.worker diff --git a/tests/v2/cli/test_inference_cli_lifecycle.py b/tests/v2/cli/test_inference_cli_lifecycle.py new file mode 100644 index 0000000000..44cd4a45f0 --- /dev/null +++ b/tests/v2/cli/test_inference_cli_lifecycle.py @@ -0,0 +1,165 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json + +from areal.v2.cli.inference.scheduler import TaskHandle +from areal.v2.cli.inference.state import ( + ModelEntry, + ModelReplica, + ModelState, + ServiceState, +) + + +def _service_state(gateway_pid: int = 30, router_pid: int = 40) -> ServiceState: + return ServiceState( + service="svc", + backend="local", + gateway_handle=TaskHandle( + host="127.0.0.1", ports=[8080], gpu_devices=[], ref={"pid": gateway_pid} + ), + router_handle=TaskHandle( + host="127.0.0.1", ports=[9000], gpu_devices=[], ref={"pid": router_pid} + ), + admin_api_key="admin", + started_at=1.0, + ) + + +def _replica(*, worker_pid: int, proxy_pid: int) -> ModelReplica: + return ModelReplica( + data_proxy=TaskHandle( + host="127.0.0.1", ports=[5001], gpu_devices=[], ref={"pid": proxy_pid} + ), + worker=TaskHandle( + host="127.0.0.1", ports=[5000], gpu_devices=[0], ref={"pid": worker_pid} + ), + ) + + +def test_terminate_runtime_state_kills_in_order(monkeypatch): + from areal.v2.cli.inference import common + from areal.v2.cli.inference.state import RuntimeState + + service_state = _service_state(gateway_pid=30, router_pid=40) + model_state = ModelState( + service="svc", + models={ + "m": ModelEntry( + backend="sglang:d1", + replicas=[_replica(worker_pid=10, proxy_pid=20)], + ) + }, + ) + calls = [] + + def fake_kill_pids(pids, grace_s): + calls.append((list(pids), grace_s)) + + monkeypatch.setattr(common, "kill_pids", fake_kill_pids) + + common.terminate_runtime_state( + RuntimeState(service_state=service_state, model_state=model_state), + grace_s=7.0, + ) + + # Data-flow order: data_proxy → worker → gateway → router + assert calls == [([20], 7.0), ([10], 7.0), ([30], 7.0), ([40], 7.0)] + + +def test_prepare_service_slot_force_recovers_raw_pids(tmp_path, monkeypatch): + from areal.v2.cli.inference import lifecycle as inf_lifecycle_mod + from areal.v2.cli.inference.lifecycle import inf_lifecycle + from areal.v2.cli.inference.state import store + + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + # Omit the ``backend`` field so ServiceState.load raises KeyError and + # force_replace_slot falls back to recover_pids_from_raw_state. + store.service_state_path("svc").write_text( + json.dumps( + { + "service": "svc", + "gateway_handle": { + "host": "127.0.0.1", + "ports": [8080], + "gpu_devices": [], + "ref": {"pid": 100}, + }, + "router_handle": { + "host": "127.0.0.1", + "ports": [9000], + "gpu_devices": [], + "ref": {"pid": 101}, + }, + "admin_api_key": "admin", + "started_at": 1.0, + } + ) + ) + store.models_state_path("svc").write_text( + json.dumps( + { + "service": "svc", + "models": { + "m": { + "backend": "sglang:d1", + "replicas": [ + { + "data_proxy": { + "host": "127.0.0.1", + "ports": [5001], + "gpu_devices": [], + "ref": {"pid": 300}, + }, + "worker": { + "host": "127.0.0.1", + "ports": [5000], + "gpu_devices": [0], + "ref": {"pid": 200}, + }, + } + ], + } + }, + } + ) + ) + calls = [] + + def fake_kill_pids(pids, grace_s): + calls.append((list(pids), grace_s)) + + monkeypatch.setattr(inf_lifecycle_mod, "kill_pids", fake_kill_pids) + + inf_lifecycle.force_replace_slot("svc", grace_s=5.0) + + assert calls == [([100, 101, 300, 200], 5.0)] + assert not store.service_state_path("svc").exists() + assert not store.models_state_path("svc").exists() + + +def test_foreground_cleanup_uses_latest_model_state(tmp_path, monkeypatch): + from areal.v2.cli.inference import common + from areal.v2.cli.inference.commands import run + + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + service_state = _service_state(gateway_pid=30, router_pid=40) + latest = ModelState(service="svc") + latest.models["later"] = ModelEntry( + backend="sglang:d1", + replicas=[_replica(worker_pid=10, proxy_pid=20)], + ) + latest.save() + calls = [] + + def fake_kill_pids(pids, grace_s): + calls.append((list(pids), grace_s)) + + monkeypatch.setattr(common, "kill_pids", fake_kill_pids) + + run._cleanup_runtime("svc", service_state, ModelState(service="svc"), grace_s=3.0) + + # Same data-flow order as terminate_runtime_state. + assert calls == [([20], 3.0), ([10], 3.0), ([30], 3.0), ([40], 3.0)] diff --git a/tests/v2/cli/test_inference_cli_parser.py b/tests/v2/cli/test_inference_cli_parser.py new file mode 100644 index 0000000000..896dfc8338 --- /dev/null +++ b/tests/v2/cli/test_inference_cli_parser.py @@ -0,0 +1,120 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from click.testing import CliRunner + +from areal.v2.cli.main import cli + + +def test_inference_commands_expose_service_flag(): + runner = CliRunner() + + for command in ( + "run", + "stop", + "status", + "register", + "deregister", + "models", + "logs", + ): + result = runner.invoke(cli, ["inf", command, "--help"]) + assert result.exit_code == 0 + assert "--service" in result.output + + +def test_inference_ps_lists_services_not_models(): + runner = CliRunner() + + result = runner.invoke(cli, ["inf", "ps", "--help"]) + + assert result.exit_code == 0 + assert "--all" in result.output + assert "--service" not in result.output + + +def test_inference_model_name_options_are_exposed(): + runner = CliRunner() + + register = runner.invoke(cli, ["inf", "register", "--help"]) + deregister = runner.invoke(cli, ["inf", "deregister", "--help"]) + + assert register.exit_code == 0 + assert "--model-name" in register.output + assert deregister.exit_code == 0 + assert "--model-name" in deregister.output + + +def test_inference_register_parses_model_name_option(monkeypatch): + from areal.v2.cli.inference.commands import register + + captured = {} + + def fake_register(model_name, opts, *, service=None): + captured["model_name"] = model_name + captured["opts"] = opts + captured["service"] = service + return 0 + + monkeypatch.setattr(register, "do_register", fake_register) + runner = CliRunner() + + result = runner.invoke( + cli, + [ + "inf", + "register", + "--service", + "svc", + "--model-name", + "m", + "--backend", + "sglang:d1", + "--model-path", + "/models/m", + ], + ) + + assert result.exit_code == 0 + assert captured["model_name"] == "m" + assert captured["service"] == "svc" + assert captured["opts"]["backend"] == "sglang:d1" + assert captured["opts"]["model_path"] == "/models/m" + + +def test_inference_deregister_parses_model_name_option(monkeypatch): + from areal.v2.cli.inference.commands import deregister + + captured = {} + + def fake_deregister(model_name, grace, force, *, service=None): + captured["model_name"] = model_name + captured["grace"] = grace + captured["force"] = force + captured["service"] = service + return 0 + + monkeypatch.setattr(deregister, "do_deregister", fake_deregister) + runner = CliRunner() + + result = runner.invoke( + cli, + [ + "inf", + "deregister", + "--service", + "svc", + "--model-name", + "m", + "--force", + ], + ) + + assert result.exit_code == 0 + assert captured == { + "model_name": "m", + "grace": 10.0, + "force": True, + "service": "svc", + } diff --git a/tests/v2/cli/test_inference_cli_state.py b/tests/v2/cli/test_inference_cli_state.py new file mode 100644 index 0000000000..03decf98cc --- /dev/null +++ b/tests/v2/cli/test_inference_cli_state.py @@ -0,0 +1,164 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +import os + +from click.testing import CliRunner + +from areal.v2.cli.inference.scheduler import TaskHandle +from areal.v2.cli.inference.state import ( + ModelEntry, + ModelState, + ServiceState, + store, +) +from areal.v2.cli.main import cli + + +def _service_path(service: str): + return store.service_state_path(service) + + +def _current_path(): + return store.current_service_path() + + +def _resolve(name: str | None) -> str: + return store.resolve_service_name(name) + + +def _save_service(service: str, *, gateway_pid: int | None = None) -> None: + pid = os.getpid() if gateway_pid is None else gateway_pid + ServiceState( + service=service, + backend="local", + gateway_handle=TaskHandle( + host="127.0.0.1", ports=[8080], gpu_devices=[], ref={"pid": pid} + ), + router_handle=TaskHandle( + host="127.0.0.1", ports=[9000], gpu_devices=[], ref={"pid": pid} + ), + admin_api_key="admin", + started_at=1.0, + ).save() + + +def _placeholder_model() -> ModelEntry: + return ModelEntry(backend="sglang:d1", replicas=[]) + + +def test_service_and_model_state_are_per_service(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + _save_service("svc-a") + model_state = ModelState(service="svc-a") + model_state.models["m-a"] = _placeholder_model() + model_state.save() + + assert _service_path("svc-a").exists() + assert store.models_state_path("svc-a").exists() + assert _current_path().read_text().strip() == "svc-a" + + loaded_service = ServiceState.load("svc-a") + loaded_models = ModelState.load("svc-a") + assert loaded_service.service == "svc-a" + assert loaded_service.backend == "local" + assert list(loaded_models.models) == ["m-a"] + + +def test_resolve_service_uses_current_then_single_running(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + _save_service("current") + assert _resolve(None) == "current" + + _current_path().unlink() + assert _resolve(None) == "current" + + +def test_models_command_is_service_scoped(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + _save_service("svc-a") + _save_service("svc-b") + for service, model in (("svc-a", "m-a"), ("svc-b", "m-b")): + model_state = ModelState(service=service) + model_state.models[model] = _placeholder_model() + model_state.save() + + result = CliRunner().invoke(cli, ["inf", "models", "--service", "svc-a", "--json"]) + + assert result.exit_code == 0 + payload = json.loads(result.output) + assert [entry["name"] for entry in payload] == ["m-a"] + + +def test_ps_lists_services_and_stale_state(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + _save_service("live") + _save_service("stale", gateway_pid=-1) + + result = CliRunner().invoke(cli, ["inf", "ps", "--all", "--json"]) + + assert result.exit_code == 0 + payload = {entry["service"]: entry["status"] for entry in json.loads(result.output)} + assert payload == {"live": "running", "stale": "stale"} + + +def test_recover_pids_from_raw_state_walks_handles(tmp_path, monkeypatch): + monkeypatch.setenv("AREAL_HOME", str(tmp_path)) + # Hand-write the v1 schema so we exercise recover_pids_from_raw_state + # against TaskHandle.ref.pid nesting. Order of returned pids follows the + # JSON walk order: service file first, then models file; within each + # ModelReplica data_proxy is declared before worker. + _service_path("svc").write_text( + json.dumps( + { + "service": "svc", + "backend": "local", + "gateway_handle": { + "host": "127.0.0.1", + "ports": [8080], + "gpu_devices": [], + "ref": {"pid": 100}, + }, + "router_handle": { + "host": "127.0.0.1", + "ports": [9000], + "gpu_devices": [], + "ref": {"pid": 101}, + }, + "admin_api_key": "admin", + "started_at": 1.0, + } + ) + ) + store.models_state_path("svc").write_text( + json.dumps( + { + "service": "svc", + "models": { + "m": { + "backend": "sglang:d1", + "replicas": [ + { + "data_proxy": { + "host": "127.0.0.1", + "ports": [5001], + "gpu_devices": [], + "ref": {"pid": 300}, + }, + "worker": { + "host": "127.0.0.1", + "ports": [5000], + "gpu_devices": [0], + "ref": {"pid": 200}, + }, + } + ], + } + }, + } + ) + ) + + assert store.recover_pids_from_raw_state("svc") == [100, 101, 300, 200] From bbc85c740ca96c38f1165315c08fe107688f5934 Mon Sep 17 00:00:00 2001 From: IF007 <44497574+IF007@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:04:18 +0800 Subject: [PATCH 19/36] feat(agent_service): add agent service with OpenClaw and Hermes examples (#1383) * feat(agent_service): add agent service with OpenClaw and Hermes examples Add an end-to-end agent service stack under areal/v2/agent_service with a gateway bridge, data proxy, and worker, plus two example agents (OpenClaw and Hermes) demonstrating session lifecycle, reward setting, and training integration. Key changes: - Extend gateway bridge and data proxy to drive agent sessions - Add OpenClaw and Hermes example agents under examples/agent_service - Add lifecycle demo, run scripts, and config for the Hermes example - Add integration and per-agent tests under tests/v2/agent_service Co-Authored-By: Claude Opus 4.7 * refactor(examples): drop redundant hermes session self-check scripts The hermes agent service session flow only needs run_agent_service.py: chat sessions are keyed implicitly by the /v1/responses "user" field, and self-evolution mints the per-session sk-sess-* key internally. The start_session.py / demo_lifecycle.py scripts only probed the inference gateway control plane and duplicated examples/openclaw, so remove them. Key changes: - Delete examples/agent_service/hermes/{start_session,demo_lifecycle}.py - Remove the README "Connectivity self-checks" section and Files rows - Repoint set_reward.py docs to run_agent_service.py for the session key * refactor(examples): hoist hermes example to examples/hermes top level Move the Hermes agent-service example from examples/agent_service/hermes to examples/hermes, putting it at the same level as examples/openclaw, and drop the superseded examples/agent_service/openclaw (the standalone examples/openclaw already replaces it). Key changes: - git mv examples/agent_service/hermes -> examples/hermes - Update agent_cls_path and docs to examples.hermes.hermes.HermesAgent - Rewrite README paths from examples/agent_service/hermes to examples/hermes - Remove obsolete examples/agent_service/openclaw Refs: #1383 Co-Authored-By: Claude Opus 4.7 * refactor(examples): split hermes flow into standalone step scripts Replace the monolithic run_agent_service.py with a 5-step flow matching the openclaw example: train.py, `areal agent run`, start_session.py, hermes_loop.py interaction, and set_reward.py. Update start_session.py for the v2 inference service (HTTP 201 + nested session credentials), fold training defaults into config.yaml, and rewrite the README. Co-Authored-By: Claude Opus 4.7 * refactor(examples): inline CLI formatting helpers and trim hermes README Remove the shared examples/hermes/_fmt.py module by inlining the helpers it provided into the two scripts that used it, so each script is self-contained. Also condense the README prose and use a placeholder model path. Key changes: - Inline formatting helpers into start_session.py and set_reward.py - Delete examples/hermes/_fmt.py and its README entry - Simplify quick-start prose; use actor.path=/path/to/your_model - Switch config.yaml actor backend to megatron:d1 Co-Authored-By: Claude Opus 4.7 * docs(examples): clarify session key usage in hermes and openclaw READMEs Annotate which placeholders reuse the sk-sess-* key returned by start_session and distinguish them from the upstream LLM credentials. Trim the openclaw README to focus on the RL training flow. Key changes: - Mark hermes --session-api-key / --api-key as the start_session key - Note HERMES_UPSTREAM_* are your own upstream LLM credentials - Use placeholder values instead of sk-... in env exports - Remove the standalone agent-service section from openclaw README Co-Authored-By: Claude Opus 4.7 * style(test): fix import grouping in agent service tests Remove the stray blank line between the pytest import and the examples.* imports so ruff's isort check passes in CI. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- areal/v2/agent_service/README.md | 14 +- areal/v2/agent_service/__init__.py | 7 +- areal/v2/agent_service/data_proxy/app.py | 136 +++- areal/v2/agent_service/gateway/__main__.py | 13 +- areal/v2/agent_service/gateway/app.py | 33 +- areal/v2/agent_service/gateway/bridge.py | 377 +++++++++- areal/v2/agent_service/protocol.py | 7 + areal/v2/agent_service/types.py | 76 +- areal/v2/agent_service/worker/app.py | 48 +- examples/hermes/README.md | 192 +++++ examples/hermes/__init__.py | 10 + examples/hermes/config.yaml | 164 +++++ examples/hermes/hermes.py | 444 +++++++++++ examples/hermes/hermes_loop.py | 138 ++++ examples/hermes/set_reward.py | 140 ++++ examples/hermes/start_session.py | 198 +++++ examples/hermes/train.py | 17 + tests/v2/agent_service/test_hermes_agent.py | 234 ++++++ tests/v2/agent_service/test_integration.py | 696 ++++++++++++++++-- tests/v2/agent_service/test_openclaw_agent.py | 210 ++++++ 20 files changed, 3058 insertions(+), 96 deletions(-) create mode 100644 examples/hermes/README.md create mode 100644 examples/hermes/__init__.py create mode 100644 examples/hermes/config.yaml create mode 100644 examples/hermes/hermes.py create mode 100644 examples/hermes/hermes_loop.py create mode 100644 examples/hermes/set_reward.py create mode 100644 examples/hermes/start_session.py create mode 100644 examples/hermes/train.py create mode 100644 tests/v2/agent_service/test_hermes_agent.py create mode 100644 tests/v2/agent_service/test_openclaw_agent.py diff --git a/areal/v2/agent_service/README.md b/areal/v2/agent_service/README.md index 2a06d19fd0..de8f32d80a 100644 --- a/areal/v2/agent_service/README.md +++ b/areal/v2/agent_service/README.md @@ -32,7 +32,8 @@ Client (HTTP/WS) ### Components **Gateway** — Public entry point. Accepts WebSocket connections (Gateway protocol) and -HTTP requests (OpenResponses bridge at `POST /v1/responses`). Routes to the appropriate +HTTP requests via two bridges: the OpenResponses bridge (`POST /v1/responses`) and the +OpenAI chat-completions bridge (`POST /v1/chat/completions`). Routes to the appropriate DataProxy via the Router. **Router** — Session-affine routing service. DataProxy instances register at startup. @@ -123,11 +124,12 @@ class EventEmitter(Protocol): ### Gateway -| Endpoint | Method | Description | -| --------------- | ------ | -------------------------- | -| `/health` | GET | Health check | -| `/ws` | WS | Gateway WebSocket protocol | -| `/v1/responses` | POST | OpenResponses HTTP bridge | +| Endpoint | Method | Description | +| ---------------------- | ------ | ------------------------------ | +| `/health` | GET | Health check | +| `/ws` | WS | Gateway WebSocket protocol | +| `/v1/responses` | POST | OpenResponses HTTP bridge | +| `/v1/chat/completions` | POST | OpenAI chat-completions bridge | ## Multi-turn Conversation Flow diff --git a/areal/v2/agent_service/__init__.py b/areal/v2/agent_service/__init__.py index 2c964e550f..c92956aeb9 100644 --- a/areal/v2/agent_service/__init__.py +++ b/areal/v2/agent_service/__init__.py @@ -16,7 +16,12 @@ - ``protocol`` — WebSocket frame types and helpers """ -from .types import AgentRequest, AgentResponse, AgentRunnable, EventEmitter +from .types import ( + AgentRequest, + AgentResponse, + AgentRunnable, + EventEmitter, +) __all__ = [ "AgentRequest", diff --git a/areal/v2/agent_service/data_proxy/app.py b/areal/v2/agent_service/data_proxy/app.py index fbf51646ba..b5120f756c 100644 --- a/areal/v2/agent_service/data_proxy/app.py +++ b/areal/v2/agent_service/data_proxy/app.py @@ -10,10 +10,12 @@ from typing import Any import httpx -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse, StreamingResponse from areal.utils import logging +from ..protocol import PASSTHROUGH_HEADER from .config import DataProxyConfig logger = logging.getLogger("AgentDataProxy") @@ -24,6 +26,16 @@ class _SessionData: history: list[dict[str, Any]] = field(default_factory=list) metadata: dict[str, Any] = field(default_factory=dict) last_active: float = field(default_factory=time.monotonic) + reward: float | None = None + # Per-session inference routing for self-evolution. Holds + # ``{"base_url", "api_key", "model"}`` where ``api_key`` is the + # ``sk-sess-*`` the **caller** obtained itself and passed on the turn + # (``session_api_key``). The Agent Service never talks to the training + # side — it only forwards these fields to the worker so the agent routes + # its LLM calls through the inference gateway under that key. Cached on + # the first turn that carries them so later turns of a multi-turn session + # can omit them. + inference: dict[str, Any] | None = None def create_data_proxy_app(config: DataProxyConfig) -> FastAPI: @@ -39,6 +51,52 @@ async def _close_worker_session(session_key: str) -> None: except Exception: logger.debug("Failed to close worker session %s", session_key) + def _resolve_inference( + session: _SessionData, body: dict[str, Any], metadata: dict[str, Any] + ) -> dict[str, Any]: + """Forward caller-supplied inference routing to the worker metadata. + + Self-evolution decouples the Agent Service from the training side: the + **caller** mints its own per-session ``sk-sess-*`` (e.g. via its own + ``/rl/start_session``) and passes it on the turn body. This proxy never + contacts the inference/training side — it merely caches the routing + handle on the session and injects it as ``metadata['areal_inference']`` + so the agent routes its LLM calls through the inference gateway. + + The turn opts in **by the presence of the routing fields** (no separate + flag); the required pair is: + + ``inf_base_url`` — inference gateway base URL the agent's LLM + calls go to (required). + ``session_api_key`` — the caller-minted ``sk-sess-*`` (required). + ``inf_model`` — model id the agent should request (default ""). + + ``inf_model`` is optional and never triggers self-evolution on its own; + only the required pair does. The handle is cached on the first turn that + carries it, so a multi-turn session may send these fields once and omit + them afterwards. + """ + base_url = (body.get("inf_base_url") or "").rstrip("/") + api_key = body.get("session_api_key") or "" + if base_url or api_key: + if not base_url or not api_key: + raise HTTPException( + status_code=400, + detail=( + "self-evolution requires both 'inf_base_url' and " + "'session_api_key' in the turn body" + ), + ) + session.inference = { + "base_url": base_url, + "api_key": api_key, + "model": body.get("inf_model", "") or "", + } + + if session.inference is not None: + metadata = {**metadata, "areal_inference": dict(session.inference)} + return metadata + async def _reap_idle_sessions() -> None: while True: await asyncio.sleep(60) @@ -72,6 +130,25 @@ async def health(): @app.post("/session/{session_key}/turn") async def turn(session_key: str, body: dict[str, Any]): + """Single turn endpoint for every protocol and streaming mode. + + The worker's ``/run`` decides the shape of the turn and signals it via + the :data:`PASSTHROUGH_HEADER` response header: + + - header absent — a structured turn (``application/json``). The body is + read fully, conversation history is rebuilt from the emitted + ``events``, and the JSON is returned to the caller (this backs + ``/v1/responses`` and the WebSocket path). + - header == ``"1"`` — a raw-passthrough turn. The body is relayed + **byte-for-byte** without parsing, so the caller gets the upstream's + exact wire format (this backs ``/v1/chat/completions``, streaming or + not). Keying on the marker rather than ``Content-Type`` means a + *non-streaming* passthrough — itself ``application/json`` — is still + relayed verbatim instead of being mistaken for a structured turn. No + history is kept on this path; stateful callers rely on *route + affinity* (a stable ``session_key`` pins every turn to this same + DataProxy/Worker so the agent reuses its own state). + """ session = sessions.get(session_key) if session is None: session = _SessionData() @@ -82,6 +159,12 @@ async def turn(session_key: str, body: dict[str, Any]): queue_mode = body.get("queue_mode", "collect") metadata = body.get("metadata", {}) + # Self-evolution (opt-in): when the turn carries inference-routing + # fields, forward them so the agent's LLM calls flow through the + # inference gateway under the caller's own ``sk-sess-*``. No-op for a + # plain turn that omits them. + metadata = _resolve_inference(session, body, metadata) + worker_request = { "message": message, "session_key": session_key, @@ -91,9 +174,54 @@ async def turn(session_key: str, body: dict[str, Any]): "metadata": metadata, } - resp = await http_client.post(f"{config.worker_addr}/run", json=worker_request) - resp.raise_for_status() + # Open the worker stream so we can inspect status/Content-Type before + # deciding whether to parse (structured) or relay (raw passthrough). + req = http_client.build_request( + "POST", f"{config.worker_addr}/run", json=worker_request + ) + resp = await http_client.send(req, stream=True) + session.last_active = time.monotonic() + + is_passthrough = resp.headers.get(PASSTHROUGH_HEADER) == "1" + + if is_passthrough: + # Raw-passthrough turn: relay the worker body verbatim, keep no + # history (parsing the stream would fight the byte-exact relay). + async def _relay(): + try: + async for chunk in resp.aiter_raw(): + yield chunk + finally: + await resp.aclose() + session.last_active = time.monotonic() + + headers = { + k: v + for k, v in resp.headers.items() + if k.lower() + not in ( + "content-length", + "transfer-encoding", + "connection", + PASSTHROUGH_HEADER, + ) + } + return StreamingResponse( + _relay(), + status_code=resp.status_code, + headers=headers, + media_type=resp.headers.get("content-type") or None, + ) + + # Structured turn: read the full JSON body, then rebuild history. + await resp.aread() + status_code = resp.status_code result = resp.json() + await resp.aclose() + + if status_code >= 400: + # Worker reported an error; forward it without touching history. + return JSONResponse(result, status_code=status_code) session.history.append({"role": "user", "content": message}) @@ -137,7 +265,7 @@ async def turn(session_key: str, body: dict[str, Any]): session.history.append({"role": "assistant", "content": summary}) session.last_active = time.monotonic() - return result + return JSONResponse(result, status_code=status_code) @app.post("/session/{session_key}/close") async def close_session(session_key: str): diff --git a/areal/v2/agent_service/gateway/__main__.py b/areal/v2/agent_service/gateway/__main__.py index 151e6c382e..1261112aeb 100644 --- a/areal/v2/agent_service/gateway/__main__.py +++ b/areal/v2/agent_service/gateway/__main__.py @@ -10,7 +10,12 @@ from ..auth import DEFAULT_ADMIN_API_KEY from .app import create_gateway_app -from .bridge import OpenResponsesBridge, mount_bridge +from .bridge import ( + ChatCompletionsBridge, + OpenResponsesBridge, + mount_bridge, + mount_chat_bridge, +) from .config import GatewayConfig @@ -48,6 +53,12 @@ def main() -> None: ), admin_api_key=config.admin_api_key, ) + mount_chat_bridge( + app, + ChatCompletionsBridge( + router_addr=config.router_addr, admin_api_key=config.admin_api_key + ), + ) uvicorn.run( app, host=config.host, diff --git a/areal/v2/agent_service/gateway/app.py b/areal/v2/agent_service/gateway/app.py index b3043ad57a..88914fedc3 100644 --- a/areal/v2/agent_service/gateway/app.py +++ b/areal/v2/agent_service/gateway/app.py @@ -7,13 +7,21 @@ import hmac import json import traceback +from typing import Any import httpx -from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect +from fastapi import ( + Depends, + FastAPI, + HTTPException, + Query, + WebSocket, + WebSocketDisconnect, +) from areal.utils import logging -from ..auth import admin_headers +from ..auth import admin_headers, make_admin_dependency from ..protocol import ( FrameType, RequestFrame, @@ -47,6 +55,7 @@ def create_gateway_app(config: GatewayConfig) -> FastAPI: app = FastAPI(title="AReaL Agent Gateway") http_client = httpx.AsyncClient(timeout=config.forward_timeout) _auth_headers = admin_headers(config.admin_api_key) + _admin = make_admin_dependency(config.admin_api_key) async def _route(session_key: str) -> str: resp = await http_client.post( @@ -82,6 +91,26 @@ async def _send_turn( async def health(): return {"status": "ok"} + @app.post("/sessions/close", dependencies=[Depends(_admin)]) + async def close_session(body: dict[str, Any]): + """Close a session, forwarding to the DataProxy that owns it. + + The ``session_key`` is supplied in the request body. Session lifecycle + is keyed solely by ``session_key`` and is identical across every gateway + surface (WebSocket, ``/v1/responses``, ``/v1/chat/completions``): the + Router pins a key to one DataProxy via route affinity, so resolving the + key here lands on the DataProxy holding that session's state. The + DataProxy close is idempotent and propagates down to the Worker, so an + unknown key is a no-op success. + """ + session_key = body.get("session_key", "") + if not session_key: + raise HTTPException(status_code=400, detail="'session_key' is required") + data_proxy_addr = await _route(session_key) + resp = await http_client.post(f"{data_proxy_addr}/session/{session_key}/close") + resp.raise_for_status() + return {"status": "ok"} + @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket, token: str = Query(default="")): if not hmac.compare_digest(token, config.admin_api_key): diff --git a/areal/v2/agent_service/gateway/bridge.py b/areal/v2/agent_service/gateway/bridge.py index e47c8d8f35..0d5936b5a2 100644 --- a/areal/v2/agent_service/gateway/bridge.py +++ b/areal/v2/agent_service/gateway/bridge.py @@ -4,13 +4,15 @@ from __future__ import annotations +import json import uuid from abc import ABC, abstractmethod +from collections.abc import AsyncIterator from typing import Any import httpx from fastapi import Depends, FastAPI, Request -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, StreamingResponse from areal.utils import logging @@ -19,6 +21,13 @@ logger = logging.getLogger("AgentBridge") +# Request/response header carrying the session identifier. A client sends it to +# pin a multi-turn conversation onto the same DataProxy/Worker (route affinity); +# both bridges always echo the resolved key back on the response so the client +# can reuse it on the next request — including a key derived or randomly minted +# server-side when the client sent none. +SESSION_KEY_HEADER = "X-AReaL-Session-Key" + class AgentBridge(ABC): @abstractmethod @@ -44,11 +53,18 @@ async def handle_request(self, request: Request) -> Any: model: str = body.get("model", "") user: str = body.get("user", "") - if not user: + # An explicit X-AReaL-Session-Key pins the conversation to one + # DataProxy/Worker directly; only when it is absent do we fall back to + # deriving the key from ``user`` (which is then required for affinity). + explicit_session_key = request.headers.get(SESSION_KEY_HEADER) + if not explicit_session_key and not user: return JSONResponse( { "error": { - "message": "'user' field is required for session affinity", + "message": ( + "either the 'X-AReaL-Session-Key' header or the " + "'user' field is required for session affinity" + ), "type": "invalid_request", } }, @@ -56,7 +72,7 @@ async def handle_request(self, request: Request) -> Any: ) message = self._extract_message(input_items, instructions) - session_key = self._derive_session_key(user, model) + session_key = explicit_session_key or self._derive_session_key(user, model) run_id = generate_run_id() response_id = f"resp-{uuid.uuid4().hex[:12]}" @@ -78,38 +94,47 @@ async def handle_request(self, request: Request) -> Any: route_resp.raise_for_status() data_proxy_addr = route_resp.json()["data_proxy_addr"] + turn_body: dict[str, Any] = { + "message": message, + "run_id": run_id, + "queue_mode": "collect", + "metadata": metadata, + } + # Opt-in self-evolution: forward the caller-supplied inference + # routing fields when present so the DataProxy hands the agent a + # ``sk-sess-*`` (which the caller minted itself) and the agent's LLM + # calls flow through AReaL's inference service. The DataProxy opts + # the turn in by the presence of these fields; absent them the turn + # is plain. The Agent Service never contacts the training side. + for key in ( + "inf_base_url", + "inf_model", + "session_api_key", + ): + if key in body: + turn_body[key] = body[key] + turn_resp = await self._http.post( f"{data_proxy_addr}/session/{session_key}/turn", - json={ - "message": message, - "run_id": run_id, - "queue_mode": "collect", - "metadata": metadata, - }, + json=turn_body, ) turn_resp.raise_for_status() result = turn_resp.json() - output_items: list[dict[str, Any]] = [] - summary = result.get("summary", "") - if summary: - output_items.append( - { - "type": "message", - "role": "assistant", - "content": [{"type": "output_text", "text": summary}], - } - ) + output_items = self._build_output_items(result) + response_metadata = result.get("metadata", {}) - for evt in result.get("events", []): - if evt.get("type") == "tool_call": - output_items.append( - { - "type": "function_call", - "name": evt.get("name", ""), - "arguments": evt.get("args", ""), - } - ) + if body.get("stream"): + # Streaming /v1/responses: the agent ran the structured turn to + # completion (collect); re-encode the collected output as an + # OpenAI Responses-format SSE event stream. + return StreamingResponse( + self._responses_sse( + response_id, model, output_items, response_metadata + ), + media_type="text/event-stream", + headers={SESSION_KEY_HEADER: session_key}, + ) return JSONResponse( { @@ -118,14 +143,16 @@ async def handle_request(self, request: Request) -> Any: "status": "completed", "output": output_items, "model": model, - "metadata": result.get("metadata", {}), - } + "metadata": response_metadata, + }, + headers={SESSION_KEY_HEADER: session_key}, ) except Exception as exc: logger.error("OpenResponses request failed: %s", exc) return JSONResponse( {"error": {"message": str(exc), "type": "server_error"}}, status_code=500, + headers={SESSION_KEY_HEADER: session_key}, ) @staticmethod @@ -149,6 +176,87 @@ def _extract_message(input_items: list[dict[str, Any]], instructions: str) -> st parts.append(f"[tool result] {item.get('output', '')}") return "\n".join(parts) + @staticmethod + def _build_output_items(result: dict[str, Any]) -> list[dict[str, Any]]: + """Translate a structured turn result into OpenAI Responses output items. + + A non-empty ``summary`` becomes an assistant ``message`` item; each + ``tool_call`` event becomes a ``function_call`` item. + """ + output_items: list[dict[str, Any]] = [] + summary = result.get("summary", "") + if summary: + output_items.append( + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": summary}], + } + ) + for evt in result.get("events", []): + if evt.get("type") == "tool_call": + output_items.append( + { + "type": "function_call", + "name": evt.get("name", ""), + "arguments": evt.get("args", ""), + } + ) + return output_items + + @staticmethod + async def _responses_sse( + response_id: str, + model: str, + output_items: list[dict[str, Any]], + metadata: dict[str, Any], + ) -> AsyncIterator[bytes]: + """Emit collected output items as an OpenAI Responses-format SSE stream. + + This is a re-encoding shim, not incremental generation: the structured + turn already ran to completion, so the whole text is sent in one + ``response.output_text.delta``. The terminal ``response.completed`` + event carries the same full response object the non-streaming path + returns, so a client can rely on either. + """ + + def _event(payload: dict[str, Any]) -> bytes: + return f"data: {json.dumps(payload)}\n\n".encode() + + yield _event( + { + "type": "response.created", + "response": { + "id": response_id, + "object": "response", + "status": "in_progress", + "model": model, + }, + } + ) + for item in output_items: + if item.get("type") != "message": + continue + for block in item.get("content", []): + text = block.get("text", "") + if not text: + continue + yield _event({"type": "response.output_text.delta", "delta": text}) + yield _event({"type": "response.output_text.done", "text": text}) + yield _event( + { + "type": "response.completed", + "response": { + "id": response_id, + "object": "response", + "status": "completed", + "model": model, + "output": output_items, + "metadata": metadata, + }, + } + ) + @staticmethod def _derive_session_key(user: str, model: str) -> str: if user: @@ -170,3 +278,210 @@ async def responses_endpoint(request: Request): @app.on_event("shutdown") async def shutdown_bridge(): await bridge.close() + + +# Metadata key under which the chat bridge stashes the full, original +# ``/v1/chat/completions`` request body. Raw-passthrough agents (those +# implementing ``AgentRunnable.stream``) read it from ``request.metadata`` and +# forward it verbatim to their upstream, so no field (``ext_info`` etc.) is lost +# in the chat → turn translation. +CHAT_REQUEST_METADATA_KEY = "chat_request" + + +class ChatCompletionsBridge(AgentBridge): + """OpenAI-compatible ``/v1/chat/completions`` → DataProxy raw stream. + + Translates a chat-completions request into a DataProxy ``turn`` and relays + the worker/agent response **byte-for-byte** (typically SSE), so any + OpenAI-compatible upstream can call the gateway exactly as it would call the + backing agent directly — no client change, exact wire format preserved. The + DataProxy uses the response ``Content-Type`` to tell a raw stream apart from + a structured turn, so both protocols share the one ``turn`` endpoint. + + The full original request body is forwarded inside the turn ``metadata`` + (see :data:`CHAT_REQUEST_METADATA_KEY`) for the agent to replay verbatim. + + **Session model.** ``/v1/chat/completions`` is a stateless protocol: the + client carries the full ``messages`` history on every request, so the + framework does not store conversation history for this path. What the + framework *does* provide is **route affinity** — a stable ``session_key`` + makes the Router pin all of that session's requests to the same + DataProxy/Worker, letting the Worker-side agent reuse its per-session state + (sandbox, agent instance, KV cache). The key is resolved per request by + :meth:`_resolve_session_key`: + + 1. explicit ``X-AReaL-Session-Key`` request header, else + 2. derived from the OpenAI ``user`` field (``chat:{model}:{user}``). + + One of the two is **required** (mirroring :class:`OpenResponsesBridge`); a + request carrying neither is rejected with ``400`` rather than silently + minted a random key, so route affinity is always explicit and a multi-turn + caller can never be split across Workers by accident. + + The resolved key is always echoed back on the ``X-AReaL-Session-Key`` + response header so the caller can pin subsequent turns to it. + """ + + def __init__( + self, router_addr: str, admin_api_key: str = DEFAULT_ADMIN_API_KEY + ) -> None: + self._router_addr = router_addr + self._auth_headers = admin_headers(admin_api_key) + self._http = httpx.AsyncClient(timeout=600.0) + + async def close(self) -> None: + await self._http.aclose() + + async def handle_request(self, request: Request) -> Any: + try: + body = await request.json() + except Exception: + return JSONResponse( + {"error": {"message": "invalid JSON body", "type": "invalid_request"}}, + status_code=400, + ) + + messages = body.get("messages", []) + session_key = self._resolve_session_key(request, body) + if session_key is None: + return JSONResponse( + { + "error": { + "message": ( + "either the 'X-AReaL-Session-Key' header or the " + "'user' field is required for session affinity" + ), + "type": "invalid_request", + } + }, + status_code=400, + ) + run_id = generate_run_id() + message = self._extract_last_user_text(messages) + metadata = {CHAT_REQUEST_METADATA_KEY: body} + + try: + route_resp = await self._http.post( + f"{self._router_addr}/route", + json={"session_key": session_key}, + headers=self._auth_headers, + ) + route_resp.raise_for_status() + data_proxy_addr = route_resp.json()["data_proxy_addr"] + except Exception as exc: + logger.error("ChatCompletions routing failed: %s", exc) + return JSONResponse( + {"error": {"message": str(exc), "type": "server_error"}}, + status_code=502, + headers={SESSION_KEY_HEADER: session_key}, + ) + + # Open the DataProxy stream manually so its status/headers are known + # before we build the StreamingResponse, then relay the body verbatim. + turn_body: dict[str, Any] = { + "message": message, + "run_id": run_id, + "queue_mode": "collect", + "metadata": metadata, + } + # Opt-in self-evolution (same contract as OpenResponsesBridge): forward + # the caller-supplied inference-routing fields when present so the + # agent's LLM calls flow through AReaL's inference service under the + # caller's own ``sk-sess-*``. The DataProxy opts the turn in by the + # presence of these fields; absent them the stream is forwarded + # byte-for-byte exactly as before. + for key in ( + "inf_base_url", + "inf_model", + "session_api_key", + ): + if key in body: + turn_body[key] = body[key] + + req = self._http.build_request( + "POST", + f"{data_proxy_addr}/session/{session_key}/turn", + json=turn_body, + ) + try: + resp = await self._http.send(req, stream=True) + except Exception as exc: + logger.error("ChatCompletions upstream stream failed: %s", exc) + return JSONResponse( + {"error": {"message": str(exc), "type": "server_error"}}, + status_code=502, + headers={SESSION_KEY_HEADER: session_key}, + ) + + async def _relay(): + try: + async for chunk in resp.aiter_raw(): + yield chunk + finally: + await resp.aclose() + + headers = { + k: v + for k, v in resp.headers.items() + if k.lower() not in ("content-length", "transfer-encoding", "connection") + } + headers[SESSION_KEY_HEADER] = session_key + return StreamingResponse( + _relay(), + status_code=resp.status_code, + headers=headers, + media_type=resp.headers.get("content-type"), + ) + + @staticmethod + def _resolve_session_key(request: Request, body: dict[str, Any]) -> str | None: + """Resolve the session key, in priority order (see class docstring). + + Explicit ``X-AReaL-Session-Key`` header wins so a client can pin a + conversation onto one Worker; otherwise the OpenAI ``user`` field is + derived into a key (mirroring :class:`OpenResponsesBridge`). With + neither, return ``None`` so the caller can reject the request with + ``400`` instead of minting an implicit random key. + """ + explicit = request.headers.get(SESSION_KEY_HEADER) + if explicit: + return explicit + user = body.get("user", "") + if user: + model = body.get("model") or "default" + return f"chat:{model}:{user}" + return None + + @staticmethod + def _extract_last_user_text(messages: list[dict[str, Any]]) -> str: + for item in reversed(messages): + if item.get("role") != "user": + continue + content = item.get("content", "") + if isinstance(content, str): + return content + if isinstance(content, list): + parts = [ + block.get("text", "") + for block in content + if isinstance(block, dict) and block.get("type") == "text" + ] + return "".join(parts) + return "" + + +def mount_chat_bridge(app: FastAPI, bridge: ChatCompletionsBridge) -> None: + """Mount the chat-completions bridge. + + Deliberately **not** admin-gated: upstreams call ``/v1/chat/completions`` + exactly as they call the backing agent today (no AReaL admin key). The + internal gateway → router ``/route`` hop still carries the admin header. + """ + + @app.post("/v1/chat/completions") + async def chat_completions_endpoint(request: Request): + return await bridge.handle_request(request) + + @app.on_event("shutdown") + async def shutdown_chat_bridge(): + await bridge.close() diff --git a/areal/v2/agent_service/protocol.py b/areal/v2/agent_service/protocol.py index 47fb7a91e7..7668131987 100644 --- a/areal/v2/agent_service/protocol.py +++ b/areal/v2/agent_service/protocol.py @@ -18,6 +18,13 @@ from enum import Enum from typing import Any +# HTTP response header the Worker sets on a raw-passthrough turn (an agent that +# returns a :class:`~areal.v2.agent_service.types.StreamResponse`). +# The DataProxy keys its relay-vs-parse decision on this marker rather than on +# ``Content-Type`` — a *non-streaming* passthrough body is itself +# ``application/json`` and would otherwise be mistaken for a structured turn. +PASSTHROUGH_HEADER = "x-areal-passthrough" + class FrameType(str, Enum): """WebSocket frame type discriminator.""" diff --git a/areal/v2/agent_service/types.py b/areal/v2/agent_service/types.py index 7290634e67..e45f7a8199 100644 --- a/areal/v2/agent_service/types.py +++ b/areal/v2/agent_service/types.py @@ -4,6 +4,7 @@ from __future__ import annotations +from collections.abc import AsyncIterator from dataclasses import dataclass, field from typing import Any, Protocol, runtime_checkable @@ -16,6 +17,22 @@ class AgentRequest: Core fields are stable protocol-level attributes. Framework-specific parameters should go in *metadata*. + + Reserved metadata keys: + ``areal_inference``: present when the turn opts into AReaL's own + inference service for self-evolution (the turn carries the + inference-routing fields ``inf_base_url`` + ``session_api_key``). + Value is ``{"base_url", "api_key", "model"}`` where ``api_key`` is + the per-session ``sk-sess-*`` the **caller** obtained itself (e.g. + via its own ``/rl/start_session``) and passed in on the request. The + Agent Service does not talk to the training side; it merely + forwards these fields through. Agents should route their internal + LLM calls to this upstream so the trajectory's tokens/logprobs are + captured for training. + ``chat_request``: present on the ``/v1/chat/completions`` path; the + full original request body, so an agent that fronts an + OpenAI-compatible upstream can replay it verbatim and return a + :class:`StreamResponse` for byte-for-byte relay. """ message: str @@ -34,6 +51,33 @@ class AgentResponse: metadata: dict[str, Any] = field(default_factory=dict) +@dataclass +class StreamResponse: + """Raw streaming response from an agent, passed through verbatim. + + The structured channel (``run`` + :class:`EventEmitter` → ``AgentResponse``) + parses an agent's output into deltas/tool-calls. Some agents instead expose + an OpenAI-compatible upstream whose response (often SSE) must reach the + caller **byte-for-byte** — re-encoding it through the structured channel + would drop fields (tool_calls, finish_reason, usage, ...) and break clients + that expect the exact wire format. + + An agent opts into this behaviour by returning a :class:`StreamResponse` + (instead of an :class:`AgentResponse`) from :meth:`AgentRunnable.run`. The + Worker and DataProxy relay ``status_code`` / ``headers`` / ``body`` through + the single ``/run`` and ``/session/{key}/turn`` endpoints without inspecting + the payload — the Worker tags the response with the ``x-areal-passthrough`` + marker header so they relay it verbatim (structured turns are parsed). The + marker, not ``Content-Type``, drives the decision, so a *non-streaming* + passthrough whose body is ``application/json`` is still relayed byte-for-byte + rather than mistaken for a structured turn. + """ + + status_code: int + headers: dict[str, str] + body: AsyncIterator[bytes] + + class EventEmitter(Protocol): """Callback interface for streaming events from agent to caller.""" @@ -52,10 +96,32 @@ class AgentRunnable(Protocol): interaction — the Agent Service only provides session lifecycle and event streaming. - Reward computation is **not** part of this interface. Rewards are - calculated externally by the training pipeline (via reward functions - applied to exported trajectories), following AReaL's standard RLVR - pattern. + ``run`` is the single entry point and may return **either** shape, + chosen per turn by the agent: + + - :class:`AgentResponse` — the structured channel. The agent reports + incremental output through the ``emitter`` and returns a final + summary/metadata; the Worker serialises it to JSON and the DataProxy + rebuilds conversation history from the emitted events. This backs the + ``/v1/responses`` (and WebSocket) protocol. + - :class:`StreamResponse` — the raw-passthrough channel. The agent fronts + an OpenAI-compatible upstream whose response must reach the caller + byte-for-byte (e.g. SSE chat completions); it returns the upstream's + ``status_code`` / ``headers`` / ``body`` and the Worker / DataProxy relay + them verbatim. This backs the ``/v1/chat/completions`` protocol. + + The agent decides which to return from the request (e.g. the presence of + ``metadata['chat_request']``, or a ``stream`` flag in the original body), + so a single ``run`` implementation can serve every protocol and both + streaming and non-streaming modes. + + The following methods are optional and discovered via ``getattr`` at + runtime — implement them to participate in training-related lifecycle: + + - ``async close_session(session_key)`` — release per-session state + when a session is closed by the DataProxy. + - ``async close_all_sessions()`` — clean up everything on worker + shutdown. """ async def run( @@ -63,4 +129,4 @@ async def run( request: AgentRequest, *, emitter: EventEmitter, - ) -> AgentResponse: ... + ) -> AgentResponse | StreamResponse: ... diff --git a/areal/v2/agent_service/worker/app.py b/areal/v2/agent_service/worker/app.py index 55507bc558..6cb6881ccb 100644 --- a/areal/v2/agent_service/worker/app.py +++ b/areal/v2/agent_service/worker/app.py @@ -8,13 +8,18 @@ from typing import Any from fastapi import FastAPI -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, StreamingResponse from areal.utils import logging from areal.utils.dynamic_import import import_from_string -from ..protocol import QueueMode -from ..types import AgentRequest, AgentResponse, AgentRunnable +from ..protocol import PASSTHROUGH_HEADER, QueueMode +from ..types import ( + AgentRequest, + AgentResponse, + AgentRunnable, + StreamResponse, +) logger = logging.getLogger("AgentWorker") @@ -67,6 +72,20 @@ async def shutdown(): @app.post("/run") async def run(body: dict[str, Any]): + """Single agent entry point for every protocol and streaming mode. + + Calls the agent's ``run`` and relays whichever shape it returns: + + - :class:`StreamResponse` — raw passthrough; ``status_code`` / + ``headers`` / ``body`` are forwarded untouched so the caller gets the + upstream's exact wire format (e.g. SSE chat completions). The + response carries the :data:`PASSTHROUGH_HEADER` marker so the + DataProxy relays it verbatim instead of parsing it — this works even + for a *non-streaming* passthrough whose body is ``application/json``. + - :class:`AgentResponse` — structured JSON ``{summary, metadata, + events}`` (``application/json``); the DataProxy rebuilds history from + ``events``. + """ request = AgentRequest( message=body.get("message", ""), session_key=body.get("session_key", ""), @@ -79,7 +98,9 @@ async def run(body: dict[str, Any]): emitter = _CollectingEmitter() try: - response: AgentResponse = await agent.run(request, emitter=emitter) + response: AgentResponse | StreamResponse = await agent.run( + request, emitter=emitter + ) except Exception as exc: logger.exception("Agent run failed (session=%s)", request.session_key) return JSONResponse( @@ -87,6 +108,25 @@ async def run(body: dict[str, Any]): status_code=500, ) + if isinstance(response, StreamResponse): + # Drop hop-by-hop / length headers that would conflict with chunked + # relaying; FastAPI/uvicorn set framing headers themselves. + headers = { + k: v + for k, v in response.headers.items() + if k.lower() + not in ("content-length", "transfer-encoding", "connection") + } + # Mark the turn as raw-passthrough so the DataProxy relays it + # verbatim regardless of its Content-Type. + headers[PASSTHROUGH_HEADER] = "1" + return StreamingResponse( + response.body, + status_code=response.status_code, + headers=headers, + media_type=response.headers.get("content-type"), + ) + return {**asdict(response), "events": emitter.events} return app diff --git a/examples/hermes/README.md b/examples/hermes/README.md new file mode 100644 index 0000000000..7c01ef28a3 --- /dev/null +++ b/examples/hermes/README.md @@ -0,0 +1,192 @@ +# Agent Service — Hermes + +## Overview + +This example runs the **Hermes agent** +([Nous Research `hermes-agent`](https://github.com/nousresearch/hermes-agent)) inside +AReaL's Agent Service. Hermes ships as a Python library, so the Worker instantiates +**one in-process Hermes `AIAgent` per session** directly inside the Worker process — +**you never launch Hermes yourself**. The Agent Service is started with the +`areal agent run` CLI; you then interact and (optionally) train through a small set of +single-purpose scripts. + +``` +Client → Gateway (HTTP) → Router → DataProxy (session state) → Worker (Hermes AIAgent) +``` + +Each user message becomes one turn of the Hermes conversation; the per-session `AIAgent` +drives its configured OpenAI-compatible upstream LLM internally. AReaL's DataProxy owns +the conversation history and replays it into every turn, so the `AIAgent` is kept +**stateless across turns** (`skip_memory=True`, `skip_context_files=True`, +`session_db=None`). A consequence of this design: Hermes' own `memory` tool reports +"Memory is not available" — that is expected, since cross-turn context comes from the +DataProxy's replayed history, not from Hermes' persistence. + +This directory also contains the **RL training flow** (`train.py` + `config.yaml`). For +training, run `train.py` to bring up AReaL's trainer plus an inference gateway, then +point the Hermes turns at that gateway (via `hermes_loop.py`'s inference-routing flags) +so every LLM call is captured as a training trajectory. + +> **See also** +> +> - [Agentic RL tutorial](../../docs/en/tutorial/agentic_rl.md) — background on how +> AReaL trains agents +> - [Custom agent workflows](../../docs/en/customization/agent.md) — how to integrate +> your own agent framework +> - [Agent workflow reference](../../docs/en/reference/agent_workflow.md) — internal +> architecture details + +**Disclaimer**: RL-finetuned models may exhibit unexpected behaviors. Please ensure +strict permission rules and an isolated execution environment for your agent runtime. + +## How it fits together + +``` +┌──────────────────────────────────────┐ LLM calls (self-evolution) ┌────────────────────────┐ +│ Agent Service │ ─────────────────────────────▶ │ AReaL inference gateway│ +│ (areal agent run) │ inf_base_url = http:// │ (started by train.py) │ +│ Gateway/Router/DataProxy/Worker │ session key = sk-sess-* │ │ +│ + in-process Hermes AIAgent │ ◀───────────────────────────── │ records tokens + │ +└──────────────────────────────────────┘ model output │ logprobs → RL │ + ▲ └────────────────────────┘ + │ hermes_loop.py │ + │ (the interactive "You:" prompt) set_reward.py (score the trajectory) +``` + +One **episode** = the turns collected under a single per-session `sk-sess-*` key (minted +by `start_session.py`). You score it with `set_reward.py`, then start the next episode. + +## Prerequisites + +### 1. GPUs (for RL training only) + +A GPU machine with at least **2 NVIDIA GPUs** (compute capability 8.0 or higher, i.e. +Ampere / Hopper). Not required if you only run the agent against an env upstream LLM +(plain chat, no training). + +### 2. Install Hermes into AReaL's venv + +Hermes' top-level module is `run_agent`. The Worker process is forked with +`sys.executable` (the interpreter you launch the controller with), so **`areal` and +`run_agent` must be importable from the same venv**: + +```bash +uv pip install hermes-agent +python -c "import areal; from run_agent import AIAgent; print('co-import OK')" +``` + +A bare `hermes-agent` install is moderate, **not** heavy: every large optional +integration (`anthropic`, `slack`, `matrix`, `modal`, browser/messaging, …) sits behind +a `pip install hermes-agent[extra]` marker and is not pulled in. No torch/CUDA/ML +packages are added. + +> **Gotchas** +> +> - **Run with the project `.venv` python directly** (`python` / `.venv/bin/python`), +> **not `uv run`** — `uv run` re-syncs the env to `uv.lock` and resets the shared +> packages (`openai`, `pydantic`, `rich`, …) to AReaL's pinned versions. +> - **Do not run `uv sync`** while you need Hermes — it removes `hermes-agent` (it is +> not in `uv.lock`). Re-add with `uv pip install hermes-agent`. + +## Quick start — plain chat + +The five steps below are run from the repo root. + +### Step 1 — Start the training service (embeds the inference gateway) + +`config.yaml` holds the defaults (v2 controllers, 1 node × 2 GPUs, `batch_size=1`, admin +keys); CLI flags override it. The explicit form below just documents those defaults: + +```bash +uv run python3 examples/hermes/train.py \ + --config examples/hermes/config.yaml \ + actor.path=/path/to/your_model \ + actor.admin_api_key=sk-123456 \ + rollout.admin_api_key=sk-123456 +``` + +Note this address from the logs — it is your `` below: + +``` +Proxy gateway available at http://X.X.X.X:PORT +``` + +> **Key wiring** +> +> `rollout.admin_api_key` is the **inference gateway** admin key — reuse it for +> `start_session.py --admin-key` and `set_reward.py` (Steps 3 and 5). +> `actor.admin_api_key` is the trainer/actor key, unused by the interaction scripts. See +> the [CLI reference](../../docs/en/cli_reference.md) and +> [allocation mode reference](../../docs/en/reference/alloc_mode.md) for fields and GPU +> layout. + +### Step 2 — Start the Hermes Agent Service + +Same command as the quick start. The agent's env upstream is optional here — once +self-evolution fields are supplied per turn (Step 4), the inference gateway upstream +takes over. + +```bash +areal agent run \ + --service default \ + --agent examples.hermes.hermes.HermesAgent \ + --num-pairs 1 \ + --admin-api-key sk-123456 +``` + +Note the printed `` address. + +### Step 3 — Start a session on the inference gateway + +Copy the printed `sk-sess-*` key — forward it to the agent (Step 4) and score the +episode with it (Step 5). To reuse the key for the next episode (auto-ends and exports +the previous one): + +```bash +python examples/hermes/start_session.py http:// --admin-key sk-123456 +``` + +These are **your own upstream LLM credentials** (the agent's fallback chat backend), +**not** the `sk-sess-*` key returned above — fill in your provider's values: + +```bash +export HERMES_UPSTREAM_BASE_URL="https://your-llm/v1" +export HERMES_UPSTREAM_API_KEY="your-upstream-api-key" +export HERMES_UPSTREAM_MODEL="your-model" +``` + +### Step 4 — Interact (produces a trajectory) + +Forward the inference-routing flags so the agent's LLM calls flow through the inference +gateway under your session key and get captured. You **must** actually interact, or the +episode has no data. `` is the `sk-sess-*` key returned by +`start_session.py` in Step 3. + +```bash +python examples/hermes/hermes_loop.py http:// \ + --admin-api-key sk-123456 \ + --inf-base-url http:// \ + --session-api-key +``` + +### Step 5 — Score the episode + +Use the same `sk-sess-*` key from Step 3 as `--api-key`: + +```bash +python examples/hermes/set_reward.py http:// \ + --api-key --reward 1.0 +``` + +Keep the reward in **\[-1, 1\]** for training stability. + +## Files + +| File | Description | +| ------------------ | ---------------------------------------------------------------- | +| `hermes.py` | `HermesAgent` — in-process per-session Hermes `AIAgent` runnable | +| `hermes_loop.py` | Standalone interactive `You:` prompt against the agent gateway | +| `start_session.py` | Mint a per-session `sk-sess-*` key on the inference gateway | +| `set_reward.py` | Assign a scalar reward to a session's trajectory | +| `train.py` | RL trainer entry point (embeds the inference gateway) | +| `config.yaml` | Training configuration (v2 controllers, 2-GPU defaults) | diff --git a/examples/hermes/__init__.py b/examples/hermes/__init__.py new file mode 100644 index 0000000000..acba72f988 --- /dev/null +++ b/examples/hermes/__init__.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Hermes example: an in-process per-session :class:`AgentRunnable`. + +Hosts the Hermes agent runtime that the Worker loads via ``agent_cls_path`` +(``examples.hermes.hermes.HermesAgent``). Unlike the OpenClaw +example (one Gateway subprocess per session), Hermes is a Python library, so +this runtime instantiates one Hermes ``AIAgent`` per RL session directly inside +the Worker process. +""" diff --git a/examples/hermes/config.yaml b/examples/hermes/config.yaml new file mode 100644 index 0000000000..91ca290c52 --- /dev/null +++ b/examples/hermes/config.yaml @@ -0,0 +1,164 @@ +experiment_name: online-rl +trial_name: trial0 + +seed: 1 +enable_offload: false +total_train_epochs: 10 +total_train_steps: 100 +tokenizer_path: ${actor.path} + +cluster: + n_nodes: 1 + n_gpus_per_node: 2 + fileroot: /tmp/areal/experiments + name_resolve: + type: nfs + nfs_record_root: /tmp/areal/name_resolve + +scheduler: + type: local + +rollout: + backend: "sglang:d1" + _version: v2 + experiment_name: ${experiment_name} + trial_name: ${trial_name} + max_concurrent_rollouts: 256 + queue_size: null + consumer_batch_size: ${train_dataset.batch_size} + max_head_offpolicyness: 2 + enable_rollout_tracing: false + scheduling_spec: ${actor.scheduling_spec} + fileroot: ${cluster.fileroot} + tokenizer_path: ${tokenizer_path} + dump_to_file: false + agent: + mode: online + tool_call_parser: qwen + reasoning_parser: qwen3 + export_style: individual + turn_discount: 1.0 + admin_api_key: sk-xxx + +gconfig: + n_samples: 1 + min_new_tokens: 0 + max_new_tokens: 2048 + greedy: false + temperature: 1.0 + +actor: + backend: "megatron:d1" + _version: v2 + admin_api_key: gsm8k-123 + experiment_name: ${experiment_name} + trial_name: ${trial_name} + path: Qwen/Qwen2.5-1.5B-Instruct + init_from_scratch: false + disable_dropout: true + gradient_checkpointing: true + dtype: bfloat16 + mb_spec: + max_tokens_per_mb: 15360 + optimizer: + type: adam + lr: 1.70e-5 + weight_decay: 0.017 + beta1: 0.9 + beta2: 0.999 + eps: 1e-8 + lr_scheduler_type: constant + gradient_clipping: 1.0 + warmup_steps_proportion: 0.001 + eps_clip: 0.4 + temperature: ${gconfig.temperature} + reward_scaling: 10.0 + reward_bias: -0.5 + kl_ctl: 0.0 + ppo_n_minibatches: 1 + recompute_logprob: true + use_decoupled_loss: true + rejection_sampling: + metric: ratio + upper: 5.0 + reward_norm: + mean_level: group + std_level: group + group_size: ${gconfig.n_samples} + adv_norm: + mean_level: batch + std_level: batch + weight_update_mode: xccl + max_new_tokens: ${gconfig.max_new_tokens} + scheduling_spec: + - task_type: worker + port_count: 2 + gpu: 1 + mem: 32 + cmd: python3 -m areal.infra.rpc.rpc_server + env_vars: {} + +# SGLang +sglang: + model_path: ${actor.path} + random_seed: ${seed} + skip_tokenizer_init: true + dtype: ${actor.dtype} + max_running_requests: null + context_length: 32768 + mem_fraction_static: 0.8 + +vllm: + model: ${actor.path} + seed: ${seed} + skip_tokenizer_init: false + dtype: ${actor.dtype} + max_model_len: 32768 + gpu_memory_utilization: 0.8 + +# datasets +train_dataset: + batch_size: 1 + path: "null" + type: rl + +# Utilities +saver: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +recover: + mode: disabled + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: 3600 + +evaluator: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: null + freq_secs: null + +stats_logger: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + wandb: + mode: disabled + +perf_tracer: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + enabled: false + session_tracer: + enabled: false diff --git a/examples/hermes/hermes.py b/examples/hermes/hermes.py new file mode 100644 index 0000000000..f7ac4e8280 --- /dev/null +++ b/examples/hermes/hermes.py @@ -0,0 +1,444 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Hermes Agent for AReaL Agent Service (in-process per-session ``AIAgent``). + +Implements :class:`AgentRunnable` by instantiating **one Hermes ``AIAgent`` +per RL session** directly inside the Worker process. Unlike OpenClaw — which +exposes an OpenAI-compatible Gateway subprocess — Hermes +(``github.com/nousresearch/hermes-agent``) is a Python library whose +``AIAgent`` drives an OpenAI-compatible upstream itself. Each per-session +agent is therefore bound to its own upstream LLM (base URL + API key + model) +so that, during training, a session's turns can be attributed to a distinct +per-session key (``sk-sess-*``). + +Per turn the agent calls ``AIAgent.run_conversation`` with the conversation +history the DataProxy replays. Hermes rebuilds its message list from that +history on every call (it does not retain messages across calls), so the +replayed history stays the single source of truth and one cached agent can +safely serve every turn of a session. + +Requires the ``hermes-agent`` package importable as the top-level ``run_agent`` +module (``pip install hermes-agent``). + +Upstream selection +------------------ +Per turn the agent prefers the inference upstream the DataProxy injects via +``AgentRequest.metadata['areal_inference']`` (``base_url`` / ``api_key`` / +``model``), so a session's LLM calls flow through AReaL's inference service +under a per-session ``sk-sess-*`` key and get captured for training. Outside +training (e.g. the interactive demo) it falls back to the ``HERMES_UPSTREAM_*`` +environment variables. + +Environment variables +--------------------- + HERMES_UPSTREAM_BASE_URL — upstream LLM base URL (fallback when no + ``areal_inference`` metadata). + HERMES_UPSTREAM_API_KEY — upstream API key. + HERMES_UPSTREAM_MODEL — upstream model id (default ``default``). + HERMES_MAX_TURNS — max tool-calling iterations per turn (default 10). + HERMES_ENABLED_TOOLSETS — comma-separated toolsets to enable (optional). + HERMES_DISABLED_TOOLSETS — comma-separated toolsets to disable (optional). +""" + +from __future__ import annotations + +import asyncio +import json +import os +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +from areal.utils import logging +from areal.v2.agent_service.types import ( + AgentRequest, + AgentResponse, + EventEmitter, + StreamResponse, +) + +logger = logging.getLogger("HermesAgent") + + +def _split_csv(value: str) -> list[str] | None: + items = [item.strip() for item in value.split(",") if item.strip()] + return items or None + + +@dataclass +class _Upstream: + """Upstream LLM that a Hermes ``AIAgent`` routes its calls to.""" + + base_url: str + api_key: str + model: str + + @classmethod + def from_inference(cls, meta: dict[str, Any]) -> _Upstream | None: + """Build an upstream from a turn's ``metadata['areal_inference']``. + + The DataProxy injects this when a turn opts into AReaL's inference + service, so the session's LLM calls flow through the gateway under a + per-session ``sk-sess-*`` key and get captured for training. + """ + base_url = meta.get("base_url") or "" + api_key = meta.get("api_key") or "" + if not base_url or not api_key: + return None + model = meta.get("model") or "default" + return cls(base_url=base_url.rstrip("/"), api_key=api_key, model=model) + + @classmethod + def from_env(cls) -> _Upstream | None: + base_url = os.environ.get("HERMES_UPSTREAM_BASE_URL", "") + api_key = os.environ.get("HERMES_UPSTREAM_API_KEY", "") + if not base_url or not api_key: + return None + model = os.environ.get("HERMES_UPSTREAM_MODEL", "default") + return cls(base_url=base_url.rstrip("/"), api_key=api_key, model=model) + + +@dataclass +class _SessionState: + session_key: str + agent: Any # Hermes ``AIAgent`` instance. + upstream: _Upstream | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + +class HermesAgent: + """AgentRunnable that runs one in-process Hermes ``AIAgent`` per session.""" + + def __init__(self, **_: Any) -> None: + self._max_turns = int(os.environ.get("HERMES_MAX_TURNS", "10")) + self._enabled_toolsets = _split_csv( + os.environ.get("HERMES_ENABLED_TOOLSETS", "") + ) + self._disabled_toolsets = _split_csv( + os.environ.get("HERMES_DISABLED_TOOLSETS", "") + ) + self._env_upstream = _Upstream.from_env() + + self._sessions: dict[str, _SessionState] = {} + self._locks: dict[str, asyncio.Lock] = {} + self._locks_guard = asyncio.Lock() + + logger.info( + "HermesAgent initialized (max_turns=%d, env_upstream=%s)", + self._max_turns, + self._env_upstream is not None, + ) + + # ------------------------------------------------------------------ + # AIAgent lifecycle + # ------------------------------------------------------------------ + + def _build_agent(self, upstream: _Upstream) -> Any: + """Construct a headless Hermes ``AIAgent`` bound to ``upstream``. + + Imported lazily so this module is importable (and unit-testable) + without the ``hermes-agent`` package installed. The persistence and + context-injection flags are disabled so the agent stays stateless + across turns — AReaL's DataProxy owns the conversation history. + """ + from run_agent import AIAgent # type: ignore[import-not-found] + + return AIAgent( + base_url=upstream.base_url, + api_key=upstream.api_key, + model=upstream.model, + max_iterations=self._max_turns, + enabled_toolsets=self._enabled_toolsets, + disabled_toolsets=self._disabled_toolsets, + save_trajectories=False, + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + session_db=None, + ) + + async def _session_lock(self, session_key: str) -> asyncio.Lock: + async with self._locks_guard: + lock = self._locks.get(session_key) + if lock is None: + lock = asyncio.Lock() + self._locks[session_key] = lock + return lock + + async def _teardown_state(self, state: _SessionState) -> None: + # ``AIAgent`` cleanup is best-effort: not every build exposes a + # ``close``, and a failing teardown must not mask the caller's intent. + close = getattr(state.agent, "close", None) + if callable(close): + try: + result = close() + if asyncio.iscoroutine(result): + await result + except Exception: # noqa: BLE001 - teardown must not raise + logger.debug("AIAgent.close() failed", exc_info=True) + + async def _ensure_session( + self, + session_key: str, + upstream: _Upstream, + *, + rebuild: bool = False, + ) -> _SessionState: + lock = await self._session_lock(session_key) + async with lock: + existing = self._sessions.get(session_key) + if existing is not None: + if not rebuild: + return existing + await self._teardown_state(self._sessions.pop(session_key)) + # Building an AIAgent loads toolsets and is comparatively heavy; + # run it off the event loop so it does not block other sessions. + agent = await asyncio.to_thread(self._build_agent, upstream) + state = _SessionState( + session_key=session_key, agent=agent, upstream=upstream + ) + self._sessions[session_key] = state + logger.info( + "Built Hermes AIAgent (session=%s, model=%s)", + session_key, + upstream.model, + ) + return state + + # ------------------------------------------------------------------ + # Lifecycle hooks (optional members of the AgentRunnable protocol) + # ------------------------------------------------------------------ + + async def close_session(self, session_key: str) -> None: + state = self._sessions.pop(session_key, None) + if state is not None: + await self._teardown_state(state) + # Drop the per-session lock so ``_locks`` does not grow unbounded as + # sessions are created and destroyed over a long-running worker. + async with self._locks_guard: + self._locks.pop(session_key, None) + + async def close_all_sessions(self) -> None: + for key in list(self._sessions.keys()): + await self.close_session(key) + + # ------------------------------------------------------------------ + # Per-turn execution + # ------------------------------------------------------------------ + + async def _resolve_state(self, request: AgentRequest) -> _SessionState: + """Pick (or build) the Hermes ``AIAgent`` backing this turn. + + Session-first: an existing per-session agent is reused as-is, so a turn + need not re-supply its upstream once the session is live. When no agent + exists yet, an upstream must be resolvable — preferring the per-session + inference routing the DataProxy injected (self-evolution, calls flow + through AReaL's inference service under a ``sk-sess-*`` key), else the + process-wide env upstream (e.g. the interactive demo). If a live + session's upstream changes (env → AReaL inference), rebuild so the agent + routes to the new endpoint/key. + """ + inf_meta = (request.metadata or {}).get("areal_inference") + upstream = ( + _Upstream.from_inference(inf_meta) if inf_meta else None + ) or self._env_upstream + + state = self._sessions.get(request.session_key) + if state is None: + if upstream is None: + raise RuntimeError( + f"No agent for session '{request.session_key}': send the turn " + "with inference-routing fields ('inf_base_url' + " + "'session_api_key', so the DataProxy injects an inference " + "upstream) or set the HERMES_UPSTREAM_* env." + ) + return await self._ensure_session(request.session_key, upstream) + if upstream is not None and state.upstream != upstream: + return await self._ensure_session( + request.session_key, upstream, rebuild=True + ) + return state + + async def _run_conversation( + self, + state: _SessionState, + message: str, + history: list[dict[str, Any]], + ) -> dict[str, Any]: + """Run one blocking Hermes turn off the event loop, serialized per session. + + ``run_conversation`` is synchronous and mutates agent state, so the + per-session lock keeps a session's turns from overlapping. The full + replayed history is passed every turn (Hermes rebuilds its message list + from it), keeping the DataProxy's history the single source of truth. + """ + lock = await self._session_lock(state.session_key) + async with lock: + return await asyncio.to_thread( + state.agent.run_conversation, + message, + conversation_history=history or None, + task_id=f"areal-{uuid.uuid4().hex}", + ) + + @staticmethod + def _collect_tool_calls(result: dict[str, Any]) -> list[dict[str, Any]]: + """Surface tool calls from the result transcript for observability. + + Like the OpenClaw runtime, tool calls are reported in ``metadata`` only + and deliberately NOT emitted as ``tool_call`` events: Hermes executes + tools internally, so emitting a ``tool_call`` without the framework also + feeding a paired ``tool_result`` would make the DataProxy build an + invalid chat-completions history that the upstream rejects on replay. + """ + tool_calls: list[dict[str, Any]] = [] + for msg in result.get("messages") or []: + if msg.get("role") != "assistant": + continue + for call in msg.get("tool_calls") or []: + fn = call.get("function") or {} + tool_calls.append( + { + "name": fn.get("name", ""), + "input": fn.get("arguments", ""), + } + ) + return tool_calls + + async def run( + self, + request: AgentRequest, + *, + emitter: EventEmitter, + ) -> AgentResponse | StreamResponse: + """Single entry point for both the structured and chat-completions channels. + + The ``/v1/chat/completions`` bridge stashes the original request body + under ``metadata['chat_request']``; its presence selects the + chat-completions channel (return a :class:`StreamResponse` carrying an + OpenAI-compatible response synthesized from Hermes' final text). + Otherwise the structured channel runs: report the final text through + ``emitter`` and return an :class:`AgentResponse`. + """ + state = await self._resolve_state(request) + + chat_request = (request.metadata or {}).get("chat_request") + if chat_request is not None: + return await self._chat_completions(state, dict(chat_request)) + + result = await self._run_conversation(state, request.message, request.history) + final = result.get("final_response") or "" + if final: + await emitter.emit_delta(final) + + return AgentResponse( + summary=final, + metadata={ + "tool_calls": self._collect_tool_calls(result), + "completed": bool(result.get("completed")), + "api_calls": result.get("api_calls", 0), + }, + ) + + # ------------------------------------------------------------------ + # Chat-completions channel + # ------------------------------------------------------------------ + + @staticmethod + def _extract_chat(chat_body: dict[str, Any]) -> tuple[str, list[dict[str, Any]]]: + """Split a chat-completions body into (last user message, prior history).""" + messages = list(chat_body.get("messages") or []) + message = "" + for idx in range(len(messages) - 1, -1, -1): + if messages[idx].get("role") == "user": + content = messages[idx].get("content", "") + if isinstance(content, list): + content = "".join( + block.get("text", "") + for block in content + if isinstance(block, dict) and block.get("type") == "text" + ) + message = content or "" + history = messages[:idx] + return message, history + return message, messages + + async def _chat_completions( + self, + state: _SessionState, + chat_body: dict[str, Any], + ) -> StreamResponse: + """Chat-completions channel for the ``/v1/chat/completions`` bridge. + + Hermes has no OpenAI-compatible HTTP server to relay byte-for-byte, so + this runs the structured turn and re-encodes Hermes' final text into the + OpenAI chat-completions wire format (SSE when ``stream`` is set, a single + JSON object otherwise), returned as a :class:`StreamResponse` the Worker + and DataProxy relay verbatim. + """ + message, history = self._extract_chat(chat_body) + result = await self._run_conversation(state, message, history) + final = result.get("final_response") or "" + model = chat_body.get("model") or ( + state.upstream.model if state.upstream else "hermes-agent" + ) + stream = bool(chat_body.get("stream")) + + if stream: + return self._chat_sse_response(final, model) + return self._chat_json_response(final, model) + + @staticmethod + def _chat_json_response(content: str, model: str) -> StreamResponse: + payload = { + "id": f"chatcmpl-{uuid.uuid4().hex}", + "object": "chat.completion", + "created": int(time.time()), + "model": model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": content}, + "finish_reason": "stop", + } + ], + } + body = json.dumps(payload).encode("utf-8") + + async def _iter() -> Any: + yield body + + return StreamResponse( + status_code=200, + headers={"content-type": "application/json"}, + body=_iter(), + ) + + @staticmethod + def _chat_sse_response(content: str, model: str) -> StreamResponse: + cmpl_id = f"chatcmpl-{uuid.uuid4().hex}" + created = int(time.time()) + + def _chunk(delta: dict[str, Any], finish: str | None) -> bytes: + payload = { + "id": cmpl_id, + "object": "chat.completion.chunk", + "created": created, + "model": model, + "choices": [{"index": 0, "delta": delta, "finish_reason": finish}], + } + return f"data: {json.dumps(payload)}\n\n".encode() + + async def _iter() -> Any: + yield _chunk({"role": "assistant"}, None) + if content: + yield _chunk({"content": content}, None) + yield _chunk({}, "stop") + yield b"data: [DONE]\n\n" + + return StreamResponse( + status_code=200, + headers={"content-type": "text/event-stream"}, + body=_iter(), + ) diff --git a/examples/hermes/hermes_loop.py b/examples/hermes/hermes_loop.py new file mode 100644 index 0000000000..6c764efd7c --- /dev/null +++ b/examples/hermes/hermes_loop.py @@ -0,0 +1,138 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Interactive conversation loop against a running Hermes Agent Service. + +This is the standalone chat window: it assumes the Agent Service is **already +running** (started separately with ``areal agent run``) and a per-session +``sk-sess-*`` key has **already been minted** on the inference gateway (with +``start_session.py``). It only opens a ``You:`` prompt and drives the agent. + +Each user message becomes one turn of the Hermes conversation, posted to the +agent gateway's ``/v1/responses``. When ``--inf-base-url`` and +``--session-api-key`` are supplied, every turn forwards the inference-routing +fields so the agent's LLM calls flow through AReaL's inference service under +that key and the trajectory is captured for training; score it afterwards with +``set_reward.py``. + +Usage:: + + # plain chat (env upstream; agent admin key authorizes the gateway) + python hermes_loop.py http:// --admin-api-key + + # self-evolution: capture the trajectory under a session key + python hermes_loop.py http:// --admin-api-key \\ + --inf-base-url http:// --session-api-key sk-sess-xxxx +""" + +from __future__ import annotations + +import argparse +import asyncio +import time + +import httpx + + +async def interactive_loop( + gateway_addr: str, + admin_key: str, + inference: dict[str, str] | None = None, +) -> None: + session_key = f"hermes-{int(time.time())}" + print(f"Session: {session_key}") + if inference: + print(f"Self-evolution: routing LLM calls via {inference['inf_base_url']}") + print("Type your message (or 'quit' to exit):\n") + + async with httpx.AsyncClient(timeout=120.0) as client: + while True: + try: + user_input = input("You: ") + except (EOFError, KeyboardInterrupt): + break + if user_input.strip().lower() in ("quit", "exit", "q"): + break + if not user_input.strip(): + continue + + payload: dict[str, object] = { + "input": [{"type": "message", "content": user_input}], + "model": "hermes-agent", + "user": session_key, + } + if inference: + payload.update(inference) + + resp = await client.post( + f"{gateway_addr}/v1/responses", + json=payload, + headers={"Authorization": f"Bearer {admin_key}"}, + ) + data = resp.json() + + if data.get("status") == "completed": + for item in data.get("output", []): + if item.get("type") == "message": + for block in item.get("content", []): + if block.get("type") == "output_text": + print(f"Agent: {block['text']}") + elif item.get("type") == "function_call": + print(f"[tool] {item.get('name', '')}") + print() + elif data.get("error"): + print(f"Error: {data['error'].get('message', '')[:200]}\n") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Interactive chat against a running Hermes Agent Service" + ) + parser.add_argument( + "gateway_addr", + help="Agent Service gateway URL (from 'areal agent status')", + ) + parser.add_argument( + "--admin-api-key", + required=True, + help="Agent Service admin key (the --admin-api-key passed to 'areal agent run')", + ) + parser.add_argument( + "--inf-base-url", + default="", + help="Inference gateway base URL (enables self-evolution capture)", + ) + parser.add_argument( + "--inf-model", + default="", + help="Model id the agent requests from the inference service", + ) + parser.add_argument( + "--session-api-key", + default="", + help="Per-session sk-sess-* key from start_session.py (required for capture)", + ) + args = parser.parse_args() + + gateway_addr = args.gateway_addr.rstrip("/") + + inference: dict[str, str] | None = None + if args.inf_base_url or args.session_api_key: + if not (args.inf_base_url and args.session_api_key): + raise SystemExit( + "self-evolution requires BOTH --inf-base-url and --session-api-key" + ) + inference = { + "inf_base_url": args.inf_base_url.rstrip("/"), + "inf_model": args.inf_model, + "session_api_key": args.session_api_key, + } + + asyncio.run( + interactive_loop( + gateway_addr, admin_key=args.admin_api_key, inference=inference + ) + ) + + +if __name__ == "__main__": + main() diff --git a/examples/hermes/set_reward.py b/examples/hermes/set_reward.py new file mode 100644 index 0000000000..6c5b3a70b9 --- /dev/null +++ b/examples/hermes/set_reward.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Set reward for the last interaction in an active session. + +Requires the per-session ``sk-sess-*`` key minted by ``start_session.py``. + +Usage: + python set_reward.py http://host:port --api-key --reward 1.0 + python set_reward.py http://host:port --api-key --reward 0.0 --interaction-id cmpl_abc +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +import requests + +# ============================================================================= +# CLI formatting helpers (ANSI colors + output helpers) +# ============================================================================= +BOLD = "\033[1m" +DIM = "\033[2m" +RESET = "\033[0m" +GREEN = "\033[0;32m" +CYAN = "\033[0;36m" +RED = "\033[0;31m" +BLUE = "\033[0;34m" + + +def info(msg: str) -> None: + print(f" {CYAN}ℹ{RESET} {msg}") + + +def success(msg: str) -> None: + print(f" {GREEN}✔{RESET} {msg}") + + +def error(msg: str) -> None: + print(f" {RED}✘{RESET} {msg}") + + +def dim(msg: str) -> None: + print(f" {DIM}{msg}{RESET}") + + +def die(msg: str) -> None: + error(msg) + sys.exit(1) + + +def show_request(method: str, path: str, auth_label: str, gateway_url: str) -> None: + print(f" {DIM}{method} {gateway_url}/{path}{RESET}") + print(f" {DIM}Auth: {auth_label}{RESET}") + + +def show_response(status_code: int, body: str) -> None: + if 200 <= status_code < 300: + print(f" {GREEN}HTTP {status_code}{RESET}") + else: + print(f" {RED}HTTP {status_code}{RESET}") + if body: + try: + formatted = json.dumps(json.loads(body), indent=2) + for line in formatted.split("\n"): + print(f" {DIM}{line}{RESET}") + except (json.JSONDecodeError, ValueError): + for line in body.split("\n"): + print(f" {DIM}{line}{RESET}") + + +def header(title: str) -> None: + """Print a boxed header.""" + print() + print( + f"{BOLD}{BLUE}══════════════════════════════════════════════════════════════{RESET}" + ) + print(f"{BOLD}{BLUE} {title}{RESET}") + print( + f"{BOLD}{BLUE}══════════════════════════════════════════════════════════════{RESET}" + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Set reward for an interaction in an AReaL RL session" + ) + parser.add_argument("gateway_url", help="Proxy gateway URL") + parser.add_argument( + "--api-key", + required=True, + help="Per-session sk-sess-* key from start_session.py", + ) + parser.add_argument( + "--reward", + type=float, + default=float(os.getenv("REWARD", "1.0")), + help="Reward value (default: 1.0, env: REWARD)", + ) + parser.add_argument( + "--interaction-id", + default=None, + help="Specific interaction ID (default: last interaction)", + ) + args = parser.parse_args() + + header("Set Reward") + info(f"Assigning reward={args.reward} to the last interaction.") + show_request("POST", "rl/set_reward", "Bearer ***", args.gateway_url) + + reward_body: dict = {"reward": args.reward} + if args.interaction_id is not None: + reward_body["interaction_id"] = args.interaction_id + + dim(f"Request body: {json.dumps(reward_body)}") + + try: + resp = requests.post( + f"{args.gateway_url}/rl/set_reward", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {args.api_key}", + }, + json=reward_body, + timeout=10, + ) + except requests.RequestException as e: + die(f"Failed to reach gateway: {e}") + + show_response(resp.status_code, resp.text) + + if resp.status_code != 200: + die(f"set_reward failed (HTTP {resp.status_code}).") + + success(f"Reward {args.reward} applied") + + +if __name__ == "__main__": + main() diff --git a/examples/hermes/start_session.py b/examples/hermes/start_session.py new file mode 100644 index 0000000000..ff29a7fb66 --- /dev/null +++ b/examples/hermes/start_session.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Start an RL session on AReaL's inference gateway. + +Mints a per-session ``sk-sess-*`` key on the inference gateway that +``train.py`` embeds, then prints it. You forward that key to the Hermes +Agent Service (``hermes_loop.py``) so the agent's LLM calls flow through the +inference service under it and get captured as a training trajectory; you then +score it with ``set_reward.py``. + +The inference gateway's admin key is whatever you passed as +``rollout.admin_api_key`` to ``train.py``. + +Pass ``--api-key`` with a previously issued key to **refresh** an existing +session: the old session is ended (default reward 0 if none was set), its +trajectory is exported, and a new session is started reusing the same key. + +Usage: + python start_session.py http://host:port --admin-key sk-xxx + python start_session.py http://host:port --admin-key sk-xxx --task-id my-task + python start_session.py http://host:port --admin-key sk-xxx --api-key +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +import requests + +# ============================================================================= +# CLI formatting helpers (ANSI colors + output helpers) +# ============================================================================= +BOLD = "\033[1m" +DIM = "\033[2m" +RESET = "\033[0m" +GREEN = "\033[0;32m" +CYAN = "\033[0;36m" +YELLOW = "\033[0;33m" +RED = "\033[0;31m" +BLUE = "\033[0;34m" + + +def info(msg: str) -> None: + print(f" {CYAN}ℹ{RESET} {msg}") + + +def success(msg: str) -> None: + print(f" {GREEN}✔{RESET} {msg}") + + +def error(msg: str) -> None: + print(f" {RED}✘{RESET} {msg}") + + +def arrow(msg: str) -> None: + print(f" {YELLOW}→{RESET} {msg}") + + +def die(msg: str) -> None: + error(msg) + sys.exit(1) + + +def show_request(method: str, path: str, auth_label: str, gateway_url: str) -> None: + print(f" {DIM}{method} {gateway_url}/{path}{RESET}") + print(f" {DIM}Auth: {auth_label}{RESET}") + + +def show_response(status_code: int, body: str) -> None: + if 200 <= status_code < 300: + print(f" {GREEN}HTTP {status_code}{RESET}") + else: + print(f" {RED}HTTP {status_code}{RESET}") + if body: + try: + formatted = json.dumps(json.loads(body), indent=2) + for line in formatted.split("\n"): + print(f" {DIM}{line}{RESET}") + except (json.JSONDecodeError, ValueError): + for line in body.split("\n"): + print(f" {DIM}{line}{RESET}") + + +def header(title: str) -> None: + """Print a boxed header.""" + print() + print( + f"{BOLD}{BLUE}══════════════════════════════════════════════════════════════{RESET}" + ) + print(f"{BOLD}{BLUE} {title}{RESET}") + print( + f"{BOLD}{BLUE}══════════════════════════════════════════════════════════════{RESET}" + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Start an AReaL RL session") + parser.add_argument("gateway_url", help="Inference gateway URL") + parser.add_argument( + "--admin-key", + default=os.getenv("ADMIN_KEY", "areal-admin-key"), + help="Inference gateway admin key (== rollout.admin_api_key; env: ADMIN_KEY)", + ) + parser.add_argument( + "--task-id", + default=os.getenv("TASK_ID", "demo-task"), + help="Task identifier (env: TASK_ID)", + ) + parser.add_argument( + "--api-key", + default=os.getenv("SESSION_API_KEY"), + help="Reuse a previously issued API key (refresh). (env: SESSION_API_KEY)", + ) + args = parser.parse_args() + + is_refresh = args.api_key is not None + header("Refresh Session" if is_refresh else "Start Session") + if is_refresh: + info( + "Refreshing: end old session → export trajectory → start new session (same key)" + ) + else: + info("Requesting a new RL session (admin auth → gateway routes to a worker)") + show_request("POST", "rl/start_session", "Bearer ***", args.gateway_url) + + try: + resp = requests.post( + f"{args.gateway_url}/rl/start_session", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {args.admin_key}", + }, + json={"task_id": args.task_id, "api_key": args.api_key}, + timeout=130 if is_refresh else 10, + ) + except requests.RequestException as e: + die(f"Failed to reach gateway: {e}") + + show_response(resp.status_code, resp.text) + + if resp.status_code != 201: + if resp.status_code == 429 and is_refresh: + die( + "Refresh timed out — the training pipeline hasn't cycled yet. " + "Retry in a few seconds." + ) + die( + "start_session failed. " + "If HTTP 429, no capacity — the RL trainer hasn't granted capacity yet." + ) + + # v2 inference gateway returns 201 with a flat list of session credentials: + # {"group_id": ..., "sessions": [{"session_id": ..., "session_api_key": ...}]} + try: + data = resp.json() + session = data["sessions"][0] + session_api_key = session["session_api_key"] + session_id = session["session_id"] + except (ValueError, KeyError, IndexError) as e: + die(f"Failed to parse response: {e}") + + success("Session started!") + arrow(f"Session ID : {BOLD}{session_id}{RESET}") + arrow(f"API Key : {BOLD}{session_api_key}{RESET}") + print() + info("Forward this key to the Hermes Agent Service to capture the trajectory:") + print() + print( + f" python hermes_loop.py " + f" --admin-api-key " + f" --inf-base-url {args.gateway_url}" + f" --session-api-key {session_api_key}" + ) + print() + info("Then score the episode with the same key:") + print() + print( + f" python set_reward.py {args.gateway_url}" + f" --api-key {session_api_key} --reward 1.0" + ) + print() + info("To start the next episode reusing the same key:") + print() + print( + f" python start_session.py {args.gateway_url}" + f" --admin-key {args.admin_key} --api-key {session_api_key}" + ) + print() + + # Machine-readable output on stderr for scripting + print(f"SESSION_API_KEY={session_api_key}", file=sys.stderr) + print(f"SESSION_ID={session_id}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/examples/hermes/train.py b/examples/hermes/train.py new file mode 100644 index 0000000000..3c229838a5 --- /dev/null +++ b/examples/hermes/train.py @@ -0,0 +1,17 @@ +"""Online training: external users interact via proxy gateway URL.""" + +import sys + +from areal import PPOTrainer +from areal.api.cli_args import PPOConfig, load_expr_config + + +def main(args): + config, _ = load_expr_config(args, PPOConfig) + + with PPOTrainer(config) as trainer: + trainer.train() + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tests/v2/agent_service/test_hermes_agent.py b/tests/v2/agent_service/test_hermes_agent.py new file mode 100644 index 0000000000..5759732e4d --- /dev/null +++ b/tests/v2/agent_service/test_hermes_agent.py @@ -0,0 +1,234 @@ +"""Unit tests for the Hermes agent runtime. + +These tests exercise :class:`HermesAgent` without the real ``hermes-agent`` +package or a GPU: a stub ``AIAgent`` (whose ``run_conversation`` returns a +canned result) is injected per session, and the missing-binary path is driven +by leaving ``_build_agent`` unpatched while no session exists. +""" + +from __future__ import annotations + +import json + +import pytest +from examples.agent_service.hermes.hermes import ( + HermesAgent, + _SessionState, + _Upstream, +) + +from areal.v2.agent_service.types import AgentRequest + + +class _RecordingEmitter: + def __init__(self) -> None: + self.deltas: list[str] = [] + self.tool_calls: list[tuple[str, str]] = [] + self.tool_results: list[tuple[str, str]] = [] + + async def emit_delta(self, text: str) -> None: + self.deltas.append(text) + + async def emit_tool_call(self, name: str, args: str) -> None: + self.tool_calls.append((name, args)) + + async def emit_tool_result(self, name: str, result: str) -> None: + self.tool_results.append((name, result)) + + +class _StubAIAgent: + """Stand-in for a Hermes ``AIAgent`` that records calls and returns a canned result.""" + + def __init__(self, result: dict) -> None: + self._result = result + self.calls: list[dict] = [] + + def run_conversation( + self, user_message, conversation_history=None, task_id=None, **_ + ): + self.calls.append( + { + "user_message": user_message, + "conversation_history": conversation_history, + "task_id": task_id, + } + ) + return self._result + + +def _attach_stub(agent: HermesAgent, session_key: str, result: dict) -> _StubAIAgent: + stub = _StubAIAgent(result) + agent._sessions[session_key] = _SessionState( + session_key=session_key, + agent=stub, + upstream=_Upstream(base_url="http://u", api_key="k", model="m"), + ) + return stub + + +@pytest.mark.asyncio +async def test_run_returns_final_response_and_tool_calls(): + """Structured run emits the final text once and surfaces tool calls in metadata. + + Tool calls are reported in ``metadata['tool_calls']`` for observability but + are deliberately *not* emitted as ``tool_call`` events: Hermes executes + tools internally and the framework does not feed a paired ``tool_result``, + so emitting one would build an invalid replay history. + """ + result = { + "final_response": "Hello world", + "completed": True, + "api_calls": 3, + "messages": [ + { + "role": "assistant", + "tool_calls": [ + {"function": {"name": "search", "arguments": '{"q":"hi"}'}} + ], + }, + {"role": "tool", "content": "ok"}, + {"role": "assistant", "content": "Hello world"}, + ], + } + agent = HermesAgent() + stub = _attach_stub(agent, "s1", result) + emitter = _RecordingEmitter() + req = AgentRequest( + message="hi", + session_key="s1", + run_id="r1", + history=[{"role": "user", "content": "earlier"}], + ) + + resp = await agent.run(req, emitter=emitter) + await agent.close_all_sessions() + + assert resp.summary == "Hello world" + assert emitter.deltas == ["Hello world"] + assert emitter.tool_calls == [] + assert resp.metadata["tool_calls"] == [{"name": "search", "input": '{"q":"hi"}'}] + assert resp.metadata["completed"] is True + assert resp.metadata["api_calls"] == 3 + # The replayed history is forwarded verbatim as the single source of truth. + assert stub.calls[0]["conversation_history"] == [ + {"role": "user", "content": "earlier"} + ] + assert stub.calls[0]["user_message"] == "hi" + + +@pytest.mark.asyncio +async def test_chat_request_synthesizes_streaming_completion(): + """A streaming chat_request yields an OpenAI chat-completions SSE StreamResponse.""" + agent = HermesAgent() + _attach_stub(agent, "s2", {"final_response": "Hi there", "messages": []}) + chat_body = { + "model": "hermes-x", + "stream": True, + "messages": [ + {"role": "user", "content": "prev"}, + {"role": "assistant", "content": "ok"}, + {"role": "user", "content": "now"}, + ], + } + req = AgentRequest( + message="now", + session_key="s2", + run_id="r1", + history=[], + metadata={"chat_request": chat_body}, + ) + + resp = await agent.run(req, emitter=_RecordingEmitter()) + chunks = b"".join([chunk async for chunk in resp.body]).decode("utf-8") + await agent.close_all_sessions() + + assert resp.status_code == 200 + assert resp.headers["content-type"] == "text/event-stream" + assert "data: [DONE]" in chunks + # The assistant content delta carries Hermes' final text. + content_lines = [ + json.loads(line[len("data: ") :]) + for line in chunks.splitlines() + if line.startswith("data: ") and line != "data: [DONE]" + ] + contents = [ + c["choices"][0]["delta"].get("content") + for c in content_lines + if c["choices"][0]["delta"].get("content") + ] + assert contents == ["Hi there"] + assert content_lines[0]["object"] == "chat.completion.chunk" + + +@pytest.mark.asyncio +async def test_chat_request_non_streaming_json(): + """A non-streaming chat_request yields a single chat.completion JSON object.""" + agent = HermesAgent() + _attach_stub(agent, "s3", {"final_response": "Done", "messages": []}) + chat_body = { + "model": "hermes-x", + "messages": [{"role": "user", "content": "go"}], + } + req = AgentRequest( + message="go", + session_key="s3", + run_id="r1", + history=[], + metadata={"chat_request": chat_body}, + ) + + resp = await agent.run(req, emitter=_RecordingEmitter()) + body = b"".join([chunk async for chunk in resp.body]).decode("utf-8") + await agent.close_all_sessions() + + assert resp.headers["content-type"] == "application/json" + payload = json.loads(body) + assert payload["object"] == "chat.completion" + assert payload["choices"][0]["message"]["content"] == "Done" + assert payload["choices"][0]["finish_reason"] == "stop" + + +@pytest.mark.asyncio +async def test_extract_chat_splits_last_user_message(): + """_extract_chat returns the last user text and the preceding history.""" + message, history = HermesAgent._extract_chat( + { + "messages": [ + {"role": "system", "content": "sys"}, + {"role": "user", "content": "first"}, + {"role": "assistant", "content": "reply"}, + {"role": "user", "content": "second"}, + ] + } + ) + assert message == "second" + assert history == [ + {"role": "system", "content": "sys"}, + {"role": "user", "content": "first"}, + {"role": "assistant", "content": "reply"}, + ] + + +@pytest.mark.asyncio +async def test_close_session_drops_lock(): + """close_session removes the per-session lock to bound _locks growth.""" + agent = HermesAgent() + await agent._session_lock("sX") + assert "sX" in agent._locks + + await agent.close_session("sX") + assert "sX" not in agent._locks + + # idempotent: closing an already-closed session is safe + await agent.close_session("sX") + assert "sX" not in agent._locks + + +@pytest.mark.asyncio +async def test_run_without_session_or_env_raises(): + """run() with no live session and no env upstream raises clearly.""" + agent = HermesAgent() + agent._env_upstream = None + req = AgentRequest(message="hi", session_key="missing", run_id="r1", history=[]) + with pytest.raises(RuntimeError, match="No agent for session"): + await agent.run(req, emitter=_RecordingEmitter()) diff --git a/tests/v2/agent_service/test_integration.py b/tests/v2/agent_service/test_integration.py index d5516d27b4..9735cfc85b 100644 --- a/tests/v2/agent_service/test_integration.py +++ b/tests/v2/agent_service/test_integration.py @@ -6,6 +6,7 @@ from __future__ import annotations +import json from unittest.mock import patch import pytest @@ -14,14 +15,20 @@ from areal.v2.agent_service.data_proxy.app import create_data_proxy_app from areal.v2.agent_service.data_proxy.config import DataProxyConfig from areal.v2.agent_service.gateway.app import create_gateway_app -from areal.v2.agent_service.gateway.bridge import OpenResponsesBridge +from areal.v2.agent_service.gateway.bridge import ( + SESSION_KEY_HEADER, + ChatCompletionsBridge, + OpenResponsesBridge, +) from areal.v2.agent_service.gateway.config import GatewayConfig +from areal.v2.agent_service.protocol import PASSTHROUGH_HEADER from areal.v2.agent_service.router.app import create_router_app from areal.v2.agent_service.router.config import RouterConfig from areal.v2.agent_service.types import ( AgentRequest, AgentResponse, EventEmitter, + StreamResponse, ) from areal.v2.agent_service.worker.app import create_worker_app @@ -52,6 +59,50 @@ async def run( ) +class _StreamAgent: + """Returns a :class:`StreamResponse` from ``run`` when the turn carries a + ``chat_request`` (the raw-passthrough channel), echoing the request back as + an SSE body so tests can assert the worker relays it verbatim. Otherwise it + falls back to a structured :class:`AgentResponse`.""" + + async def run( + self, request: AgentRequest, *, emitter: EventEmitter + ) -> AgentResponse | StreamResponse: + chat_request = (request.metadata or {}).get("chat_request") + if chat_request is None: + return AgentResponse(summary="structured") + + async def _body(): + yield f"data: msg={request.message}\n\n".encode() + yield f"data: chat_request={json.dumps(chat_request)}\n\n".encode() + yield b"data: [DONE]\n\n" + + return StreamResponse( + status_code=200, + headers={"content-type": "text/event-stream"}, + body=_body(), + ) + + +class _JsonPassthroughAgent: + """Returns a non-streaming :class:`StreamResponse` whose body is + ``application/json``. Used to prove the DataProxy keys its relay-vs-parse + decision on the passthrough marker, not on ``Content-Type``: without the + marker this body would be mistaken for a structured turn and parsed.""" + + async def run( + self, request: AgentRequest, *, emitter: EventEmitter + ) -> StreamResponse: + async def _body(): + yield json.dumps({"verbatim": request.message, "events": []}).encode() + + return StreamResponse( + status_code=200, + headers={"content-type": "application/json"}, + body=_body(), + ) + + def _make_worker_app(agent_cls): with patch( "areal.v2.agent_service.worker.app.import_from_string", @@ -60,6 +111,61 @@ def _make_worker_app(agent_cls): return create_worker_app("mock.path") +def _worker_patches(worker_transport): + """Route the DataProxy's worker calls through the worker ASGI app. + + The DataProxy opens the worker ``/run`` stream via ``client.send`` (so it + can sniff the response ``Content-Type`` before reading) and closes worker + sessions via ``client.post``; patch both to forward to ``worker_transport``. + """ + original_post = httpx.AsyncClient.post + original_send = httpx.AsyncClient.send + + async def _forward(path, body): + # Use the unbound original ``send`` so the inner ASGI call does not + # re-enter the patched ``post``/``send`` (which would recurse forever). + async with httpx.AsyncClient( + transport=worker_transport, base_url="http://worker" + ) as wc: + req = wc.build_request("POST", path, json=body) + return await original_send(wc, req) + + async def patched_post(self, url, **kwargs): + if "http://worker" in str(url): + path = str(url).split("http://worker")[-1] + return await _forward(path, kwargs.get("json")) + return await original_post(self, url, **kwargs) + + async def patched_send(self, request, **kwargs): + if "http://worker" in str(request.url): + path = str(request.url).split("http://worker")[-1] + body = json.loads(request.content) if request.content else None + r = await _forward(path, body) + headers = { + "content-type": r.headers.get("content-type", "application/json") + } + # Preserve the passthrough marker so the DataProxy's relay-vs-parse + # decision is exercised faithfully end-to-end. + if PASSTHROUGH_HEADER in r.headers: + headers[PASSTHROUGH_HEADER] = r.headers[PASSTHROUGH_HEADER] + # Return the body as a stream (the real DataProxy opens the worker + # with ``stream=True``); a bytes payload would refuse ``aiter_raw``. + raw = r.content + + async def _stream(): + yield raw + + return httpx.Response( + r.status_code, + headers=headers, + content=_stream(), + request=request, + ) + return await original_send(self, request, **kwargs) + + return patched_post, patched_send + + class TestWorkerDataProxyIntegration: """Test DataProxy → Worker chain using ASGITransport for the Worker.""" @@ -90,24 +196,14 @@ async def test_data_proxy_manages_history(self): worker_app = _make_worker_app(_EchoAgent) worker_transport = httpx.ASGITransport(app=worker_app) - # Create DataProxy pointing to worker proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) - - # Patch DataProxy's httpx client to use worker's ASGITransport - original_post = httpx.AsyncClient.post - - async def patched_post(self, url, **kwargs): - if "worker" in url: - async with httpx.AsyncClient( - transport=worker_transport, base_url="http://worker" - ) as wc: - path = url.split("http://worker")[-1] - return await wc.post(path, **kwargs) - return await original_post(self, url, **kwargs) - + patched_post, patched_send = _worker_patches(worker_transport) proxy_transport = httpx.ASGITransport(app=proxy_app) - with patch.object(httpx.AsyncClient, "post", patched_post): + with ( + patch.object(httpx.AsyncClient, "post", patched_post), + patch.object(httpx.AsyncClient, "send", patched_send), + ): async with httpx.AsyncClient( transport=proxy_transport, base_url="http://proxy" ) as proxy_client: @@ -136,21 +232,13 @@ async def test_close_session_clears_history(self): worker_app = _make_worker_app(_EchoAgent) worker_transport = httpx.ASGITransport(app=worker_app) proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) - - original_post = httpx.AsyncClient.post - - async def patched_post(self, url, **kwargs): - if "worker" in url: - async with httpx.AsyncClient( - transport=worker_transport, base_url="http://worker" - ) as wc: - path = url.split("http://worker")[-1] - return await wc.post(path, **kwargs) - return await original_post(self, url, **kwargs) - + patched_post, patched_send = _worker_patches(worker_transport) proxy_transport = httpx.ASGITransport(app=proxy_app) - with patch.object(httpx.AsyncClient, "post", patched_post): + with ( + patch.object(httpx.AsyncClient, "post", patched_post), + patch.object(httpx.AsyncClient, "send", patched_send), + ): async with httpx.AsyncClient( transport=proxy_transport, base_url="http://proxy" ) as proxy_client: @@ -162,6 +250,35 @@ async def patched_post(self, url, **kwargs): h = await proxy_client.get("/session/s1/history") assert h.json()["history"] == [] + @pytest.mark.asyncio + async def test_json_passthrough_relayed_verbatim_not_parsed(self): + """A non-streaming passthrough body is itself ``application/json``; the + DataProxy must relay it verbatim on the marker, never mistaking it for a + structured turn (which would rebuild history and re-serialise).""" + worker_app = _make_worker_app(_JsonPassthroughAgent) + worker_transport = httpx.ASGITransport(app=worker_app) + proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) + patched_post, patched_send = _worker_patches(worker_transport) + proxy_transport = httpx.ASGITransport(app=proxy_app) + + with ( + patch.object(httpx.AsyncClient, "post", patched_post), + patch.object(httpx.AsyncClient, "send", patched_send), + ): + async with httpx.AsyncClient( + transport=proxy_transport, base_url="http://proxy" + ) as proxy_client: + r = await proxy_client.post( + "/session/s1/turn", + json={"message": "hi", "run_id": "r1"}, + ) + assert r.status_code == 200 + # Body relayed byte-for-byte (the agent's exact JSON). + assert r.json() == {"verbatim": "hi", "events": []} + # No history was kept on the passthrough path. + h = await proxy_client.get("/session/s1/history") + assert h.json()["history"] == [] + class TestRouterIntegration: @pytest.mark.asyncio @@ -196,21 +313,13 @@ async def test_tool_events_through_proxy(self): worker_app = _make_worker_app(_ToolAgent) worker_transport = httpx.ASGITransport(app=worker_app) proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) - - original_post = httpx.AsyncClient.post - - async def patched_post(self, url, **kwargs): - if "worker" in url: - async with httpx.AsyncClient( - transport=worker_transport, base_url="http://worker" - ) as wc: - path = url.split("http://worker")[-1] - return await wc.post(path, **kwargs) - return await original_post(self, url, **kwargs) - + patched_post, patched_send = _worker_patches(worker_transport) proxy_transport = httpx.ASGITransport(app=proxy_app) - with patch.object(httpx.AsyncClient, "post", patched_post): + with ( + patch.object(httpx.AsyncClient, "post", patched_post), + patch.object(httpx.AsyncClient, "send", patched_send), + ): async with httpx.AsyncClient( transport=proxy_transport, base_url="http://proxy" ) as proxy_client: @@ -233,6 +342,213 @@ async def patched_post(self, url, **kwargs): assert "tool_call_id" in tool_msgs[0] +class TestArealInference: + """Self-evolution (decoupled from the training side): the **caller** mints + its own per-session ``sk-sess-*`` and passes it on the turn body as + ``session_api_key``. The DataProxy never contacts ``/rl/start_session`` — + it caches the routing handle on the session and injects ``areal_inference`` + into the worker request metadata. A multi-turn session may send the fields + on the first turn and omit them afterwards. Without the flag the worker + metadata stays clean. + """ + + @staticmethod + def _patched_send(worker_bodies, *, stream=False): + original_send = httpx.AsyncClient.send + + async def patched_send(self, request, **kwargs): + if str(request.url).endswith("/run"): + worker_bodies.append(json.loads(request.content)) + if stream: + + async def _s(): + yield b"data: ok\n\n" + + return httpx.Response( + 200, + headers={ + "content-type": "text/event-stream", + PASSTHROUGH_HEADER: "1", + }, + content=_s(), + request=request, + ) + return httpx.Response( + 200, + json={"summary": "ok", "events": [], "metadata": {}}, + request=request, + ) + return await original_send(self, request, **kwargs) + + return patched_send + + @pytest.mark.asyncio + async def test_turn_caches_inference_routing_once(self): + proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) + worker_bodies: list[dict] = [] + proxy_transport = httpx.ASGITransport(app=proxy_app) + + with patch.object(httpx.AsyncClient, "send", self._patched_send(worker_bodies)): + async with httpx.AsyncClient( + transport=proxy_transport, base_url="http://proxy" + ) as client: + turn_body = { + "message": "hi", + "run_id": "r1", + "inf_base_url": "http://inf-gateway", + "session_api_key": "sk-sess-xyz", + "inf_model": "Qwen", + } + r1 = await client.post("/session/s1/turn", json=turn_body) + assert r1.status_code == 200 + # The second turn omits the inference fields; the proxy reuses + # the handle cached on the session. + r2 = await client.post( + "/session/s1/turn", json={"message": "again", "run_id": "r2"} + ) + assert r2.status_code == 200 + + # The worker received the inference routing handle on both turns. + assert len(worker_bodies) == 2 + for body in worker_bodies: + inf = body["metadata"]["areal_inference"] + assert inf["base_url"] == "http://inf-gateway" + assert inf["api_key"] == "sk-sess-xyz" + assert inf["model"] == "Qwen" + + # The Agent Service never surfaces a training-side session id back. + assert "areal_inference" not in r2.json().get("metadata", {}) + + @pytest.mark.asyncio + async def test_turn_without_inference_fields_skips_inference(self): + proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) + worker_bodies: list[dict] = [] + proxy_transport = httpx.ASGITransport(app=proxy_app) + + with patch.object(httpx.AsyncClient, "send", self._patched_send(worker_bodies)): + async with httpx.AsyncClient( + transport=proxy_transport, base_url="http://proxy" + ) as client: + r = await client.post( + "/session/s2/turn", json={"message": "hi", "run_id": "r1"} + ) + assert r.status_code == 200 + + assert len(worker_bodies) == 1 + assert "areal_inference" not in worker_bodies[0]["metadata"] + + @pytest.mark.asyncio + async def test_turn_requires_both_inference_fields(self): + proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) + worker_bodies: list[dict] = [] + proxy_transport = httpx.ASGITransport(app=proxy_app) + + with patch.object(httpx.AsyncClient, "send", self._patched_send(worker_bodies)): + async with httpx.AsyncClient( + transport=proxy_transport, base_url="http://proxy" + ) as client: + # inf_base_url present but session_api_key missing → 400 before + # any worker call (the pair is required once either is sent). + r = await client.post( + "/session/s3/turn", + json={"message": "hi", "inf_base_url": "http://inf-gateway"}, + ) + assert r.status_code == 400 + + assert worker_bodies == [] + + @pytest.mark.asyncio + async def test_stream_turn_injects_inference(self): + """The same routing injection applies to the raw-passthrough channel: + when the worker returns a stream, the proxy still injects + ``areal_inference`` into the worker request and relays the body.""" + proxy_app = create_data_proxy_app(DataProxyConfig(worker_addr="http://worker")) + worker_bodies: list[dict] = [] + proxy_transport = httpx.ASGITransport(app=proxy_app) + + with patch.object( + httpx.AsyncClient, "send", self._patched_send(worker_bodies, stream=True) + ): + async with httpx.AsyncClient( + transport=proxy_transport, base_url="http://proxy" + ) as client: + r = await client.post( + "/session/s1/turn", + json={ + "message": "hi", + "run_id": "r1", + "inf_base_url": "http://inf-gateway", + "session_api_key": "sk-sess-xyz", + "inf_model": "Qwen", + }, + ) + assert r.status_code == 200 + assert r.text == "data: ok\n\n" + + assert len(worker_bodies) == 1 + inf = worker_bodies[0]["metadata"]["areal_inference"] + assert inf["base_url"] == "http://inf-gateway" + assert inf["api_key"] == "sk-sess-xyz" + assert inf["model"] == "Qwen" + + +class TestWorkerRunStream: + """The single ``/run`` endpoint relays an agent's :class:`StreamResponse` + verbatim (tagged with the passthrough marker header so the DataProxy relays + rather than parses), and serialises an :class:`AgentResponse` to JSON + otherwise.""" + + @pytest.mark.asyncio + async def test_run_relays_stream_verbatim_with_metadata(self): + worker_app = _make_worker_app(_StreamAgent) + worker_transport = httpx.ASGITransport(app=worker_app) + + async with httpx.AsyncClient( + transport=worker_transport, base_url="http://worker" + ) as client: + async with client.stream( + "POST", + "/run", + json={ + "message": "hi", + "session_key": "s1", + "run_id": "r1", + "history": [], + "metadata": {"chat_request": {"model": "m", "stream": True}}, + }, + ) as resp: + assert resp.status_code == 200 + assert resp.headers["content-type"].startswith("text/event-stream") + # The worker tags raw-passthrough turns for the DataProxy. + assert resp.headers.get(PASSTHROUGH_HEADER) == "1" + body = b"" + async for chunk in resp.aiter_bytes(): + body += chunk + + text = body.decode() + assert "data: msg=hi\n\n" in text + assert 'data: chat_request={"model": "m", "stream": true}\n\n' in text + assert text.endswith("data: [DONE]\n\n") + + @pytest.mark.asyncio + async def test_run_returns_json_for_structured_response(self): + worker_app = _make_worker_app(_StreamAgent) + worker_transport = httpx.ASGITransport(app=worker_app) + + async with httpx.AsyncClient( + transport=worker_transport, base_url="http://worker" + ) as client: + resp = await client.post( + "/run", + json={"message": "hi", "session_key": "s1", "run_id": "r1"}, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"].startswith("application/json") + # No marker on a structured turn → the DataProxy parses it. + assert PASSTHROUGH_HEADER not in resp.headers + assert resp.json()["summary"] == "structured" + + class TestGatewayHealth: @pytest.mark.asyncio async def test_health(self): @@ -246,6 +562,71 @@ async def test_health(self): assert resp.json()["status"] == "ok" +class TestGatewayCloseSession: + """The gateway's ``POST /sessions/close`` (session_key in the body) resolves + the owning DataProxy via the Router and forwards the close, mirroring the + internal ``/session/{key}/close`` endpoint shape.""" + + @staticmethod + def _patched_post(calls): + original_post = httpx.AsyncClient.post + + async def patched_post(self, url, **kwargs): + url = str(url) + if url.endswith("/route"): + return httpx.Response( + 200, + json={"data_proxy_addr": "http://proxy1"}, + request=httpx.Request("POST", url), + ) + if url.startswith("http://proxy1") and url.endswith("/close"): + calls.append(url) + return httpx.Response( + 200, json={"status": "ok"}, request=httpx.Request("POST", url) + ) + return await original_post(self, url, **kwargs) + + return patched_post + + @pytest.mark.asyncio + async def test_close_forwards_to_data_proxy(self): + app = create_gateway_app(GatewayConfig(router_addr="http://fake-router")) + transport = httpx.ASGITransport(app=app) + calls: list[str] = [] + + with patch.object(httpx.AsyncClient, "post", self._patched_post(calls)): + async with httpx.AsyncClient( + transport=transport, base_url="http://gw" + ) as client: + resp = await client.post( + "/sessions/close", json={"session_key": "s1"}, headers=_AUTH + ) + assert resp.status_code == 200 + assert resp.json()["status"] == "ok" + + assert calls == ["http://proxy1/session/s1/close"] + + @pytest.mark.asyncio + async def test_close_requires_session_key(self): + app = create_gateway_app(GatewayConfig(router_addr="http://fake-router")) + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient( + transport=transport, base_url="http://gw" + ) as client: + resp = await client.post("/sessions/close", json={}, headers=_AUTH) + assert resp.status_code == 400 + + @pytest.mark.asyncio + async def test_close_requires_admin_key(self): + app = create_gateway_app(GatewayConfig(router_addr="http://fake-router")) + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient( + transport=transport, base_url="http://gw" + ) as client: + resp = await client.post("/sessions/close", json={"session_key": "s1"}) + assert resp.status_code in (401, 422) + + class TestBridgeExtractMessage: def test_text_message(self): items = [ @@ -286,3 +667,234 @@ def test_without_user_is_unique(self): def test_default_model(self): key = OpenResponsesBridge._derive_session_key("u1", "") assert key == "agent:default:u1" + + +class TestChatBridgeResolveSessionKey: + """``ChatCompletionsBridge`` requires an explicit session key: the + ``X-AReaL-Session-Key`` header wins, else it is derived from the OpenAI + ``user`` field; with neither, the resolver returns ``None`` so the request + is rejected with ``400`` instead of being minted a random key.""" + + @staticmethod + def _request(header: str | None): + """Minimal stand-in for a Starlette ``Request`` exposing ``headers.get``.""" + + class _Headers(dict): + def get(self, key, default=None): + return super().get(key, default) + + class _Req: + def __init__(self, key): + self.headers = _Headers({SESSION_KEY_HEADER: key} if key else {}) + + return _Req(header) + + def test_explicit_header_wins(self): + req = self._request("sess-1") + key = ChatCompletionsBridge._resolve_session_key( + req, {"user": "u1", "model": "m"} + ) + assert key == "sess-1" + + def test_derived_from_user(self): + req = self._request(None) + key = ChatCompletionsBridge._resolve_session_key( + req, {"user": "u1", "model": "m"} + ) + assert key == "chat:m:u1" + + def test_user_default_model(self): + req = self._request(None) + key = ChatCompletionsBridge._resolve_session_key(req, {"user": "u1"}) + assert key == "chat:default:u1" + + def test_neither_returns_none(self): + req = self._request(None) + key = ChatCompletionsBridge._resolve_session_key(req, {"model": "m"}) + assert key is None + + +class TestBridgeBuildOutputItems: + """The bridge translates a structured turn result into OpenAI Responses + output items: a non-empty summary → assistant ``message``; each + ``tool_call`` event → a ``function_call`` item.""" + + def test_summary_becomes_message_item(self): + items = OpenResponsesBridge._build_output_items({"summary": "hello"}) + assert items == [ + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "hello"}], + } + ] + + def test_tool_call_events_become_function_calls(self): + result = { + "summary": "", + "events": [ + {"type": "tool_call", "name": "lookup", "args": '{"id": "1"}'}, + {"type": "tool_result", "name": "lookup", "result": "ok"}, + {"type": "delta", "text": "ignored"}, + ], + } + items = OpenResponsesBridge._build_output_items(result) + assert items == [ + {"type": "function_call", "name": "lookup", "arguments": '{"id": "1"}'} + ] + + def test_empty_result_yields_no_items(self): + assert OpenResponsesBridge._build_output_items({}) == [] + + +def _parse_sse(raw: bytes) -> list[dict]: + events = [] + for block in raw.decode().split("\n\n"): + block = block.strip() + if block.startswith("data: "): + events.append(json.loads(block[len("data: ") :])) + return events + + +class TestBridgeResponsesSSE: + """``/v1/responses`` streaming (A): the bridge re-encodes the collected + structured output into an OpenAI Responses-format SSE event stream + (``response.created`` → ``output_text`` deltas → ``response.completed``).""" + + @pytest.mark.asyncio + async def test_sse_reencodes_collected_output(self): + output_items = OpenResponsesBridge._build_output_items({"summary": "hi there"}) + raw = b"" + async for chunk in OpenResponsesBridge._responses_sse( + "resp-1", "GLM-5", output_items, {"k": "v"} + ): + raw += chunk + events = _parse_sse(raw) + + types = [e["type"] for e in events] + assert types[0] == "response.created" + assert "response.output_text.delta" in types + assert "response.output_text.done" in types + assert types[-1] == "response.completed" + + delta = next(e for e in events if e["type"] == "response.output_text.delta") + assert delta["delta"] == "hi there" + + completed = events[-1]["response"] + assert completed["status"] == "completed" + assert completed["model"] == "GLM-5" + assert completed["output"] == output_items + assert completed["metadata"] == {"k": "v"} + + @pytest.mark.asyncio + async def test_sse_without_message_still_completes(self): + # Tool-call-only output (no assistant message) emits no text deltas but + # still brackets the stream with created/completed. + output_items = OpenResponsesBridge._build_output_items( + {"events": [{"type": "tool_call", "name": "f", "args": "{}"}]} + ) + raw = b"" + async for chunk in OpenResponsesBridge._responses_sse( + "resp-2", "GLM-5", output_items, {} + ): + raw += chunk + events = _parse_sse(raw) + types = [e["type"] for e in events] + assert types == ["response.created", "response.completed"] + assert events[-1]["response"]["output"] == output_items + + +class TestBridgeStreamingEndToEnd: + """``handle_request`` with ``stream=True`` runs a structured collect turn, + then returns a ``text/event-stream`` re-encoding it (A), echoing the + resolved session key on the response header.""" + + @staticmethod + def _patched_post(turn_result): + original_post = httpx.AsyncClient.post + + async def patched_post(self, url, **kwargs): + url = str(url) + if url.endswith("/route"): + return httpx.Response( + 200, + json={"data_proxy_addr": "http://proxy1"}, + request=httpx.Request("POST", url), + ) + if url.endswith("/turn"): + return httpx.Response( + 200, json=turn_result, request=httpx.Request("POST", url) + ) + return await original_post(self, url, **kwargs) + + return patched_post + + @pytest.mark.asyncio + async def test_stream_response_is_sse(self): + from fastapi import FastAPI + + from areal.v2.agent_service.gateway.bridge import ( + mount_bridge, + ) + + bridge = OpenResponsesBridge(router_addr="http://router") + app = FastAPI() + mount_bridge(app, bridge) + turn_result = {"summary": "streamed reply", "events": [], "metadata": {}} + + with patch.object(httpx.AsyncClient, "post", self._patched_post(turn_result)): + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient( + transport=transport, base_url="http://gw" + ) as client: + resp = await client.post( + "/v1/responses", + headers={**_AUTH, "X-AReaL-Session-Key": "sess-1"}, + json={ + "model": "GLM-5", + "stream": True, + "input": [{"type": "message", "content": "hi"}], + }, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"].startswith("text/event-stream") + assert resp.headers.get("X-AReaL-Session-Key") == "sess-1" + events = _parse_sse(resp.content) + types = [e["type"] for e in events] + assert types[0] == "response.created" + assert types[-1] == "response.completed" + delta = next(e for e in events if e["type"] == "response.output_text.delta") + assert delta["delta"] == "streamed reply" + + @pytest.mark.asyncio + async def test_non_stream_response_is_json(self): + from fastapi import FastAPI + + from areal.v2.agent_service.gateway.bridge import ( + mount_bridge, + ) + + bridge = OpenResponsesBridge(router_addr="http://router") + app = FastAPI() + mount_bridge(app, bridge) + turn_result = {"summary": "plain reply", "events": [], "metadata": {}} + + with patch.object(httpx.AsyncClient, "post", self._patched_post(turn_result)): + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient( + transport=transport, base_url="http://gw" + ) as client: + resp = await client.post( + "/v1/responses", + headers={**_AUTH, "X-AReaL-Session-Key": "sess-2"}, + json={ + "model": "GLM-5", + "input": [{"type": "message", "content": "hi"}], + }, + ) + assert resp.status_code == 200 + assert resp.headers["content-type"].startswith("application/json") + body = resp.json() + assert body["object"] == "response" + assert body["status"] == "completed" + assert body["output"][0]["content"][0]["text"] == "plain reply" diff --git a/tests/v2/agent_service/test_openclaw_agent.py b/tests/v2/agent_service/test_openclaw_agent.py new file mode 100644 index 0000000000..89a3800aeb --- /dev/null +++ b/tests/v2/agent_service/test_openclaw_agent.py @@ -0,0 +1,210 @@ +"""Unit tests for the OpenClaw agent runtime. + +These tests exercise :class:`OpenClawAgent` without a real ``openclaw`` +binary or GPU: the per-turn SSE parsing is driven by an +``httpx.MockTransport`` and subprocess-spawn failure is triggered by +pointing ``OPENCLAW_BIN`` at a non-existent executable. +""" + +from __future__ import annotations + +import glob +import os +import tempfile + +import pytest +from examples.agent_service.openclaw.openclaw import ( + OpenClawAgent, + _SessionState, + _Upstream, +) + +from areal.v2.agent_service.types import AgentRequest + +httpx = pytest.importorskip("httpx") + + +class _RecordingEmitter: + def __init__(self) -> None: + self.deltas: list[str] = [] + self.tool_calls: list[tuple[str, str]] = [] + self.tool_results: list[tuple[str, str]] = [] + + async def emit_delta(self, text: str) -> None: + self.deltas.append(text) + + async def emit_tool_call(self, name: str, args: str) -> None: + self.tool_calls.append((name, args)) + + async def emit_tool_result(self, name: str, result: str) -> None: + self.tool_results.append((name, result)) + + +class _ExitedProc: + """Stand-in for an already-terminated subprocess used by mock sessions. + + A non-None ``returncode`` makes ``_teardown_state`` skip the kill path. + """ + + returncode = 0 + + async def wait(self) -> int: + return 0 + + +def _attach_mock_session(agent: OpenClawAgent, session_key: str, sse: str) -> None: + """Inject a session whose subprocess HTTP client returns ``sse``.""" + + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response( + 200, text=sse, headers={"content-type": "text/event-stream"} + ) + + client = httpx.AsyncClient( + base_url="http://mock-openclaw", + transport=httpx.MockTransport(handler), + ) + agent._sessions[session_key] = _SessionState( + port=1, + gateway_token="t", + config_dir=tempfile.mkdtemp(prefix="openclaw-mock-"), + process=_ExitedProc(), # type: ignore[arg-type] + client=client, + log_file=None, # type: ignore[arg-type] + ) + + +@pytest.mark.asyncio +async def test_run_accumulates_streamed_tool_calls(): + """Streamed tool-call name/args are buffered by index into metadata. + + They are surfaced in ``metadata['tool_calls']`` for observability but are + deliberately *not* emitted as ``tool_call`` events: OpenClaw runs tools in + its own subprocess and never returns a matching tool result, so emitting a + ``tool_call`` without a paired ``tool_result`` would make the DataProxy + build an invalid chat-completions history that the upstream rejects on + replay. + """ + sse = ( + 'data: {"choices":[{"delta":{"content":"Hel"}}]}\n' + 'data: {"choices":[{"delta":{"content":"lo"}}]}\n' + # name arrives only in the first chunk; arguments stream across chunks + 'data: {"choices":[{"delta":{"tool_calls":' + '[{"index":0,"function":{"name":"search"}}]}}]}\n' + 'data: {"choices":[{"delta":{"tool_calls":' + '[{"index":0,"function":{"arguments":"{\\"q\\":"}}]}}]}\n' + # no space after "data:" — valid per the SSE spec + 'data:{"choices":[{"delta":{"tool_calls":' + '[{"index":0,"function":{"arguments":"\\"hi\\"}"}}]}}]}\n' + # a second tool call at a different index + 'data: {"choices":[{"delta":{"tool_calls":' + '[{"index":1,"function":{"name":"calc","arguments":"1+1"}}]}}]}\n' + "data: [DONE]\n" + ) + agent = OpenClawAgent() + _attach_mock_session(agent, "s1", sse) + emitter = _RecordingEmitter() + req = AgentRequest(message="hi", session_key="s1", run_id="r1", history=[]) + + resp = await agent.run(req, emitter=emitter) + await agent.close_all_sessions() + + assert resp.summary == "Hello" + assert emitter.deltas == ["Hel", "lo"] + # tool calls are NOT streamed as events (no paired tool_result available) + assert emitter.tool_calls == [] + # but they are fully accumulated by index into the response metadata + assert resp.metadata["tool_calls"] == [ + {"name": "search", "input": '{"q":"hi"}'}, + {"name": "calc", "input": "1+1"}, + ] + + +@pytest.mark.asyncio +async def test_run_keeps_full_summary_for_replay(): + """summary is returned in full, not truncated, so replay stays faithful. + + The DataProxy stores ``summary`` as the assistant turn and replays it on + later turns; truncating it would corrupt both the replayed prompt and the + user-visible output. + """ + long_text = "x" * 500 + sse = ( + f'data: {{"choices":[{{"delta":{{"content":"{long_text}"}}}}]}}\ndata: [DONE]\n' + ) + agent = OpenClawAgent() + _attach_mock_session(agent, "s_long", sse) + emitter = _RecordingEmitter() + req = AgentRequest(message="hi", session_key="s_long", run_id="r1", history=[]) + + resp = await agent.run(req, emitter=emitter) + await agent.close_all_sessions() + + assert resp.summary == long_text + assert len(resp.summary) == 500 + + +@pytest.mark.asyncio +async def test_run_skips_malformed_and_indexless_chunks(): + """Malformed JSON and tool-call chunks without an index are ignored.""" + sse = ( + "data: not-json\n" + 'data: {"choices":[]}\n' + 'data: {"choices":[{"delta":{"tool_calls":' + '[{"function":{"name":"orphan"}}]}}]}\n' # missing index + 'data: {"choices":[{"delta":{"content":"ok"}}]}\n' + "data: [DONE]\n" + ) + agent = OpenClawAgent() + _attach_mock_session(agent, "s2", sse) + emitter = _RecordingEmitter() + req = AgentRequest(message="hi", session_key="s2", run_id="r1", history=[]) + + resp = await agent.run(req, emitter=emitter) + await agent.close_all_sessions() + + assert resp.summary == "ok" + assert emitter.tool_calls == [] + assert resp.metadata["tool_calls"] == [] + + +@pytest.mark.asyncio +async def test_spawn_cleans_up_on_failure(monkeypatch): + """A spawn failure leaks neither the temp config dir nor a file handle.""" + monkeypatch.setenv("OPENCLAW_BIN", "areal-nonexistent-openclaw-binary") + agent = OpenClawAgent() + upstream = _Upstream(base_url="http://upstream", api_key="k", model="m") + + tmp_glob = os.path.join(tempfile.gettempdir(), "openclaw-*") + before = set(glob.glob(tmp_glob)) + + with pytest.raises(FileNotFoundError): + await agent._spawn("s3", upstream) + + after = set(glob.glob(tmp_glob)) + assert after - before == set(), "spawn failure leaked a temp config dir" + + +@pytest.mark.asyncio +async def test_close_session_drops_lock(): + """close_session removes the per-session lock to bound _locks growth.""" + agent = OpenClawAgent() + await agent._session_lock("sX") + assert "sX" in agent._locks + + await agent.close_session("sX") + assert "sX" not in agent._locks + + # idempotent: closing an already-closed session is safe + await agent.close_session("sX") + assert "sX" not in agent._locks + + +@pytest.mark.asyncio +async def test_run_without_session_or_env_raises(): + """run() with no open session and no env upstream raises clearly.""" + agent = OpenClawAgent() + agent._env_upstream = None + req = AgentRequest(message="hi", session_key="missing", run_id="r1", history=[]) + with pytest.raises(RuntimeError, match="No subprocess for session"): + await agent.run(req, emitter=_RecordingEmitter()) From 4ac8c36058612b51d0f46523aa07fe805e1dbcfa Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:56:57 +0800 Subject: [PATCH 20/36] feat(ppo): add reuse_train_logp proximal logp method (#1453) Add a reuse_train_logp option to prox_logp_method for decoupled PPO. It reuses the training forward-pass logprobs (detached) as the proximal logp, skipping the extra proximal forward pass and its memory/compute cost. This is only valid with ppo_n_minibatches=1: with a single minibatch the training forward still reflects the policy that generated the rollout, so its logprobs equal the proximal policy. With multiple minibatches the weights change between steps, so PPOActorConfig.__post_init__ rejects that combination. Add tests for the enum/constant wiring, skips_forward_pass, and the ppo_n_minibatches validation. --- areal/api/cli_args.py | 16 +++++++++- areal/trainer/ppo/actor.py | 3 ++ areal/utils/constants.py | 7 +++- tests/test_reuse_train_logp.py | 58 ++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/test_reuse_train_logp.py diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index b151894e88..b27ae62727 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -1613,7 +1613,9 @@ class PPOActorConfig(TrainEngineConfig): "Only effective when use_decoupled_loss=True. Options: " "'recompute' (default): Standard decoupled PPO, recompute proximal policy via forward pass. " "'loglinear': Use log-linear interpolation to approximate proximal policy (skip forward pass). " - "'metrics': Like 'recompute', but also compute approximation metrics for evaluation.", + "'metrics': Like 'recompute', but also compute approximation metrics for evaluation. " + "'reuse_train_logp': Reuse training forward-pass logprobs as the proximal " + "logp (skip the extra forward; requires ppo_n_minibatches=1).", "choices": PROX_LOGP_METHODS_ALL, }, ) @@ -1648,6 +1650,18 @@ def should_compute_prox_logp(self) -> bool: def __post_init__(self): """Validate PPO actor configuration.""" + from areal.utils.constants import ProxLogpMethod + + if ( + ProxLogpMethod(self.prox_logp_method) == ProxLogpMethod.REUSE_TRAIN_LOGP + and self.ppo_n_minibatches > 1 + ): + raise ValueError( + "prox_logp_method='reuse_train_logp' requires ppo_n_minibatches=1. " + f"Got ppo_n_minibatches={self.ppo_n_minibatches}. " + "With multiple minibatches, weights change between steps, " + "so training forward logprobs differ from the original policy." + ) # Warn if rejection_sampling is configured but use_decoupled_loss is False if not self.use_decoupled_loss and self.rejection_sampling is not None: logger.warning( diff --git a/areal/trainer/ppo/actor.py b/areal/trainer/ppo/actor.py index a0922bafe9..f35ac219a6 100644 --- a/areal/trainer/ppo/actor.py +++ b/areal/trainer/ppo/actor.py @@ -436,6 +436,9 @@ def grpo_loss_fn( entropy = entropy.detach() + if ProxLogpMethod(prox_logp_method) == ProxLogpMethod.REUSE_TRAIN_LOGP: + prox_logp_gt = logprobs.detach() + # Resolve proximal log-probabilities based on method prox_logp = _resolve_proximal_logp( prox_logp_gt=prox_logp_gt, diff --git a/areal/utils/constants.py b/areal/utils/constants.py index d10a794b22..dc2e5f4838 100644 --- a/areal/utils/constants.py +++ b/areal/utils/constants.py @@ -40,15 +40,19 @@ class ProxLogpMethod(str, Enum): RECOMPUTE: Standard decoupled PPO - recompute via forward pass. LOGLINEAR: Use log-linear approximation (skip forward pass). METRICS: Recompute + compute approximation metrics for evaluation. + REUSE_TRAIN_LOGP: Reuse training forward-pass logprobs as the proximal + logp. Requires ppo_n_minibatches == 1 so the training forward sees + the unchanged policy. """ RECOMPUTE = "recompute" LOGLINEAR = "loglinear" METRICS = "metrics" + REUSE_TRAIN_LOGP = "reuse_train_logp" def skips_forward_pass(self) -> bool: """Return True if this method skips the forward pass (optimization enabled).""" - return self == ProxLogpMethod.LOGLINEAR + return self in (ProxLogpMethod.LOGLINEAR, ProxLogpMethod.REUSE_TRAIN_LOGP) class ProxApproxMethod(str, Enum): @@ -74,6 +78,7 @@ class ProxApproxMethod(str, Enum): PROX_LOGP_METHOD_RECOMPUTE = ProxLogpMethod.RECOMPUTE.value PROX_LOGP_METHOD_LOGLINEAR = ProxLogpMethod.LOGLINEAR.value PROX_LOGP_METHOD_METRICS = ProxLogpMethod.METRICS.value +PROX_LOGP_METHOD_REUSE_TRAIN_LOGP = ProxLogpMethod.REUSE_TRAIN_LOGP.value # List of all valid prox_logp_method values for configuration PROX_LOGP_METHODS_ALL = [m.value for m in ProxLogpMethod] diff --git a/tests/test_reuse_train_logp.py b/tests/test_reuse_train_logp.py new file mode 100644 index 0000000000..333b270efb --- /dev/null +++ b/tests/test_reuse_train_logp.py @@ -0,0 +1,58 @@ +"""Tests for the ``reuse_train_logp`` proximal-logp method. + +``reuse_train_logp`` reuses the training forward-pass logprobs as the proximal +logp, skipping the extra decoupled-PPO forward. It requires +``ppo_n_minibatches == 1`` so the training forward still reflects the policy +that produced the rollout (with multiple minibatches the weights change between +steps, so the reused logprobs would no longer be the proximal policy). +""" + +import pytest + +from areal.api.cli_args import PPOActorConfig +from areal.utils.constants import ( + PROX_LOGP_METHOD_REUSE_TRAIN_LOGP, + PROX_LOGP_METHODS_ALL, + ProxLogpMethod, +) + + +def test_enum_includes_reuse_train_logp(): + assert ProxLogpMethod.REUSE_TRAIN_LOGP.value == "reuse_train_logp" + assert ProxLogpMethod("reuse_train_logp") is ProxLogpMethod.REUSE_TRAIN_LOGP + assert "reuse_train_logp" in PROX_LOGP_METHODS_ALL + assert PROX_LOGP_METHOD_REUSE_TRAIN_LOGP == "reuse_train_logp" + + +def test_reuse_train_logp_skips_forward_pass(): + # It must skip the extra proximal forward, like loglinear does. + assert ProxLogpMethod.REUSE_TRAIN_LOGP.skips_forward_pass() + + +def test_reuse_train_logp_allows_single_minibatch(): + config = PPOActorConfig( + backend="fsdp:d1", + prox_logp_method="reuse_train_logp", + ppo_n_minibatches=1, + ) + assert config.prox_logp_method == "reuse_train_logp" + assert config.ppo_n_minibatches == 1 + + +def test_reuse_train_logp_rejects_multiple_minibatches(): + with pytest.raises(ValueError, match="ppo_n_minibatches=1"): + PPOActorConfig( + backend="fsdp:d1", + prox_logp_method="reuse_train_logp", + ppo_n_minibatches=2, + ) + + +def test_other_methods_unaffected_by_minibatch_count(): + # The constraint only applies to reuse_train_logp. + config = PPOActorConfig( + backend="fsdp:d1", + prox_logp_method="recompute", + ppo_n_minibatches=4, + ) + assert config.ppo_n_minibatches == 4 From 9c6a70392c1f08f8314c59cb38b6a4b35a063c18 Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:16:27 +0800 Subject: [PATCH 21/36] fix(ppo): coerce ppo_n_minibatches to 1 for reuse_train_logp (#1457) reuse_train_logp requires ppo_n_minibatches=1 so the training forward pass still reflects the policy that produced the rollout. Previously an invalid combination raised ValueError; instead warn and force ppo_n_minibatches=1, making clear this reduces the PPO update to a single optimizer step. Remove obsolete tests that asserted behavior no longer relevant to this follow-up. --- areal/api/cli_args.py | 13 ++++++++----- docs/en/cli_reference.md | 2 +- docs/zh/cli_reference.md | 2 +- tests/test_reuse_train_logp.py | 21 --------------------- 4 files changed, 10 insertions(+), 28 deletions(-) diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index b27ae62727..151bd2e0d6 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -1656,12 +1656,15 @@ def __post_init__(self): ProxLogpMethod(self.prox_logp_method) == ProxLogpMethod.REUSE_TRAIN_LOGP and self.ppo_n_minibatches > 1 ): - raise ValueError( - "prox_logp_method='reuse_train_logp' requires ppo_n_minibatches=1. " - f"Got ppo_n_minibatches={self.ppo_n_minibatches}. " - "With multiple minibatches, weights change between steps, " - "so training forward logprobs differ from the original policy." + logger.warning( + "prox_logp_method='reuse_train_logp' requires ppo_n_minibatches=1, " + f"but got ppo_n_minibatches={self.ppo_n_minibatches}. " + "With multiple minibatches, weights change between steps, so the " + "training forward logprobs would differ from the original policy. " + "Forcing ppo_n_minibatches=1; note this changes training dynamics " + "to a single optimizer step per PPO update." ) + self.ppo_n_minibatches = 1 # Warn if rejection_sampling is configured but use_decoupled_loss is False if not self.use_decoupled_loss and self.rejection_sampling is not None: logger.warning( diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index 78d9515cde..5efb5ae945 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -406,7 +406,7 @@ Configuration for PPO actor model, a subclass of a TrainEngine. | `use_decoupled_loss` | boolean | `False` | Use the decoupled loss. Implicitly enables recompute_logprob. | | `rejection_sampling` | [`RejectionSamplingConfig`](section-rejection-sampling) \| None | `None` | Rejection sampling configuration for filtering stale samples. None disables filtering (equivalent to old behave_imp_weight_mode='disabled'). Only effective when use_decoupled_loss=True. | | `importance_sampling_level` | string | `"token"` | Level at which to compute importance sampling ratios. 'token': per-token ratios (standard PPO). 'sequence': sequence-level geometric mean of per-token ratios (GSPO). **Choices:** `token`, `sequence` | -| `prox_logp_method` | string | `"recompute"` | Method for computing proximal policy log-probabilities in decoupled PPO. Only effective when use_decoupled_loss=True. Options: 'recompute' (default): Standard decoupled PPO, recompute proximal policy via forward pass. 'loglinear': Use log-linear interpolation to approximate proximal policy (skip forward pass). 'metrics': Like 'recompute', but also compute approximation metrics for evaluation. **Choices:** `recompute`, `loglinear`, `metrics` | +| `prox_logp_method` | string | `"recompute"` | Method for computing proximal policy log-probabilities in decoupled PPO. Only effective when use_decoupled_loss=True. Options: 'recompute' (default): Standard decoupled PPO, recompute proximal policy via forward pass. 'loglinear': Use log-linear interpolation to approximate proximal policy (skip forward pass). 'metrics': Like 'recompute', but also compute approximation metrics for evaluation. 'reuse_train_logp': Reuse training forward-pass logprobs as the proximal logp (skip the extra forward; requires ppo_n_minibatches=1). **Choices:** `recompute`, `loglinear`, `metrics`, `reuse_train_logp` | | `log_agent_stats` | boolean | `False` | Log statistics for agent trajectories | | `log_agent_stats_keys` | list of string | **Required** | Keys for logging agent trajectory statistics | | `max_new_tokens` | integer | `1024` | Maximum number of new tokens to generate | diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index 455339316f..c65794eecc 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -404,7 +404,7 @@ Configuration for PPO actor model, a subclass of a TrainEngine. | `use_decoupled_loss` | boolean | `False` | Use the decoupled loss. Implicitly enables recompute_logprob. | | `rejection_sampling` | [`RejectionSamplingConfig`](section-rejection-sampling) \| None | `None` | Rejection sampling configuration for filtering stale samples. None disables filtering (equivalent to old behave_imp_weight_mode='disabled'). Only effective when use_decoupled_loss=True. | | `importance_sampling_level` | string | `"token"` | Level at which to compute importance sampling ratios. 'token': per-token ratios (standard PPO). 'sequence': sequence-level geometric mean of per-token ratios (GSPO). **Choices:** `token`, `sequence` | -| `prox_logp_method` | string | `"recompute"` | Method for computing proximal policy log-probabilities in decoupled PPO. Only effective when use_decoupled_loss=True. Options: 'recompute' (default): Standard decoupled PPO, recompute proximal policy via forward pass. 'loglinear': Use log-linear interpolation to approximate proximal policy (skip forward pass). 'metrics': Like 'recompute', but also compute approximation metrics for evaluation. **Choices:** `recompute`, `loglinear`, `metrics` | +| `prox_logp_method` | string | `"recompute"` | Method for computing proximal policy log-probabilities in decoupled PPO. Only effective when use_decoupled_loss=True. Options: 'recompute' (default): Standard decoupled PPO, recompute proximal policy via forward pass. 'loglinear': Use log-linear interpolation to approximate proximal policy (skip forward pass). 'metrics': Like 'recompute', but also compute approximation metrics for evaluation. 'reuse_train_logp': Reuse training forward-pass logprobs as the proximal logp (skip the extra forward; requires ppo_n_minibatches=1). **Choices:** `recompute`, `loglinear`, `metrics`, `reuse_train_logp` | | `log_agent_stats` | boolean | `False` | Log statistics for agent trajectories | | `log_agent_stats_keys` | list of string | **Required** | Keys for logging agent trajectory statistics | | `max_new_tokens` | integer | `1024` | Maximum number of new tokens to generate | diff --git a/tests/test_reuse_train_logp.py b/tests/test_reuse_train_logp.py index 333b270efb..4b82747711 100644 --- a/tests/test_reuse_train_logp.py +++ b/tests/test_reuse_train_logp.py @@ -7,8 +7,6 @@ steps, so the reused logprobs would no longer be the proximal policy). """ -import pytest - from areal.api.cli_args import PPOActorConfig from areal.utils.constants import ( PROX_LOGP_METHOD_REUSE_TRAIN_LOGP, @@ -37,22 +35,3 @@ def test_reuse_train_logp_allows_single_minibatch(): ) assert config.prox_logp_method == "reuse_train_logp" assert config.ppo_n_minibatches == 1 - - -def test_reuse_train_logp_rejects_multiple_minibatches(): - with pytest.raises(ValueError, match="ppo_n_minibatches=1"): - PPOActorConfig( - backend="fsdp:d1", - prox_logp_method="reuse_train_logp", - ppo_n_minibatches=2, - ) - - -def test_other_methods_unaffected_by_minibatch_count(): - # The constraint only applies to reuse_train_logp. - config = PPOActorConfig( - backend="fsdp:d1", - prox_logp_method="recompute", - ppo_n_minibatches=4, - ) - assert config.ppo_n_minibatches == 4 From 5ea8aae60e8d89d2238c957c44fca5a3963a0407 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:48:51 +0800 Subject: [PATCH 22/36] Chore/add areal 2 report paper (#1461) * chore: add AReaL 2.0 report paper * chore: rename paper file to AReaL2.0_report.pdf --- docs/paper/AReaL2.0_report.pdf | Bin 0 -> 1498405 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/paper/AReaL2.0_report.pdf diff --git a/docs/paper/AReaL2.0_report.pdf b/docs/paper/AReaL2.0_report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d0b1f0247a3a3afd799f77e24d758e01cdb7f372 GIT binary patch literal 1498405 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 z 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
9v)5OKI-b-1uXv@miQeH_^9`Ei@ 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�u%U84Rq}G4MBP--=ivJA1T$V+Ur>o3LEk0D3Kon#-afg3hkSMDyRc?02Zes7zZh_ z=xfhNe-Ho>r82Bd$3&aBM-w>4QJS>w#N*?$wzTDyXTj`f%;sX>{?WZx@@D42LnZe& zGMkIr`2?0c)@ZV;k+jh?#awQjirWgIH*6`{?iKOd$LKXdJEcOA7C@&_p zg1;xUpeEzNSU>M|(8zb4l2CHY_{k4nKTr4=I&ify($T$(+)ov8@JSl|h`Tuz8> zst7jJy!BULW)N^)HB(c%P_UAEJTX(Ku&ZL@U>r?8i6b}&Idp2IFk#(*`POPAFfopT zhdWa2v>RFomHgI6s-S^R*3)lof8MTD&!Q7pnd3q`OS`^EyI&mQE2M#4AWVkCvG1~7 z)eLeo4M_dTU+=`NHQ2cW$)IfupZ zYXv;D*(ccv0(6rcsyUkE<4_)K6g~kg?n7hwF9)TvFSiy|QJygUdWrSw22SKsQw@6q z8RKdpw<&NwR$KUy3=5n(bh?GqTAK7Ual(c;pS-j ztv$(c%T~XMdf?K;O6ZhMI7$M7_(sIn5{$6#j0>TRCB419K*(>Sm1hYJFrf0Hd5Vow z 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

7m` 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 z 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మ{C)E*z;z>1%wVeaBMiX@9a1^qPvN9~`JH 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

^xD&B>eybVZPb;zLVeBUTgoZ6sl7WGPmHD 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

JZo6VO-*%Xq&v(%b(Qd zHnI;^pQd|6Hd$flpYFy|6S)Ce{FzadoiT;qPsZsjh1)++_wqs)cpUzomgGc#W4(yd zofjDT1zDWoG^Zoa0zGA{nar@XCz*jEJbFmDu}B52m4D0qWT=0TP~;WovJ-b_Z~+*G zB5{&;1?J@P9_UXS@xc{JR^Z%+sedhp{|RkTx(3gsC{0S+Zx9lX3@%cls8h#4V8Ey3 zuu$JtK7rxqbS{8V7PCYtdMVNX8LZ9NXZq8)0QGqo#CtXpbdM0Hgbwn-SFRjlD#cH$+29f$DB5EuwV-vYn0t z6AUb{txfWNVjDJOd_DT$TAS45XpZ_^v4MkzkHuUqhoC~N^}DS z`QoH)JvhKpCtgSFbF;>H^}7FhtgpRSJI&{(=9 zl4zJg_#pI`3gOjFU5H(bJ(m}UfrM@4)0#Kp3L&Yo)vxmiwLZIMe}~*fd&&&_KW@jR^o2S-Kv=!0nhQcM-XrpkcEtBpg z7~pZRP7>A&%#RN--^3C6exZ!r&bOs))8o5dnHC;6<@5Z=_@oudk>r%lPVwNN#f z*bDI}3qDG~bQ+*y1I;2%rC0QMwOT8GePv?AzDm?Y&eE`yv?35#`{*#=zrwf=f0|KO zj&kcud?*jM;9~8gSWB{>T5{se6JtXu*t7t2r<)XJMYZ~2xC(^UhrsPSpLYXg5+Wkn z6(}QTFVX}5h$zp92VuMiL-%%Mqinj0b=UFnaR%QWrElg%xbuCF3uhY8D0*t>X@rcZ z`ukz;2pkj-jzlnI8}G-qB5vwFa!3q^@3W2mz_ITyvn0sG`9wqu4F-dt2XfMbUfT<; z1CFW~48r|j(AF%Z=c|@L+L2)3nDq7Ul`ibW)ze8tT*w^=q)spXO^;k^x(bTG((z{2 zr2#K+7>(==9|}fXZN`XleKqv|(J4ytZAy#HVV3eq+{GE(mF3@HjO3x?{UVT5yLxfS zH_wYc`v@Y%K}{A_Kn!1UG$&e9?_75dGz=d4X9*#=p5m(V9W_wrt&}V5#TOyzgCt-N z<@zrTP4|yvs{J>f!ykXJEbm@yNFVI#cyc(5ZjA;>j#-YiztsOTA~|6dkjdbqbXimV zoi*@e%vlW;NdtGFFe=Vb5~=sCLECxIXG;;vRwEguU0;9!B8*GgaE#9wAic^!O7untj2L->ihflkVr;Ww#mlBUGe z%IZ*iVkRfOG93?F=|gPu>T)2Rymum`#7(zdZV;GnUnv=|w894Gq4znMY`?j8 z5<4g1;U(9dMcY^9jNFuVbWE+KfV1S=FC6&jg^WIcnPSr+hn)A(I#Vst4}SH1g8AQ^ z_Ba_5CAeMt>OC5PL~AexrFSP3WQOrJ%q%P?YodOh%X?})DKTlb1!=>qu$!^;y%VJ& zi)*+9$AaFbcOON6Y<27cS}Z0Ls-Vv7ARcQEhSyALZFJU%C#w8`6%PX?%`y3u>|7oB zDVcJj^cvis0yWzUw#ba$$k)zjqGnoIEhYJdJ)6V}pf~)M?IDR?LR#z&d)x0zHbvo9 zRS?U{w$BIx*tEOb7YAD+;8@7uz16?p19HpylOac(@?%j-x9hulLU^I$MjMUSSmnT)Hsx+@Bu!QhFS`vORVAPwg-Qxh+ zmW3s?XqfEeJbYmptS;Z8NaCiDZ76rkAsS9$FzKFR^zA03Q!*c%+e8X^=k2SjGi%{OD;zfVU*V#oe2 zxFk2lAHA%JwqvVVV5IA@OK;G_msH9p{J$5iplGM`!VT1BKe^8}=O{A;bJ7=ikPlqEVnA?nNiHmqg#0D`3{DkH%nKe2L24`yDjmgM)Q8L)4OYK2sHEN_mWCM z2T@g_gcD=5zWTMo{1MUO*zzGtFwE4ae0|(*%kh-5a_i&)qPO*}uV(h`Ea&jG<7DNh zg{|wm-0Px4@>;`o=bpK?<(rK3vbqjnDQd>Ilc&=mwlZ*^S!8H5kBCq6Gab|1?Oe0t zXRGtsJpzLt6@^|K&-iXj-k$o`lRTw&DfeY|hL7339pI|fQ#YrU=2>{5r%zS!t%Y9u z%!Us^2HX<@yQ@#zm18?Z@SI%g1DsqgA2s+NJL_f@M+Nn>@k2SAxVh%3-E8ht4cjKO zd42iaB830{_an0Y3=6&FK>M$H&{_W<^q>QmT>dwE&|mgF{;#VsIe~uk|Kp&VRCA{d zDYOu8Sp{_7FoGy4(LdFuuaMt-#{Uf5jp+zwM{jJ}Z;a`iRo9m$c0Fafyg52f-E-4% zbGthy{<6-z*8;6d4*PSmY=pQE?`<0_j)L~?k3Z^7okjzkB@6D}XO@)!nN*4Rp~&bV z(|j#?l~_bJd9#SwuXWmt*z88EuXKuo5T(+gzzH@4uIcLhShA)pX%$?d5ysj{4!lQf z>g{TnUpaIS6@DF^-g+5u%ND1h`-w(TMT`!)d-*HTU|x74Cg!VGsL{>r8worq1#LE4 z@W800_Xnvo2rCya6NmqbFEI^3h8Kv0er4PEsxJMyKmr5d@=actGgtB`MN157ei~<= zVqO!kwl7cC7*=?{7uEZB?s{n($vI4lBxxhqvI4%@iv6MnzbYw95ID6Dne1qi#**n~ zkqk%rdOyw?&nnR(LT-9aMY*s|s_X!X zkFlneY8ZVI?Rrv9Fu`Y5sxm*nb@3J7Jcw17V2~mhNC;cSaCB?k^#hs~%Dv!&IvN{HCpf zt_IzgEMDAiqs8Q&d9Bkz^X}VE>5^kFOr&>wT?G*cHr-sKzhLP&X|YZ*dVveBwh({8 zELk+@np7kNQw3)#>hDm?6~|}#cOM3=fQ z7LV&w2&+!7_eDktf2Cl&V!qUni3*SvP#wmDa62MI^V4dN69%5K~;Q6`pT$(R^sk z>>ss$aMbNybJwtZ)OfJvjL=Wl1r|Lo>U)rx+^mf@&yjjO^=j}I5Fos29d%^0=5iUhK6;%81;H$Z{r)2!%Fs0ZtDSfVHrLn50bNHd`kGHw6DAFuDV46Fz$I{AKAwUfD|hk~+Jhv-Suvx%_T5S=ep^4f zdwF|4y9;G!U8nbt{}BEOv85%!O2;^epg>OChWS}^J}@xA?;kMbxp;42Sj-Q?e z_si)^4eD=VL*|LwYIVl^!4f3N%S$&7-jXbmWLiY%spCCGub@C=tegZODd?m>pKd(=-NPk`1LUy^UB6AV9j#uPIJklmw z%1Kokss5yFra8X6{x{hB^i0#C(pBB&Ve-meC#?x|86*WB3q(ImKaGAIhPx(u?GnI9 zmW<^2G|PBu%z>|L%ic-q2~e4;LvT%&wXdwT%5*mF8^#N1D=9 zW=HTR9GEP>pIUU9e}-8;4fRPoskEhj>{g?rA|a6d;&|)DMF>wW{zjEfMzK3i?E=#n zB}BhOi~;?kcAZSE)d^-8k>qm=&49S*l$OF?<#S|~@XxB8BFFoOZX=fU^DdalT<8Ugx@+pla?95$g>>q_x>AXiB z@|p;4hTkJgcZGVj8BAn%NGqKY``+dDJm2HQAYttq&a>y33y=F{!3w_ecysY~7jJ_7 z@AG)q8q(YW(SHSIZ2vF7jP<{}q3ZttGmifY%u*~hH)PO4Toy=!V2qLPJAxPyTZ<3F zi;J5dpAFg6p`;$|PUEDAPGgV49cU>xBg^Vgb-Vq;L+4B~`|vJWP!U4L|NaL|RM+k8 zt-~kSu16P!zga~-M6V;t4eyaV3H)VZu{~h#an*Y2waPWp0XL$pjZVIF_?VsSz)z@F z_M1@{+wL`0ucJQ+@!LCZ15_pH#z+;1Q;pW%(HoE#&xa@pQ4y44S9Fa)C9;`qdsudY zDC)Q&cLtDJ6D=Lq{soMxlv!$W4~j8-bj)EK05{s z-Auo|7egZb?0ytji#9*FdhLLVArZ;NV*xmNg+78&I%zpd<|Ec7DU5_uuqdf)^^JQW zMI(Q^PbWPmU5gk+*M~%zpe0h-Zi`YSE4NUb&C_b?Sy?<%Q>{`ej*LO|w9MaIrhF|Q zE|(fa={|7KT*_K7=$8)%4)2+?r07TUjXHAKs_nb|oOQJm&|j^z(>yQpXZVm*in#T8 zPvsMyeA2<_<0JGd*Kq@#L_w|+eIXHwdq{8S0zlTHR3dMQ*77d@utnw+k4SggQg*dqgL}hQc+g0piVhG$;Ff9 z_0`kU-CLCbs2MN$-+Yh}jzB$}%1?z5WW+_sGB;jBp^i3hFR!+v#`LXA;nFP(C0J){ zdQgESWIn(9^31l?^ZDihuH>3~I#z`{u9UbeyPQk0^?H1K998dmWc?0S#!-=rcAH4F zlurFTd)m9SKITlcaAj9dPBWeH58`JTZuQo*JdxSSavIE1C#k`SO=o!i{iCCI*E6G- zC|JS!bbcQaaHLpU(l%Guj$Etn=aCzVcWq$y&jddb$yhebwx=+xCjfiAyLt8H2fU>j zM3rY-imsHn(4B8qiC6ZNxte7j!7Q20ebfK( zE6P`qbddgQg?)pPH?u0G2X9ahaMU!CpObU6GJgl{qZbQ47|J~FOw4J@MHgK#i(nPJ z`$AkkRFi<+pY))(`}>!gtU${x#OFg=cK%=v<}j`D*AGZlDh-7cvW&a=rO@elsg5v8 z8NwSbSpCw;r2IlH2nl8(0@csH+a3B#jn>&>?CBdbkGB>uYok_CW~vl++e$Y-H`Bvu z6{vcQROw9g%aa_X-Ire%|8{_Hz|6ncw8&MdNJ8En8Nqz=7V5t4kRpp`P6`jUj=NGJ zY<$q&*kjB$%pB=Mx6HVr2(}kKD0udGs3zXc3TRPdA&{j|EeS&VZOjxrHe!)gf7#lu z$3hT=SYwd_-)jDKuE2vhg*f0A`R`Aq@2HN3t|!&~PrqsPgv4gIPVaa+*T%aaVwMD1 z?0wxIsVN=Lg=V4ieGWzI0?n)R*>Sa&EVBFEskt-hR$B7om(GE+xY2DnjvxhW=SXHp7`onXSyfkw^kHuSBl^; zdVaOuT}R5!;|L}Pq*5H%I9xwWL3E4LZ!s%twb^=^6)kE{h&2g-Xn{sfkui z4eL&CWRcRU_+_>gv^CEfAzK+uiE4?D_Yk4Ek@_=YV>Qihj|C5t$YK)PRu{idbZ$Kz zvbj&i!IR{k#Vgtk_vuPZ_k^*O2sC0P&^_nY_QPFQ{>&@nK9thZ{enA6(a<>CZXy}c zlYG;{@LU;{>Rbw>ZJAY_TmE!y{ckm?iHTOBPmQTd=h-G|hTpad9$@mqTQ@Utb}R87 zkD#ZNkv@y^)_99TfeHj$A2o(94z6%C_S8B&9{qQ2ul}}m=d2y**eF%h-OcW8Bi+%2 z1!OknxfD`3SJvj2g_G6ke0z?9zzX-PDx}((bw)!Bby6t|s|4mpAdqR4t@PVUh~Za{ ztTH|Ri;~Dny&Od|H3zr0YmhfrUK;YnkMtY;$TPPA9B3am>)nm~1B%4jnRv1tI4TZ| zE-7-%)H}S~P@yyx^p%n2$n(h_6K?aA=k2El{K$}w`4g8BPAuvAwb9kCbEnq@e{xWP z)^JY7+o|I#_+ZsHQiJZhw+;qKTl{oWe*324wU7LIc+=6`a!%Z&-V&-%4 zv9v>&d$sT6vswrL$7d=ZoV;j&xomg4ufbulZ`2%VN!h;_*{5{kpew9zbB<3T{a%u{ zi<+{;&yjIp%Vfru6~vCXWk?l6qq4|+46uNiyO!9rV9V5(88e@jj;unjS0JZX#(#v| zlJio^7%1>&ifzWukN;8qq`o`Pm?+Cmnl(w`Kxa5oBu(%p#+09DW*2qsT69bHV!);l z!Nz+FYqYJx@!b2FSE5-OlIZ&`?+og&`TCAbVisq2F8xMB;;g;6nP2BSI2B>!rUn%n4@`7B!35@&L}8 za^ruf)2_m|nBmltA`(Yh&vjQ`)>fKYk!-S6n2NkCN1uJh z@n;+2F#ilk?&<%h%3m+1z<{LxX)hAuHsHxTVa{IKDXh+YI)MAzGq#hzW0$HURT(O; zcT?$|GTGX(OKb`!D4N2NzL`RrB;plhw0(9ct$sM_tG3PbT+Fh$6x}mq-LiUo6`DsB z+H2ctFosT(^6tP-2JUTI3J^0LJ;fxgoN$%(VUp?i@a;eO` zPcHFNuJm#&(R0>#vMw5RrdzB0>7s%T4uWYaJlp)4oBCa_j82kWQ-R%Kto}?4Z&uS* z<_V4b8pL>xvwMz*b@C(I^p75i6NWj?;7Z?^_l2=U=%?SuT{u#|Sk$j4uUSe<+WzX_ z6G)ugPnjB*fQV9xg@UN=tN^?oScO@MV_HAEYMiWfDY0v*qKGX|7OvhB1 zcr)2oPNEgIl<$uv^RbBn6#rLocAC~wZ1R_Hi!Np@u+L@6Eb+76S9;%HMi`&jXBX>J zX~OuKs|MMW^;N$u$q{~>S7Qzsa+xw znHyT`#dyF+|9DtpiC6>?n|hCL)l{G`_?$C0UFH%-OEDb9%Rft-4yhSxG7wd-^BYvf z=ZlQSeD6~8q=#^(Jq#WJ$$?MpOOPrKjOg{h4~p6Qd)61T5U5!xjfSS}8zQ!`IWw7| zVO2;%>_a=PTBYDKtpq2I6w?;~DQd4k?cGwgs4WR+6B~|hGkQ68CTiaPbZNKRk*m#N zozat$b{orhi&a&gZeF_)PuS!b9;r2Xs$&G=ngDYa}=F<^hDP~g{iVqZ{-z)Y*0?81&OoB4N$ zvZwwQm&RPl4_S8Bnh5{4k^Mp^vI$#$%0zXa7^h=XvMI}L@mQB0$BOaS)?4m(Iu?at zn6inR3H^}i7V{B|9RQA3p2BXeB}xZxktqb9(IpC6^NvB@sCf`%=gk+pXumZ`I6=9l zay`joHdkv4gW4&zq!Jrnlo(z#;V#06Wi{Ye+@E{!L7eFL-KS?=G_&SPrbxrtJoUfK zms&Sg`|ecZ%2K)EXyqA2V_`cO^`|i;%W66dj;Xd)rN-9h2x4rATeIj z47TYCrtb;ns!YoQoK3JC*?U1@^;eEtB>`AzEj?#g92i!YwOhWau2x&gs!L%RUhGzM z%-pJ5=SPsy@E&55{Ah(zrio-0x)s7E!<;H(H5dQt=DqFo8#9U9bX?#1kuHSjP@&To zG(?5=ofw-Iik`2LJmwC|=rRa9b#xqa??NNhzOamNkPD__#x$cSE?w663n;XHm@AM8 zm6u(xT^es}`kGNBYg=Q=oT^}M#yZTPKSqWexV5rKIot1z!SHS_z=@Wpo6Nx5G=-6L z?j3HeHt7DtiYf$YV>rJi3*(FypCz2qg;Bo4k+y^$c`KnY)pG0j?n~W0xf^83EQyYr z5_weZwynld8}r;LvQquYGE#Vr$@Oj+`C>2fE0dB7yU<9nz1w$4@^RjYsaolSAouCJ zNTfARF|1o=zJL!q2fEs~t++$4Yw0a6Ymz>y#P3P+Uw_lzFP~a`{Q7BeHM||0s%v+!*EC^MY2p>MyT!)txA7GPIi-H>5LXD0~wxLVAeU znnrH<32MXAqUeJK`dE5qL9I^ytpVK(6JTb(J);$fGO%I{h{jH}z{u9A9J5%i|P3p*2h>F%}wCYPeQ*JqeFHL~PAI z(`%qEaGZ<>$Buf{I8y@h`wkcKo%72lPcW7`@%$dSKnh|P39X=Mq?P{I6xM*N#;2A2 zj5r!C``;-8U|}4+@7{yo+NSi zHW418Zto#$-Mg=FcIokWaioZ6Fw<`xB-WfT)&_U;xE;uW5P!jhA>i?<7DJ7@)I?^Q z;oNUxPF~g`Czu4Ey(0GG9z;>6w3Rs{n4f1Ijj|(x=j@}hHX2oJ3N!7mj5EpQkgqx_ zcEyl5UULORtjjdMoi>#gxQN2kB?X%T%k8Mdopf#G7FYdDiRpDkaNhf)sKf}0XNtUi z$PqE_`G6Lmny>$=>BTAX1!W`)A1##d1RMs?i*LK3i|2jwVw!e$xx+XJzExW#X$_>4 zyzlk5jo(Cbs!$uM*Kxf)pfoL1ZOS#7W8+lq@n&9ABmkp}waf91tYC;PhS1p;;r+u9ot21eMaAtUhhmvnlICw%;?Xj=+Vdck4LK5Tby{gt zGS5dJ**XHk^YZkDEI z=1aej3~^+{-oxUgtcrcnIwS$n8H$ADJQ zTzqJS_K4G4af>W+?6d>IEJ|&YSh#z_zZ0K)rd^qOSD2gcLm|{wva<#03|AKSwXJH? zJJT7jt9+)(uK<#5E<1<2-+l+_?#H(dw4zw=`h@ z*aEylWcoQ*N`L(zOTE2m`XtUK=ibou!+p=A_OVNi(IfA3hiRqg_2^pm*_9dia?8M1 za?5RMgaGGy)Q!dQV7QZMF6+7Eyi)70>tR!}bLnvB!z85cJClughs{`Pz3^4%dELWs zx!4`q^XDCX9g;^wU(=bfAXitqkxVw(VZLX65vWOBvUlSE^eZ;JK~rY-h<)i?@)$1? zGcqNzkjvVtUaeBK*puT_C(}1x@zYtu7ecVhoL6zay7{QMYty)UZZ#z{%}qD>t1QO^Y5xd+lj6*8|M7uJ-p3% z68z5NIWv_2l^w?oE8x)*=u-V-Obye7`esaA+#$hJkH|t5Swaff>4BLEQawKx>AHVg zkykO|i-RcE(a>so(dMjidY{}De@06&3YF1qYFZ;5+#8cc>{m~TQBi`1(jn4_@KlCz zFo+yUeu;cFA-%_l`U=)>MKg^U6Tv@Y!5A44$<@uWnrR%3cGkEr1vV`ZnG3>x-(SrY zHsjYRl)6(>PU%1} zH50Mz&E=PKDL=RbzgX7zU-UmX)8%QEP3T+}4x-h9f#dZ{2wV!njuS|vUIb$y02|Ai z&heq6uWqkTIw6H?74p-4mK2%-b|k}~Ka;UnArtR=miB{7lNT;fm>LSj_ye?3%CZ_J z%Rh`EXcQc4x6f_TPGzLlkAH>i$r*68W?K^?#&nTm_=WGq@mevJulC?rQOQaaG5r-l zrB(Yqg|cR{fSoMS5-k_&O$`vhAT|tw$GV9TQ?0u^GP=3u;_ybxFw&)zPmSu>`<+wHX@g&9t>d~Qku#Z zR0eWjJ^<@|! zlMbO-1>Tct0a>@i3M-dyk=iP2cJJe&^CL?h7^(Q-JDgOGB^*IXAVwx%NqTW5gA0q$ z-n)lM8sU&b5ye8v=DB>!ecFpzn8!&Kf(<%$~_9al*=+;sWI zK%OfMpgY75aiN-(AgcKre>=dQ#^QMiQg?Li2^VNshn=OoX^iPzYaP=1r3$ovb%JqaEazC3^dm`2$eL# zS=bZ*XQFCj!nYyDg+!*bRnJaMT?YA;uGtJCfIo;4jz^Tza&y3~Jj->;kucBO* z08uE(3fstLtrxEDUEMtzplXx%8Qq}I=u=sKfhkLZY5k(&EL0*WKo$&swS0_ z&y_5Q?69~ePFiX2$6$K`0H;v2>BESK*=pnAG)K0D3Jm)oMKa5`OAu6<<7@`A95AVx z!oYRkds?kODsxQz?hq?o?1gw<5PHAJM){PwY^H;dwv8Lmt;1SSCyhd~1}NIfE7&~X zY&R_B%pe!V@uN!a@IqhsKmt8~@LPhV^D7s?0Q0#j4EMuHFz|E_KYWbcnv z|8{57WzZb{hW?sE&kgaE6X5hYV)4|_+j6B+W5>R21C_=|fURP} zi73e54Gor#6qXN@1_xYpK)M4f3G;&tu+pYVnmt1=yhxMo>a(s$c5Pbj#n!TZSLXmN zXyGb9xOlUQ7J72o%C*TkP=zDo1?ViGmNBf~(66WVF5$Cw-sPAS_>IE(&`ckp+Fy7Q z(hvrM-x#WMMF9^M7AEIX2Kx&UL`n2=m<+#DP!$Y%L_|42Bsn-iqTbZ~XYmxU=L9G+ zJMRKn!h22!lP4`nO{Y~-S|;E<+%t+;^%dUYFYrT4?M$e$H$ z#II1S+K2c5zN=N@$!-^<%$M%dvyjbFZZ+XzstP=^5*@9?Gs)WMbj0)(zio2bJBI9= z3A#*ax{!L4H0w|uH!Mb9DL`5TfD-|0|CpcWE17=X_ZO~fteQicfjfkAhp(0rP;X7d zW)(M{=i76S6~=faoH<_E@07*88i*;`7kA!=S0}D$3wu6cWz;*U|FjMd&Nh*+%-MS! z2en89Ri|j#l$U)n@?cN{&~ZIV`}mpEoXXkbt4C!PBKT?{|GCNjJv!~D7(g3D;PgtC zvkh4NDpHb!Nv%uVyCoJUdBQh&-i8E*w|{=OkDV_1VsA`L!q_7&;1)HmC{GavtGY-Z%E;`B8Mf<;W^a z_Ggk-7sSz+{1UBn1!7dI%o9sY&tBQ{5nf}pQYs;AYL-id#q|U2mdxgI-yYYm-?b`& z+$2ci!7SR=SDcQVdq2%#9^xH7Xu2bigiXDe_<^AI$ zow|04zg=)*1FQC=jVhWcV6vMq;{jeRNfqv0V`Q;7PCd;b2Cg@rXNi2BzwgkyE zIJuEvqcF=2%ApL!Pl9=OfSC@MnlR`!h%YF3$X+(uyv%ve!1tabIzk?4<|YYFYJChr zO6$6+P|z!6Oz^RY6)L`W*e5>u7$p1sAkOE>!zDEhYwcFL5qyPn7@fK$g&!llkOhyF*`HHg?InhSVlN$CQe^{w9H z#3%`NbQ}kCkf#^X_NZ>~S*lJa-PUWlE zn3^hpoNGzp&?+{b%aA-01c{Cy>sr*toBA;n$5yCwLb`8`o3EgG#LIM{!dzSHQNN5a zC5_in*GIME_%gvP>^Lc{F%`9H;DHv|1y%HBPY_q}0cn$1hFsAQ1=2&us99!uP{VZn zUH~Q?6l&<<`j0NaeWG3k@}x3jpD$7neA}v4xcad-#;R?Wk(SM)aw9dkpb03>J-Jan z3a=v&Q~~#LMQ961XlW93gO^RqxYSk*2Nq3$A@F1*H9Fu#-p4XBccDzGy zDj3CZHW8YdY@uPe@ZrUpib-=+%vJuByB3`v0I4Fy=gxT_AILkSsHyIIF_j~!LP98g zH>veI4Ie<<6AVB;q-Dj1+h2G2baChL(KeaRr6OY{Y>G5LklN2wiQ|g;tgTWz)7T1l z9jTl&AdD^NbePtbRntu1JN<@!9?yhgAh61PGL3f(BGMLpbL9)JEhsOu-OJ?m9!$UYUf(S!{v1}uotur_+5tX& zN472leR|L#oV8Sf&n{Z+3Z)k2K?%uXU}Mq(aTEiY;0x3KApFTczPKxL)DcCpgs=!9 zYz~>CM@bq~eeMCTEZ&6ta}UzO>LUzXtnAh55%Tp)Wz**pzfJkl$K%`u65e4cW(3*% z5&x10yj?h4dH6et$+xoK$9JYhlF)GGW~G1bBr|zsQPlzWeq~TA`>Fq;l=a&fy3P70 zT#+srLaq?`4=%FH!aW=lBVs8hm?P02n#zOM$y94yUZ6crA^>8`oH}5D}okbYh#R9 zL|X$%!T&bYMXm>Yx7DrE*PK1_QV^{O^=#LMhmZs<}Cl}zFaz&=0**p9mH@`xAWp91_W zGEIkAC28?eWb~0+Ok#y6wLsR8Bp6?1M&ICTvYu{4t(%Aj;uMPhf<7D%E*Hf1%fl8dh>Y$637O9$p7t z)-MaEZ+gq46{{0$|NRhMj_EWLC+uZ1uNc2P=MQs08X3#?84*pvYdx$2X(HZRFGlC6Eae8EF6hjm!l%E$O4u*~swq^f;A_;7nvzGcbS?FORjYZ}g z@3_+?{5Qs!!JDznyj8cwmHwLX@?`(aUG(Z=#lzAK3{@!7U&1w$s8(dOfOZl8kRUg0 z3z;G<{&b<~r1E0zfNqfi?wz9E*3HwKpuN&MfU!6Q@j&NDp-Skl0N}zKV_$2SbV|4c zNBxWP_&}*smx8`L7e7Bw@Fmtn{LtnV)0;o~y-Rk%F5^b4;^yI_@G|Q=5+k*@PAJ~K zn2`gp5=CsAN(hT=6`q#E#=n~1?tid6g3wsxI>{sz5*UES zUJu^%$VZ=|Hj6O}{Y=U(MYq7qe~O3&LN7HY53=6MuHb5gy0%O8{LV3<4@%^-$m9VH z{!N^&((F2u1SpgzBiWp%UR>o`yyW^j?%>zhzO!I*0=qr#?ou{C0HqXLxZeX}wsP#huTiWtH@M-Rs2)z~u{ z8pB&4Zq+IWY*#9xbaNfFMa^_^cnJ>2c6=QD}D zxN&tpchDf!f-`D4eaQ8sej^Z7t+W0o!1kXxqRU4qFwNz1;_?s0xV{(Ke%EUd-{pw5D! z-Xff`ujt&;5D1tHP?BIiQKOdsaG-U$_JA(hqPw$zQfGpP99xBmP#17RyykABjAsR#T@AO6aMf*#<(dq4oC-w1uH(dt+Xqmh<;Udd;%canrdjLS3D*|e^@yL% zi#=b$k$g@Kn|7>gpi;Q~J}O1{rp35+!Qv16b-c-+>jUYUz!z4^2|LSG@b>=C070*Q zdnBK!_L%xUsyRF1YG{@HO=Bj&M>h->75H8-iNbb7zZVyxBoVp}e_a`!%N~QxwvS39 z+zm8mfgex^PbwqPMprw(Kj=fwX%-&{Tg}F7ugCK%EfAGhJiz8}t@#1HI-!CgjQ@k4 z*}?rGRGA>yKbq3XRw;Q+*`YQG=7nkL=$cgo-6LDO_1lh}%b(6v#j;2HdL?CULbtOq3%oBBQOAI78~LFI zov7^KM!L*R@f{f`mSU?XeD~Fh_`ZTl=72U~9unt;Wjg+jshqKoWf-zB3vL_Q(3Yi= z8_iE9J1;tr1=L4kiQ!%MnVBo%rb=c6x6T!DRb9urRv#P8;icC7fk>K~*O3Ab7yVBH z+n+%g>=qMb?f&MFaEtG#+dEyr5(m<7$MxX>~v=%I1q|05jkzjS>`s zUQD=!hN3+F2)*~zv{9=v>6`-nbNtFNYm>lBNFF2n3Q>4}1oJSSz}29~vIwu+)nk<= z<3ELvSY1%6(UcP5|)qxE&F$A6ZJvO=|_7r}KM-dG@2F86fsz(ps+&*qr( zUokTrPHeBb^cWBM`?8I7QVeI`t-ZXgQM$F=dhZhj0sJRLuW&-y)(zhO1SKCNge9vK z84r;UHg=8bI^VOJTYT*SC!-L+hV)KOT;Kiu1_+9FrrNfK_r=|=qfNxTlbPSUg6A=^ z24T^|SvMs(C=9)13KSC@pqB48S{OA@v|G*!Lu=F*)YL;*xgt=I)%VVF<0UtI0bAis zz^@-yEUHiPS>IJ<7ue_xERg*3&S**{R_*O(V-|#miWf8$k}VlAFL{Ji&Nn!*ecMGu z*pg|NUoQM~Y4hxjKuy*UjtzuXy?oX5p`sT_0LrJaM4g@)!~E|X4nV2m5_=w<-i7m+ zI_dxy|6h>{2dEPCxv~Qh)qLBoGfbICW{69!SMu~Co$wCFAjKh@RUv@(>AGi7%QEh_NAm2BV6|qivnKtvRRjkG#5M<|G71l zj&MZL6I^x-B>`0{@%(1z-u7E744D&?npzgpT4OfD%K5T>1~bTknSo*>PzcM316B?I zyL1;g)Trb68}nxtFB6i-7iri|lEuiq{10(SC94yKZ%F}OVj$v{u#Zt49QkxEGZalq zooO59GijPN2^}_VvB@9eFaHxiEo`RV>~|Zmo?TP=tQXn)^z`wiu!p4Ob}^rWZ?tE! z;{z`%D^fZBK*eGvm?vW-sciNuYjF6GS(u3$d>*yH_%fm5&a^=x)1Ply>@JgCl{M1; zgp3X-t_w@+2g%o$)cTBmO>y%scdK>0;~Ue-BYti;TD>DXjc zk3%|6I?{zA7%xWBKME^BpC{ zNB90`^c>)ac#z}r$`{xc6p}?lxNXSHPYU?sCO8W*^96)|!l5$i0RsdapbCuR3-49O zKeN%EjteD#zdAl708sM-A>Y86`EPFTAxnNGXlNqzJ~xgc?yj__RJoPM!^fL z=~+8!&L>Av!lEmI(C0|I>1&BT3Pns{C5%ZCt|*4^USlwkZXdlAC=KCqT99O(+K;P`iM#^{q%1!AA1G%Elz;k) zsO2Ov1l+M*p3IJm)%!@%bXR%XZi@U;J4p8fikBT`V)~~r*cLTjD6+P~3iTL%V1B@b! z{I?_91RHniMF9X%B&kVQ2@{SBpo#L;dqi51^2|nZ?-O=`UgllTS2UTa3HC_(;phMH zV^P3*q)K;Bf63UQ7|`DZR8l;=2_`-rCd?Y!PJ8rT{c}H*uv_w zTlj72eMFeo%N zvJ_Y?lz+G@=E6vY_Ys8)%0wX1029+ByQDJXS>IY3A>;MdPf8rjJX5d>87s}V9SmUu zD4+(^b?LYLFUzn;OXlz-VHRm4n$*BDxeqTjRDU=UV3}b7;r+i13kb-hc^FJ3XUrlR z!*GOw#P?O}FRB+L@uM{QRdtjllr=KH{OHr9t&k@ZNF_sP2b4AZTPcn56|A}A`^|g> zh+O~XmeoT!L^4m|)?uz}nR)&PZEqb_)z|Ki0-}HlC@2krq;xj|B1ornNP~23QgI_C z9h($3AR*G-EscaUNF!Ye2#ADtuC3qqyyrc?bH^R`jywFz!QN}nHP@Wa^UO~?i(DBa zN)BM4*7aXdj0r7XdlSDF_BvHp-iYLe@%7f{A>EDIQ?>-PW*Ayq#?k`%yO988FV$@g z$5UslrcaC{%^BSx1dF`wqc(VmCz9Y`apTQ`{0r-NWl{9nfS(VGS@q?hd35T%dCG9sePv+4Q-ZW?K70x{X}S@5%q`nWak``1Elj18F+h)vwN=?wc`iC zV_>@Fl(Xj~4a8T&9eJ%$j!^ya8qNYctDtNp$`U3V8-9DpUd%)eeoIwy@ji^_rItsr zw^r~YUV{>NcNB={dx3D-IYfV}3^S}`z1{uYD8Zg7AsbzYak9+1Q8CL5_+~V%@l(Z| zVqZh#9aEPM3V~*DnyY02cV5b6SI^r(Ke0MTz|;{p=>gN7)&V0H^$Q#8rJ4dHQqj~E zmZ&ZBbFz*KV11k^CUz1hbu$W0=|0F^17#}Man}LR-8F4qHP<$6uI9pDX7TG%P`A({ zxZP-_bmSuYL6fNnl(DH*B(QS-VV1762;2GXdMIv5Y5X8KotE5F#;P-vfqBJ2Dg(gg zW}0n15>4wT`ZVs6_Sn=;od)J_GDsjXt8$QlHn2;l4~V<(8<=)I3^(K$MyFpSbeOFq zc>0A}3^M4g6kUA~H5hXNQQf@g<6Jn~=hWeh2svIVuck=4%0baoKve90X~?(eirAwg z2fqidr)SO`h}ncUo3*^~92OHj)3roeHxg`4~YI+lDc z9A+?Kc|5ssvN^@`1R2v!=G)*FsZ!x{pMH-(rPHq_ytqN!t?0eC>L-f=H~^7vfuqIU zJTunku&;G+Z$KW0Y1Z?8_#`)X_iXSP{Iwx;E^Wf(E%e`KXdIid$3`;GS*S1Hix;lK zWKvdeD(n5$#4^Q0@be|+LhPm;yM#vf{F<6>3OEJQeWJi13uD-#P%4IaIq=|HV%2{iTK+P!CpK{6Uc_V*Q6 zbYcK!;K0Xr>}`Z3I-?T)#1EJwQC9RP+thBEZi!2_%rF2?HO-hc{4O`g`Chs)6kG3RVwlvQxP4^9 z6&AL~7$qNh0fKhzJ&e2&u_gF?{72#B#9Qq!@i&N5Q?7KSk1@ewck2SXPDTx@p@c>Q zh{cOwoGB}z4EOOvf++?Oif(W^Qp`k4aHnW_xH9)q8PPgN5ItOgZFnQ>&A@e2uWRdy zin8c*i5ow}1Th{v-??@Pmz#%EZblHRa=QUi^pty z<4P`@gf_nnwI;M)kItHmh)u1!N)ywpXR&f~*>_Z4EF3Q4WCXgDE#wIPDSk(>XYZTU z^q7qP>O{q1xD)`iSKFTZ_8R2FO2zZYSA=s{{#0}%=r~&rNiLtKzI_>!Zm;iLgW@(^ zq4gRp{)Swt5EjmMX;-bUWT=lal-i(peJiiQ!mHE<84BTaL9nm^2$exk1Q_f}kC*MJ zc0R29WufU|(m&s@ydCZQK&$_#ks7>+tq(s>N_UPa?6*AFL|a7qC`N-FjN$Rt`M$aD zTt+Vwh@Tl_bvIG$)IOjeNoNKI_-2#{$4tKGM{UW^n3FH4@q-Ay7z309Cqc>en!-Bi zBu~(6=4coAc0_PyDM6{6IB5JsCX|A-`F`f8q6ovJ^6=%#Uj5)Tu!%(R(A78#`AEY< zL-6%g_KbWrg^wOeUOw*4V3I4F!-$d!7Xt-FtrRHZODE_6E3-Z+UedkGO?oEw)1hAe zecL_m&gzCaDhl+)SC5Js+6x945UuS5Pz1fak~hPKC}n~%(Gym2AdOxvdd+9xpAilV zWBTALL{fryP)my;pn&{K`)TjibiD z#aSkdY(k8T^EeUQHWesCeary19D>8;qVO0&CueFtY~m2&(>M!g7Exx9ax13Ho5Y)% z)9_~kl@(f*#e8n%dQZiMo2YT802EnH@UaWdVdb7$Z`lI)O^?T|n0D@l|6o2-+mhYa z)lXh$u>UPhBd#Fpdd@uN)oM_O&hGnAb$v4z9AP|#_a+Vnzt`9G2md!{Lr!o7`?zfH z&T#_f-dhP~*xy*}+witCOHz4Dk<=Eabncr+$|^5&T%9epohT>*9U0L`KPjearOW~Q z2(>Bs%7JdKi{MQ}U>PQyf{3wUr2Ym-zA^s`FOipR$FF=lmJ zrVYUU0n^tG;JfuRsRd9j_ao}ZsshwcbUZM96fjz1;jp=YyM`qT_ke)~>+3-3bDW$# zZ6irJ3T5!cik83^N89b?;=@XQ;hmn3#Z0YQ-)9!yiaX{v@zV4#=|yj zbJNJ)3*jj}foV?bfxq2HFM!3~U{%6e>u~$MakEe!nf(P|b@v}OLyM9SLuQ+iJs4E%CsK|C>XeG zWE_$G;KH|SFQH9=P^ozM^=ZU3F@s3EJbC*_Jgmn{?0az5?S2)yuQ1=NopI)$QqXvQ znqZKPNp1?yU7|f{ne+?2yQZb2Gil=i`3}9VFd5FTZw8;WZb1jTMO+N-7t-!HkL`fm zBVEQWx^+h&g2MF8%;vF^+Y*lP*mre0mQTcBE+L6xzUfdAf7g3pyoy(| zx&6;I%`pI1_K(sf({_z6LRonm0M!cL)DH=n4G|nHkmM+uky<@hgr*BIZCa-{7{gs? z!x&%#fy!iGNBSDhg-#<;r*s*RoiBys1Tk@PLwsc<9 zRG_3&zE;Bh!s6*9Jgl1mT14`owj5p63|?|Ow+Rm&VAqW_cI@nT0dpm;&?-L%LOOs& zgp(1nhbq$()BB+vaqrZ(scmewv7=f4Uicr(yD&86j(Rkh9g)Zk#RzO9w-+bnShx)J?POh||#i;wE2j z9S3vVD}eS_MC*WhE`P^MN5Si_jaTqH0%}V=cB-%IqQp4O=}@VC$a1ap?$DMhwm!3w z@JswPMhHt4hYMJ>jqRP050RnP7C%TzoL9(aNTF-BS*p`s zv-}QUPph^T;K%Al@wsR(#+D{yIYWG@H_~v%=nO@mLaCV}`>wlJ;&EyuwSAo2y#)x@V2?3WQYfn}FV}AFP_zjIVb~IM(4&I2+$xIYnLO z3Q%rn0W6O!(Og~Iz-N7M#5V)yTobipZ7@EU)wn-;Ggi097%#%yT#x+wI{~1Et`EE@ z8dSyx^dTGi!;x#3?5GiNq2p{9HzcTTHUA{|{sSznXDW;K9KzaL;q(&mxpR{>aePp9 z)Ve?@TeX*i>%WAjC=lkb1>in{#e9wZg&KGZIOjlLlL#)=K9G4crmFxiIAv^Z&M8Fk zpOt7R zLw>qDV?SEDN6wB{0L3C+fN@K%Q@h0;eOB`EjHx#-o4ywrylbQ7B7Ry~{rxetC%#K# zKAxd=*4oU^8LUk&tZuk)s9>r~f1%dy39K^ZOfRH$qClqaah3h*x&o+dMth9aB!95Q?bA z-~KO!v=Pq0@q_A$+WUeEFIb?8N}VzEKW1F_%Vpcd6=l-r3j)%8pds_v+1 zGvrSIgXs0VM#R2C|Ngzn=OI?TksGwP?I6b8Ji(Z1Dx3zTj821xOzUy%I@C;V!^-&j zyI_e4Go{6&j0m5Wv0^6>P9Fix3eal}ob(~JRYBx^ImUw(2? zq3=CNZ*e$p#x|ATIC1M2YZY|z;w=t6rD&7a$<HGB+a-0;B2 zt^2EqHsD;Cka)(cFa6Itp>agRr2xvo1bD@MMpJ!tiq=QdW1k0+@fj0Bbf?fWZ^JAc z0~&@Mn|bUkH~x!&GI0}~$;EBdO{`$Q853)fU2aeAz2-j4PAC^M>g@tTR*wHkZy1G+ zWT9<1>BOq>0g$6eXn@Lh+fvBr*><)v@vU}3G?DfZ^RiiziLrt{cQBAUKwg5j$RN1j_$Le(osxnk-AlU@we0ECEg9mZNUKh<*ODcU?TFd zXX${%rCnyaS^Rp}SF~fs&%*VBn&_)mVD%``0gU|eM<}{oXC>x!6NSzj{C$yKY82fj zoD@%YC~ZxDPccAG5bdwR8Ct{TBuDGb6!h7mo zBoNNj8T7|ud$|EMfyb~JKa^F_adh&w@v}B9{^49*Ueh}p|7o3V{?Zvs~Ne)HtaW% zO-qmLa*8=&G$`B@|61f6bXZ~pMF$&W&0|sawFK2X@YNnY42tR-Rp2ByHyUrLFSqy( zCn;KqJ1AnCJ1CE=dDr*gGs(0U$P$)>@W{M~(Rk5PnXSUJ-s!y$Ou87n4Jjx?fj=Rq z38jk>{`r!1z;03OzOa@4vbbKc7?3$8**gYH$FG2Tdgfr)$US#bmbNnCr=bPWkbp zd2LR2g=W3N{2N7(0a~|{!<6R3l2)PlOoQUMMMP!r(C50`-@NY|n2=;*VkVnN9gH0y znta}(tJr1m==@MSk`1f|PkcZYzM&k$`+X-KASEW{ywA((xOT)mbphs_L4(O%r16fh zU5W8Sg}N0Ry1S3v$unhIhZD!1Dl;CiA{`j0zZ{p`G^Nizt6_ruenSuEbJlaHp6JL^ zA0#v!`kt{Q0E_IPMlRajYQzGF_pD0=)j}1EpXrpxA2_Eu_?TQL3sc39CNQ5!UeR|*34 zkZA(f8Bu`j*QJcG(nv(9IBZUb15{QG*gAK4wzvL(?227RC5CarI9owqd+o{$*w*Fl zWMCs`n{V*lfPe!Eba1#P;Wu=^&mFx@-p=3`96eA*Bl9kI-F=c=dbHEKV!CjR@O|TD zoo&}!FqnBI>UiN-sjL$X?V#KpSdIx-=1{llBT;~lyVoq(6ODdQU7NdmNDQr&#WQS) zE^flyKVY_;AO#BSz*c|*9zl@mFD~tx^A*~(2 zs^@RI%qXv%?0ExEJT##ZfKBXhdigKKb|GD_l8{awi=v_NlR$uTL?~HmqCRMZA=CY( zJep3~vSJgGqq5VreKc|9W;>X89d|PE>EQ3xo_3eUW=BQb`jgsL{Oez3zi}4#qi@|j zQUO8*0sgm|sNtPqPa)6khwF_+@nodz0(u<(YV{nbKu=m$6p6K<&>95$EIgEmJO><- zE=4L?euKL9H{XM%B1?PJ&#_}L6#4*hFC_nBkHga_h~&y;0Wfby-^Qk4d^snoIOjoU zO5Rx$Xih<1pBYFU+t-~Fct)iKLFYo4U<()drwHTi&-RN9hk#(7T<%)dyKaD<24s1Q zBG@`bQjruwX`%ADJf+OASR2L)%*FcpZ?*h|{^0M2*d#?2$@uo{r2zVyFI^)%o}Z^Akw{{ zN0+5cn_LP(VjD(`ky;^+-0`gt^hl-V1DEf!;BKEuE!L08>0q_g{|{{+U!uPiNTxr@ zFeb4|gQ>NWUusWJ?}%i_2+d^riCaY z>ghl+j_w`o!u?ge@INV;s$Zpkw{d9HiZ5s#m2*pIq8}I*g&<^+eJ0yEqd>Cv5!7%N zT#QS_yq9m+_6t#~0SA;uRmHllnF)&9MqgY$Frwu?6GsWVwiTS9s^E>WUo!9 z6#N*yz27jx9rAys@s|^!?uHsbzZ2uU!s`A3uuNDz9lidGleYR($Ou69T?q(0=4B&4 zu=xQ;QyxA12=nKAhH^607n#tpr}Q6`DBYFW4!@9_CRofGSo{X!Ky)iHLA2_w+<#S8 zF#$j(sD+Kvd_0?aoQ9HAKa`i~Dp4tKg8dBufy8sx@YG4^86_FmJ&S<_fC2pE!pIxL zZQoR<4DzsbCSeC4u<&6v`_Z@aGOY?zXb_&nAm;e1u{#DN<%zEvC=`x3UpqE*(*>eC zP$WFvq9fPg)&Hha0ugL6;h!G3P{Znr2U!cH$|;KrYgz#O4E}0%IeUMA z4K*Y)(9U-`gy4B7%%UYoLy7D$P+t(M3;c=o(p%uY_tJTzie=w1qaosVYW7x*T$8Cj z6sxG807Sa|6hGTRn+64ICs*Kzrq(WqcgHw_zmq8~TITtW43`uSC&oqqfc699qP!tL zJpMV32U>y3*7wIz>>Wqcr@#>G#{g7kCtZ~RcRqdxbna``8cY}31T9j5x-2_#FwwMx zs&{$9lz^6?&busR>s<;&>|zVoYHA-p!28@ zLaPURuN0UW$Q$}|u%$rdEKRhWVG?W2+1Gt|gh`JTa)Bi1k;;$_4yR#p&CGxeo&s8= zF~^-#NCdJ3GGCR?r6*)JoZ4$il?mLvtmB5r-Uo@#Jq1wAcsBzIo<ze3Z_cB-vE@XpPeum%xzDuwLAZ@5ZoF|=5yu$R6vwhj>$wK z#`mx5!6W5dLX;QCBiflFKel$q3JH8bbiA<)XGuU~w8c@KRoidHo`!)3Awq53Uhl9# z9{HM@J7NY#)iI#5 zx)oij?6M*%_Gv$W6i49wS+}gAg2rwu0epMQ6J;D^{BK%3K7_E{O$85yB!e=0vWdC% zBg+E0MKzpkJx0=^-)zi=WaCFK=dSn&QdQ4hIs4#o{Y(kK{}P#kvJ@C1r!}|6bn|%j z7z=hI2N^tG7EOB+cp3wa+D;WnQiuPW5-*ai)Jp-571Uh}9{ns1fV1zrxg?bi+6!kZ zdj#Qj|AAH!vf)ZD4c)T`|0p^#+O?BkkGBsh2=3m=^Rb~Znff;^UPHI}|2r+-tr4*r zBcm_>ixw}<02l=1{u%_N(c~GwYW#OCp72T|m1p~s^@h*OpQ6uI*YDV9mHaVoIN@0L zT+7*ZJG(-`$q#^UjB5S9+Vzz`l`V;%K?i?f$dtpIbFv9G{$Qv`rogBX3m#S1PG1-L zhhroI*mVwVql;uAR zWMw|T_*-JrprJR|zbY6OE^OsVLpzud!i8M+sHm{N%~njWwPiD_LN+T6IVRzMAd6&R zL$x(>ixwt9ul=em18aN*{<0SUGl!DhI|IZ87L%q(j^N($;Q4T+b2Fv?2T!m$|@0i3@9DI z6sU|vQh$%hQwEiA7H)s+>^%{~KW5B_r6zmsGz2#>`ZdrvRgAR_7tj48rbR2tTKF|* zV8I`UNtFgc$%vcAH3~>b^~@B&c9#R$j77K%g@k*pDHX;xC*N6dKwyWQe`7FCHZFas zEu$gOxJ1$rpWNaPoD83R!_m(+JoTv$GP|t+oQZ~X>IeqY!awu91$xt&K1>OqjWRJz zqgw!f=S_mg*I#nJEv^n8-K<^cXN+AL$DIbJ32FI%j-km1^=-uQN+3Tl5%fSs6BTG( z586{u#Wr4g=sseN+T!#*dJP%DJ!X#X;{;Q*5$PU?fmt{Ezth}B>{641SPxjK3`7B0 znNs5Ru6xB>6*^PbLtT-W1($}z9!D__Y2N@8DvQfNE&QInW68YQphzq1!Ba(kg*#co zBw|Zm^Zz6h)}g~$%DcFGv``9x%yL_Ll%P*>uie9Rm9P7^!q#Sw+Q`O9K~2Bs<|}ru zIz9mne<&6W%7Y56;mR|AO%q+hu^+>w7+{@M9^qSLKDyJKAn1w$J7Ku7h?pZufZCnN zC%!GE=E~WFAn4Aj#fY;&^9ZRmlS=XQ1ONjyY`qaOVsbgIYr8CK*hDW;;oUib0`nq) zBJUq-1X@nJ^5v&&OorzVU_z!)(+j6yOm&WWV$b2F4R`gNPux#x|4nXJx^2e|C7#>= ztK1HoqycCpoD%9}CevxgA^SR%*40UW-yj7}>Rdl8u`Fac#ZUFwUcc18ZE=-XmBDxV2KaQ8;}?LvJCstGwF%It)s!NgCUtRXU2PY7!S|Itb7q{?SUQ9gh`WJrwt*3V)!17fGLU- zuBoK`bNUtp5Q?sZ!4#Ww$fP39Uq%?t2!G$`F3`u{h(MJ9S%jZ0T!(Je1IvrjNvo-y zH@aD9#^Z&d{fP@?$XpCRiv(`LTkI{eRJ8FX@!N^WSdsBzl7cL3p}(-Zk&z`e4#k08U(&d%qJU7u5s_ehB~1qC4d`BNhp0%h;-wn6-CSS)jt)nhYpLfc=zZ-z47+ zK+>m#3OILCCOE49COlMYiJ{4}(e3m}+7aMFOdQ$|HLX|EIu7g|hKDlwXj{e%P`gD5seE8BGKo%OIY5}=ZhG`2=np#(ZdBOA{Up( zzCGHmy^-K?3+!Lyt)3?^cSs|``@u(Mu+&c+2+0LWyvut?96I;3rO~aBq&u6jxZU`nPw|v{|2n- zq%GcA*7Z7${3)xc4@$oM0H_Jw_Y6#Sbw(d!{OmQCjTlReFbOj}d?jF3HaR z4Lv9#qZK!$Iebj_1W{o}RZxye{SO;MK9O8>-kx7{j*@cx>j!|_@NwQ(LkREsjhSIS zX)j=yH?xgaJ<@!5DOZXG5>rbO@EAI==kaL}=lSGwLsczDGusp&!A}?@qRZK=3(UF? z84Y7?)=}y?*wZN*FvtadYcc)1l=qm$+l1(%Bj%O@- z2^MQ^n?MzjFAd6eNHA8XeuDp~nbADq`@d!5Si{%7^u9|2t?V(aCG?9-z|cUSSCN30 z#hJ`8xjk-56a~y{oua}m#|g&WvWo%)@Jf^G$KpI3CRX&brOH$jCWLki_{-+aI{j7% zrxhBp`#F08^SC>NIOkdrLQ(qVF&^@D=M!befktp9#OnsVeHjo;5~|(%1^#cKvhb~P zD>J3mcvH_XLD{FFHu=vdbSB7P-cl4t4shll+kMayS;5}e(BxOB6q$3#9VIGbv7LLB zl(cc&w8Vt{I!1g$uFrN&rr&P`I4Zz0h|a*%r#JcntVH4)C-s!plnJS|QaYOLR@R39 z_>b1)kRpTjPK;1r7a)!X=*2zeYxs;fQ=#0JtEGXp_})|7u-m_A4nFlRWx1OFhDNH` z`XTA2GOB4q*1KH)(ev24r`w!8oxpV%DCvL@>8*y=vM?x#P3U{A|N0I~8Y3SEmnLP8 z*WF0M=uy_)SjZYRgD$rh0GCJF1^y8x5$ej0#0v$d#>(jg=XkhFhxQMzz4B#jvfu%q zq>6*fA`5BTV(b2pN!FO7u_Fh@kV{F|(<&}fa?XFAcZVNm_8vf}-am_>Wt1l>7_e+g zRmvB5nWKk=^#3fU_#~&H<t?KGMTa` zs8zw>_I>$!_dSYELcgX|pestYXkzGl{rFz(L9QOk`O8MT$yMLMD4YQV4y_=i9!UGAQ;t(YRPMAR^l%drL6Fw22nns07 zd8PnDfjFq(3n*K+5W+iD+g|O|;h9dNANi*b=F84D`A}L^V-Ns6EdHm0534}~=(vEy zE#K6U4VVw(*zQWkfp=k?x)I>T7|3?(XR{1 zdzNmT^Zj1KYFqie`=CvhpU-_X`gRB3{ka{t2FhB8>g!r=#SpeEW~f&U3HlyMd=wt9 zp9rJ4B}{VjYkOe;P`R`6FWt233V=LdV2a0ud#@(Ezv3E?M`~O&wpt$QYzUI2HIyvD zOI(HJ_CvplBfcIHjFIN~2akeNQoln;)f}6o6#}cpbqZKkSumo7!%bq&Rq+SBH<;$6 zw`#GDi`iMo!CF$KUbLp9Q=uA*&jE16RqGef`f|ji$ZP*z#ROQiyXRZRQa4+KkrSlHN)UHzDjL3EDNA(unbS?{Rvy8AwB@ z{odccl_Mn^yi35b%YQ7tVHuM8=K1Gt(Nd)bC&-o8>Vy3uS}ggcNjXnpCD+cqhFMIZ z60A!nUNT&b_|dN!!)o?lW?Ny%OCkHn*516>*3t4s*hq#Va29K*_*Ud+;{yQd<4u-k(DXMQ3nXH87PThFdlp8NzyEA{KSMYhxY}{%k7rLR(g5c@zIPU&aCXK(~2AE;&m`Pi2=S8+@y4_ddu4`bQMN7zl zrX6+x8>e4;U!a;Zbb^VjUoE;7UH^2Oc~+&I@Br1#fj0`2$N#Cb8}!8B%(tD7HxC21 zP<*RacFxUf+(a;9GhD6AORF1aipBgC6763D)EKTq%FTEza%n)ogZ6VY<#eAlSeRD>9iSJ*FCD z1#2F~7eW>B&1Yq5)i+eo7EVN`(mzZ{V!Wz6DolkQjKh6^isa!s|3x*gaM7^uP&VW} zBtos#d(@C_;~dg$R0!Ysm=Kz%yx@N-_fO*%C-GCii3(J;R~rK6ufMg+{aUrmmYYJS zv0x-(lQ;P>`f+5uz|+~Mv^Suq(ON?YT1IjI3#}e+Z@!1>EjPr_lht0jH^V{10;L(aH2@(f{Nb=f4Yfsrl;?BO0kSPS zUKJp)CgSV872qoNcT`YdO*#ZEZx_?Y)eq4RR)E<&NAs&`+5m(wE4qA|yZC1}EfhK+ z@NX)+^q4WShL7AV!n80`9H;TTpKI53fk+G#u5e)FE?J4V1=XY4hbre1qq-=9e^wqu zZaUARfot#8Cr6%&lwt~k{#Mr&;QB0Pz(@|B4Go4C=!K+0(vaZNsFx?Dw_{K|<#0Bg z{QlzI`wBhiBen5p*?7*B7$QR7AI*!jq{t)hfmed=p#l7dl?u21$C?{8q+0+7JDog@ zA{Lf8aha`f$i+SLZ6fTsAjRjP+I`{#=ve23law*|7b(sv86MNe*EtH^)2t zZ3EFw>d%)${9ZJnCnteNMZx=KIxj0M4vMgX#CeC%{UBWxu8x)|>hchImVV;HmC(;oEDlH?@wzTi!S`C=Dj z8H8Lo>8QXTZA@rde%hE^IA?ifXf|(fBx1)N8+oV%h_& zIT^ywP{*|U6_0u4-uGf63jBEsUG>ujMjzI=CGZ;}mVZS2!xZOFgymT{`**{-f>)gW2Mn<>rO4BpCIJ0cJ@z7j-p z;_Bjay`WTCE7-ksnzAQ4QV%XFJs`I^rWrYL6**ujLMWo&?DajSPR_c^lP-Vrf9xSA9m)KNDU#+mi z+cJbVPdJ8s%juowQ5v5d`3$%VgQ zLR6n`V%QQQD*pVnhOcz6Z)Ng8LE}NDKuPkw20q7@C}tmJ8KLv|@DZ?8`Mui`dl5_9 zMD3H}!oE#g*=d&qaSkNm?MxI{w6&?`ri^^=@AmvYXx?-BG-vd^L19hrbS{2f=hAL< z(uR>%A!ARgdtJP5;|a@<$Hn!`7j3ozvuE;|^0-?gLLr24doN~-EEzr&P_~%n;ArLk z5S@FHK({<1^qM@BOPDJmO;<{>=GJaJ-XY@C-OndE>IZ({g;S~%4RKK^3YW0eAm2L6 z7YCa0R;;_}5gZ>HV;M+uPEu*qIxk8{$I}l!?PXO8{Cs9xvNeRo@_Y7dZJ@7WpOJ$Ym6ZfW9{ zx7_LTd|bywH{}P~a@vo#^(1TJ#ntNywa(>bs@J_o6TOYJPv>}k6*PP;Ce5iCCSHKv zuk)(?r6!MbrQ1>I!#u?1`+XBF=kYeH zl##}>{qA-*Z3?r;WJKFsOiFH7oOR&{j)b%2*~TgL&^=Rem47o5LGM^-AS;XUZaZFK zSNu!=y`pnllDnlTGg$|j#PV;+SJ}U1;e&Vlm_LP|S__m>T83JiC{b1i^uDbpj8i7i z#qwP>`pt?tqN@5zfj@Tlq=Ai_o?%~yYFf8Jd7RT#S~^Zo=QX44_Z>fhWwiKWl^B;b zA56afyB(Qg6*fcF0&KbubGYJP7mTIQmuNa@U9TUAqxTQgc&%HPy!At!Ro7wdjQO{@ zjAs0elPLZ}c9NHi7%y*UaUY8`hb#`JrQa)=*%U+r|`xC5ORIjf08e1RRgY{qr5{80CJ*}K(8UlvqIs7Q{pa2ndT8s%@f z*h73C+b5o`mbYHTt3bbMbUUO)K_(Be^K6dt*~g7U@Cpd?r@yXg`c6KKiHR4UCQM+R zn4$d0Grjp+#$Ee1XT;TD8ln$7c)5q9!;20Tv1+UsA_cHw-%SkBp~GGDCnpc7b|gR2 znfBf1OY+Z8Jj==8ps!FQuBJoVATH)u`@Z>LG}gU0Oej|^N1sJN=3rHWO#4*-ZE^{C zfzU>jTTf`s=wV-! z?4j3XpkV9D+%T(|3CaCQ60O{uFL}aDx=YEpjg?#$1e>$oJjh%7Rv%4&;Zx}MfuZe| zqoNAsl`L7lY-L0_f8 z{^Dw`cjdmO*KS=NMaUF;{41O+)768T+GC8DVp-sE9CLbfYje7M!<(gwW#}}JUw`&24=SuZA9DZ}vGwv2wu^HcvL&r%!-MWz;x_Gi{e9I|(AJ?{k* z#>zs(N`>ip@sCqq%im);`naL4UOw=)&QOGTLZj~yu^7F)Wp3PE4&fWD!e{e+`!5r( z4(l*@O0B*(FH2tSvwfRyu7Mq6GM}~Vh>}kPxcZo zVJ}k7z;4Zo^Vch%$!f})2+Whz9R<@84xyN=7`7*|tL7)aUvX-Vawud%>`J2sCl zPr3*vZhiY^fkmhEJqE`5Wv{g6S9F`;(sF_I{+C^!&#ufNRUf0iwT@DC&-K}-qG3d) zh|^n^zX|eTdrwjS&MsRIQX!!UMjqB#Z;Zj`yo|UwaYJ;ZnWJ1HT_fT z@ItEmgUCH}vNvksVL>i+KD$q5IQwX==R-;YViqdj-o9gMPc37dj4%5%SuJKu&|mrs z?Bs!^YM0yYW`Qf^sIY}U{)5S;4E3vLHEBezTNOi(JPPH#e0{sx(uanUn0zj+3z;!^ zx-O1>obGQE(mi83{b3GcHRV)r=(#CgmW?0Cp5$5}C%?S2GGr{>95ge(=NjW8NOe6^ zeD0NUza8@lxs(aRCwuNy)1VQ>BZ?=Yw1-F99`J2JwEV81gC{20Y53hAwAio`q-ksH*tdj_S>?5Zj$zX-}%5E5v-Xak_d}zp>Fs7l~1va?rw=ne^n9 z$!X{f^@GjrL_(7O`7M8j9Ac%{o3$|nZ#$@*k{*%=pRfqX-F=+p9%O}(;bB?zTjb$+ zmX}(P`kB0Q@1E=GS1#uG1%fQEZ=P%ev*9t+^o)mniKgmvYPHqQ{=ZV(@5Fj z#`%TU=tUZcxQ=RBx=xaN>v7$)h{A!;l+Ttqq9OPbgx7e6)78D+;5QZ03Lq?mG}aWt zS;g4nzWJ*ai_(p*V_D;euVG<3@ZQN{yHbfILz`MloOP9P&~#Urq;2j>Z|?0%jSO-b zF7(^?^4$Gb%~{cgMQ>Z&`t&rH((}E)PH;sk?({8#O4F3|`PaCeG%FlC&(5PZ?^WAY zz$Y;%Y>hluM3N(4NAn&xwwMuhcs(b&M@{ZX5ViNEShlUgB=jQF-qf~SYtPPqB)KF% z%YD^OJ-l$4oRxa9+>d!6yzewlPWk|mT&;#HBlFhAM_}hx9-gjE@e5HI`&B0DZ)6!r zP8(*c)e)WWWi-BTAJ?y0v&x7mr=AWoglh@z7n4l8DBax%Om64=g?qzB(0f$F%bfai z)JKbnEoEsw1;rB%7Qtbo>j|f&k{K2ZELux?$ZBKKuAD@;G>OaI1+Aw~>Ye4*UUl)~ zV0h4|(Y^F8yHQaun$uyEe8?*2tn793YfleiB|BA7TWv1ZMT~BizampOj)_0+t9$e; zqT0;7m$CaHc?P*@T(EP{8fUm=Q!`7SAE;(nQh|{1J_jj9p#+4&mZ?{ z%em0TiX`+)aaS)nJfM3<7vlb`l*L4Zl13l5uPP~tS^J$^acZ`;5B5~z8%v@-zdM^6 zXZgeZjvFc(bsTNIigt|pLv`wX_6g6Wx*V~p4h%VCU7oI#_5@44Gz)(*$8ZwaMgqfC z-Fnhs_Cv~?+;w?OWG(e);y4Me>#{S+>g}<+8@5~@uNc%iz7dOzK?-(K>L4+oa2%vXk8HbBs{?B%)ethVYLeS-pZeMSQ*?Ok(e{tibseW~vqQzRdk@ z_@j)(+X3ojZGKc-rg>}v)JY$gmKP64I1Cl744y$sSte<;YwDEAM78FKek#{9kB8a%mnMPNl~+2p?1N8&z= zhWH9uRl1L}ck-TFT4ow}V6GW6OYNu*ww3g+AiOsBITZTZX3e;49-IUZT<4`pW?sOh zeTes5i~54I=)U^TWKZJTkb9#ZU-*w_XHQ17?LZFXZeAM~?`R zzs1R-`jz!-#V<*e4djPdl4F~re07f#ALo;w3tAoW`RHc%UI^14x8rRF5 zJ&#A~ahhbgN?k1;hlIX5d-zHgF3-Z@1A8i+*asy8;bKvIVtQB6ywwM52u|}A}Al3G8k<<)2kji=1W zY&L!G*VQTWqy)xTtN3mHPdrD1@`Fir8I9JXk<}>@T(edQ zCzx_>)t10N1TCyN^Xhz1kdUm(j$#%9KPDJVnlW%#QhhO(W{z=SyQ)$m0a z?_j-{fwxpO{VV3BuRG%nVxi08cI5xKZW zGsZ9C%qTO>`~AA#9*JR}T~*`vQGSWp&T;Ly4y=eg&{S2`!qDg=aZoR&5&_4})@S}1 zo<`k{S-L$X;_Urb*tgjmB+ywi@a>TC_u5~Fa zuC~>yMgfAq*@u}nrfIHD%5^bN`X@OJ3Xl$ek@6aOe`GK#bo3(iAa6jLS7Yk;eG*bm zvJAluT%IMKknER@pQFg;hixj}olp42{W$*?KAdT5^cW}Nog!y>ltrQay0G+3%`?8L z4;+hvjW2T(o0sopS;r}Mey?N8b0m+op|2!Op(r#c^i}pUj+svDE`Gv8uB7~ct22mbyfc7=qbY)J$c`iBF2|XPZBBe8e0Z}k}ow)EM6fl zVsTernS@886JXe%e0TgkOp=2nzZB|tbW}0&R4#yHLLrDKKYcsprk?>{a#`V*66y04 zb)TBW-%$tG2d-q1`L#!3VDRxTrBFLprAwAw7^tS)(85bUAF7I#BV6sSQaVo2wNdnK z$vb#0MOe`nCBS$1z7Ic~;&Oz3Wa03({BQLagICXJKRhc}^*r~ccKTobeIJrsR53hu zRQNx=2%Y!;hZms>^8G)24>~V5H#LW%g@cuwH8qdWf4l{KSMRyLh8!X6nz;h?*(3gP z1m>+k$y+3nPoK`JKDqe)dqpfa|I9&drAy6dz3dwn#yj;-c{v67uIrIL6u=!843`4E z5^p(S3j(q0|KtD8a$~&easBiNS*v?Xl{h)Q;>G9Tc`}S;r1+|HWIf-Q)t$#ft5pe21I)&2qBA;WIk0*o@CAE{k-pM7wYVkDE1dLv5Z6n9B6=d>a0Q*V`GvyM^8(}_!gcR}(C^?@1bB(p)eM^1A3x`B%zqSrQLOPG&(p5&L4ew3nQrECH!;=2 zj&W+6Z_^YcuFE=91j^Lwd+f!9?+;h?D(1FN&YF}9)bzE*-|anb!K$B{=L*o$sQmf9 zds6ux66fv9na8n7@}&cqOC^+qR7- zwrx&qXJXs7C$?=qF(x`wkDboe{_bDzDRpP$g(Tf+erRewVR9G!z2pI)hKO9f}nOQTYA;Yzn2X=cr#^Y=;_v@8PdHVGkjt#Au-6<%X)WtHp% zm_?DTx}@_Hkr#>W%liEegxai91nDomMr)AfOF*|Q3TSZBa}>ax-K zOetUg=S0IZ_~P6(Dis%>2int9nd z#Kd1u##ZZ&j_T84b{E~-=6Rt={~Ru{`e_nc-f-`GC1vv>MmVp6TU zBPW^4)I!j(-2FhxN(GnZw2>VFVW-Vxe!L8RGGVT0u&^&;%Xbo?BqEDh`VzAoVZ%kL zIiv)Q|2li6R4p^7Gy=s`A3~@l7-2QprrdCbjjKdAM7g;J0{uOHUyqZADZ~9Yj>tZ> ze2cWLQ+CPCv>vnQg@qTgY-y92rK9yzhwX1vk{ggN7$k+AVo}3Cm2?;~%`NX>DV>MR zGnkWAVQX@UqP=6i8;o2eXd&Ndhx}z%H_Os!huHCoA$HZ+>3W*#hEk{iu}(8wV&@xX zE2-9Ow$c2D#uSjudJb;w&+$aUy?*XbZx<^uR!T&~#5cDNPg`+t+aq?1cJY@~3Y4)X zw6P|aE4pUj3PJ^l9&cyz9FmxRY)2bvbFOVa;3zzc`S4ES*tBu z!S-$9e0=gLQx%JyjAm&7u4vR`JkPxkOCYz z^z!QdTzT-T;TzIK86KOP;+h)rGWJ&03!$aGvsJ2V< zE1Xo=5jp<_e*$Hxl26*_6a&n>BLNJtCQWJShU^UY7xel0e(oQRBd^^W_WK0fzXCzG zm+R`W*{B}BBDYRtZU^E3bnqJ7n>$}8#w;W3ICjt7*Or91a5XruxgU}Kd|K7ZIfSao ziK)Fz)L`>;%Edgu`(xXZe@ueZ9JWJ^>Bk-sxOBQ6k|AHfYvK?;isHN+n+i`ObTJB% zM#^|N}uZO=6!UaM-fe#mdz&2X=FF0$;?BL*}Cu4N(y%>Og2-6(ZTf_)b zdU{)1dU_>5WAr_ARbraKd_b!SI!u}i&zYwlqS(c`Eg`b!Gglg=HxjpraJ(|HYKVUo z8d@LZel8@WYP!H^0OWc`%>e9BZ8n`gX_^YeO=-3c&u_Sw<LNw>yT+-pI;en0=u^M{OjBcUY)=7PKef*aPXslP4hJ?pa_ zK2f;AX;A;JV=Ff^=-dfJT3o~?9Ah;|P#v;J70vHe%~Yvbksaa${&^}{KSFa7H?MCF0DhkZF`|^_>D@c|(RWK2E`^OG!CY zN`d<$nwU8Pd1xL)3N?YDSPZqH&;7~U(!a3!1OA?pC!haKSt|L=6Rqg`$}?OXO@US2 zOpqdr-WSN1Gfbo_8tkJxrahyCPxUg?fqQ-i;gpn$uB2+Bh{T-p&O4?}mJ?E@FkiKq zis)?#hjT#JSs}bt;BZH0rW#7R){rSb5W<+Xl#U8b7X6j%ia+KZ)c9jZ;&5=Fa^ zNwGAIS41hfGR1i@^#;tl!YXzJ__}-FzrOB$Y4&T-Hy*i|W)~eeN5#OAN(m;4#Y9Ll zkC2$iiN+U7vMQ$DW??CS$AOBsL`dml*cI_2{}KgDQ6KMilG z{$+Gv;@?`Duo&Tu&ely$9z<16`$e0Ch;+godu^Uz#V%2KuHar=j3+)*GJquv>M;5l zk#5L)L_$HYkMHOGQbhvs?_n9Gc}1d|qCHN!HlYDI)^2Erdk9sJI_ptZh*n*Dfm$p} zEF^9|PLe_v4(ZEquM;oS;t`zE6wH*l9Qu9lmV+mNns=~83Of!peVk<=${!?<<6wG~B7!ZD1Q(>lj4*;T-WWr1Y1G9KE{vK!oUr@d zc^@=3Gk-nubS)?61xb+S7xYmFHgNjxkw%oOjq)jM-SNwtK=1-Fjwq@Y~0 za^e252Qw!bBpkGvcu+}HG@_mm!{k7FrkDlRsgg(u$%SkVo4;Z!S;u5iU}mWScM1TN zi!QvYhI`qu39VN|QRHl-`RU~NB{?^&u%7rjWRtj^<8!mzuoNez-BC$9#Z7i0hPtPVJ=IfGz;~eoE)0{EsnUjV$2nRS!~72Wu~)%SnUL>v~lPj z{z9Ce|F2BlAN=_XiG_^(aue4;f<`SP;2h&k{XA@#i%zaNp&w<%rt@IVb^_0$JLO2$Jt*6Ea9w5zbo4~+*}doXQ}dnf zI8yB(AgK?P4PExerb;mOeXzxHBWYGz+Kt&O&l?}ZnD2-d8r1W-O-2Yo>unny2ybqP zi3)P_35W?J6V{mzwHq*@?W~-VJE>0p$%qfsc_sLNGxL%a23J@_%1Np!q7xMzDL5Z}d1gCxRzL3iW!y)TU6mR8lf#;< zC!%klp3@8L?d?ZGLV70N-Wn9e|Eu4Dd z`tcR=X1Sorm8la$YAf#ZigqZEp$Hi$&~FFanq>d`qD3emn^M@sCTC1#T-JU$z5l+s z>AgnPBEqqGbQq`KZ|0qhfQv64dfqo$FYx9HVyuDvbR(yHkf50yIi(FuDx*j~t4dwc zNmfPdic0=QZXi%|{r5J2Kht3p%)I?LX>${FVk{c~hnlH1)(AVRQaz43@FSLc6PijkB-ToC1kfJ$7CNOUrqGVAXDyFVuM1D$ zp@RE2uZ(FRj`L2EKP83tTJSH`&`|6%hp!W#T$9lIPosI&AEv5l)8+Q147_ip9uAGF z<(q%7F>T)=piSN$rV49zctvedFu^*RM3O3mD8}PuPpaSkIz4hsCK95;@90bqoj7ep)IQs&l~rv8D3A&yC@ z5(rFnS|!)1I4h+wE5OCD`@yRL{gSXH2J$<7Qg)0F_vDHj^V`0*n-M+$o_AG&j|Ol$ zybBim9h`JPrTECE>CA#0oBIt39sp11J>(3L2s(pg==adx@Fi?k+BF$(O8}Ca5FmMq zD}3HECah2NS?z#!dzN6NB3XfsdY5~&_x}lM{|5o@QIp&W%k4^*{coKB&i|tm0I;!h zvv46{2H2UnSx8uzIhtGiH&ACKVP#`w{SR*E;pJi>5fXxPb#t*Wv4`^l&GUivQeWA) z^PTH#X3m<)&oaNsled(iqR3+Ooj{aX7o$ilghi8Hhk$?t#iS3WN0aza-^R56(fZSZ z4V2J{wADIhb+@gJskXoMlZ=Ve_H#g$lKtEFcBEFRAYlGsH^=pNbB|T$ZNQxQl0}Zz zPoy^RkZOxdsI`!fjyehD)~i1XVs&-95QRR*$8RyK#}}IDKiL9+n~e0X?rX3hKW?ir zL#AEbc_JEoFu9H{DB>H;SGp7ydtN&>Sz_vY-8RhoFP(p3*WvfLxcGbFUk*`*Txybx zR5YP9zrIat%0H(Vg5>GlyeZ)hDuQ!>rX|s27NX5T?~T#Mk4s%>fnKe@CcI}HaptRF zS00}b-=ABR4P9#OIPg?tc1U&`8HBl7oBLIPk5~0%d4OC4U89-1Tiyq0XCLy$7m6c@ zp5YY4Q17lV{Q_qV!vPHa0>ummMUwORL3HwD?;%nFyElDQMVyM-8;~`3VQhbH%RI$`X%GIX2V8Pmf zF}blREeeafiEhHVfxPVY4$KGcQblG{CJ#&VxLV-Or11E2Qp}`3&!wPKM0TW40DilI zXgkn8mdP7W2GCS8l0>JCt6(K1rP-l*(AS<4r#jqx1$B_fl}cC^p_-JL2~Z{tK`l*D zCxnG?UAUYd6cY`nHA3npdA?(puAnIQJTD@bH~+1wfA)~{CZhI;>yh^;){7T^R~)D^ zlQMRz-$H;YgnO?w@=H4nr}>T7gK^PXdM-{?&tjS4X||iCI%CiKp?DN67l_MACa;iS zgBqDV5L>yiyGc)8R?KQ6U(H~duKXjr_@u&lh$_-TDTXUhZRT7_ABXcV_Tl+ zYB*{@sQ85$SUiDEK9MAyBYxGd1Tw-lQvPE)*?(D!WK5>F*>apRGBh=a`(z}$k~ta^ zy1E#6Ao2@qO_<&myrY9^?->x0KU@a0Mb(QkfLycp!^@(qNoVpIDE`RfpXTzuPU?}{ zJJlhF`UBZapvHXGj!AC_!<+HRjXh1$@?1;9W=(01OJ{dn}A)tnMVRU3D8W^6KC?XQ{CZ=njt{o6a ztV}#ID3X2D1DM+>rgvCJ=iWx){uPb?b`YN5VPd(hOvD{}5RQee@Dl{;BHhJBaW0Vb z)WF4%sS9G*DtMoF(5b?BoeXCcCR}u|L4u_PHXI8)OV*YWo$ge_#gO9I{J_c3ipV-X z1@;UA93Mcob!TQ@C6cS8_JeLARwVC_z-&8)GNsotgfC_$lJNl*DR_R$CaeU7uSg#l zF)#7{ylN1ekV5`M5cpy)BiA1uh*IE@${mXE5b-c*=a+co#9flitMODjmXt6_5Ef!5 zvEL$Dhlsr4(I53fu|>J$A8~W;C!!5D1LbcaZoD# z>4)tZ>OW9&jeNy0B{`S}`$GXOxE}e6EQ$5t2~BixJ25M$PDXB!Fd{&r`+9ko!u3A( z`ouHqvTN~iu&w?nc12brLRNjH zx>`d`eUz?#s+y}t!L~%VezkhVwk%ssjkB`${Hohj+a*(zG8_RS^{gJo$z>zdA#0*9 z3q-lNoCQA_YgLrLtAg^!5TT8RM?e$TcG9w&!8*VEM6G`*Id2yNUpm|*bCR_q;IRLAY>bmQH)?)SHW0Pp7J4FS%6 z2cNC}NvNZ-ptigAgomwSeHSds+l#Xsg&XDzIY60MTV*{tZ;77vvTv$GnXt7ZzifTF zh|#SITb_e`0ds50xxN2xVUdVIoHfld3`L9mUebbcSNo)O%<+b@V4JLU0Iali0yuNY z{gE>);;8%(zR*B>JK{qoDN;KT(gY85#}h9>f2`{TKMx#wZnCo?t^6MsB;2x6h>9rV z)ir*7{a~13%2Ah>9>;F~&YRkQ$K{1`*JIpe=iPPZz46^B^5xsWfb}{Mf3vU47w`P+ zf>3j~rB9P02I9K>NzH~C_Z=ivGe}ji9YCQdi3|3-BhcLTqtR52o-TKijyM@ z%pymgztxBu4gVhbekMCgJmGA>SREnPIK_uhlgcJ>{P>28t28A8I8!`(gs3z9PgA73 zOt=jwCouEx;B#C&l~>jt2<%~> zOy~%fuCLf0Q1(RXfDSNO(NNfi6wypY+XfGMo+f6K`LGS-bJ`ZZm!Z}>Uc2x$6WpH2 z{R@8zC`x7c{_yfENn@2B*cB)AtJyDDb$tS&Fz#kYD|Qz202s$)z~XEhefbci6Q(|5 zI|?){dFzoa%pOr)sFg`-Hiwi*LjJwLS&(4|3{r_>QQ=P}9*M63UFR~xbxAaH4qud= z24X42AS_jd;SICBAU3xv#03QIp*$z#9#J}J^cW|p!Ju6Z7^$eFbs1LPe18xxKYkO` zn@6k;_~+s2JGOlp)yjVoK~pdYf#)B%8Uq$#`?6y)!)h z3Pi@+v`BaEfE}|tirAs~Br3;bvPH=ossg7R3}Dm`_Ws$PoK~xEB=3$j$Obs3Ao~XN zzYiz^$bq4(JaRn6RDX}WbtS(iVz~GyLA(FwT6&7aCb|14j{=AQKFI* zj@)faz1l4|sle)uMOLhoBSK`h?U zzrqNGEJ;stnq%WqNlge>rU{9D59F!!RJ}618$T1|JIEyt9V>@9BGB@HTkab&1-&P# zQSW1V;rE3yN98Oh{g^;vlW?H)oisR_KZ5r`hl;vw=w4USN&bu`*H6_eSugX_;#K0z z?4)-!Y6I_|3w9k>aVRH4)GB0fkjU}^InNBP*yFjmCceX591S+o6T_SEbD`|a)|Y!u zjF{AOqcRX@F1y4vk(?P1zOsxzMRMvt{;KSdZKOw^c(kB5N3N2zu^#Y$Ip~JaJfPFs z?}k5jg2@+k_(Usv7Ee84zr(BpB+c`gk{hB?T)#4j)V(B+nkW7u)0<3~0R1EN2bNQX znz7vXEbme3(eu&2Eof)(#)|xkGkw1aE{`nQF`^i`|K;a_>oM4|;tI!#>WbNgky5I{ zhwVXLtdQ9>1p7`FKAV!2Z*zuK{e)1mKf}Akv$BV}rbc*EjFr(*@6U)uxaC}=Oz;@z zZ|q^H)Jf6~#4q^W@jHCL9!suB)H~w`((fSQLti){k-0C0uYa9rr2a`6KZ@W#s^VBl zW#*JRN_EM*mlJ|VhAzS~$%p2tJraiGj)~lp-LC6>n7g5Mr!x%4%Vsg9zuH zMUNJ-$QIzVVP2YYl9miY2*eoHBqq~=62dxMnrqO=kR_(&xag+W_5(1FY{dJk-C@hSa{3>fo-ylDzZrn z6x#(iI>0^kJ~Ph&&I!O$D{s~Vwj7G47v_JFJ+S_)Bk`&mdJ#$u?{}>+fwz}u5aM^;_DqFROlb<}b<-Y_S zYHmz23F*gfrI>WlCu)cDp*Sp}I#7PTJ1zsa*dcoLMpPb|N&gM0He(XKYmAn%028M+ z73NWp=vOvNT{=1B-K+h3=j0MS`Ou|PjcJ!E15x|g$NA^Njs6N1#pKV0{M>-Q)kV)i zZM4Vc+7U447~)uvKP-Lo8R&`L#E8A?QIdgN6%vehT2VQNxdIUD3~n1~6xDD8vG zz)e0~cK-}Y_R4TYy0%b140kx_Ejq7cK5Wq`#se?nZ=bt1Pyo)sp#mH&;?hC_^)LKp zJBBvleL}h5Lmc0zjlP`xuhQ#HUlJK#Xi5eL;E& z-QM}`8c14@5#^(8rRHC3I|HH$rMa(i7$_NQduJ3Vfn#Xy?48(@&cGo@<%Y!?C6|B? zwW;JDfm+oPnkhz!%zO^SnVDmM-Fi<|m9KWu^DC=G8eBhqyFcv7Py3T`CvACluZEw# zTT94|@kA}`EhSd_3yIIZ{!74ML_~!b*rf=laHyq?YdQbmNp3d`@WG>nH;)!U-uUw7 zagWZFCCBq&B^}8N6$e^*{O6>cQ^c5~lbf(!srzd2j?M~hNmzTr%vEmvVES0NkkWaTtk-$dtM4V@&*P4S9>a4M;;PpisE;p z^b;4sgEe-i_42t^`+1WHs>YRdvt*z6BB?Eu`JxFcJTd{A{!=SGE)>4VucQwhhv0B!u;Fd*#=uuO&Ds$#5S%;_vgpp5x@ zFo0{k*pHQ_@1-^?RXDXF@Ju(7ZeKQM=SLa$hjys{V81-!G!j=6K(Uur`T6WUBE++D+~ z0gKS*$3w!kaQwnW(+1cIHa5lM*n#pk&C`lW(FW^U#3i(yMIe`EdG5}7VWZ5yxM%H|pK9oOt~BnJOZ!V8!+ zdHhsJm)PuLVUo2d5+C&?mr-3l_eMN^(mg}xTSJLrmtkUl-yE`1#5;HBaK*R+x>2lL zOeP?ywfz1L$xL#+W5tc^$`0yR&X{z7L0>0RKhie{{D_-N#B#mEwh2cbPmui8_MJInd7TKR47y3}RtHE|hHksAuWhvr&rdk3-XXdU|fZF%h44FV^(d7D=W`ppif z%Dvl?u}8A#Us}`Zw*rju{uvzIiG_@nu4P6uxz*`nGsU^N6IjRC=`<%G)SaVR8ELL} zkfdw^xn7dizMG8WDSTIwulD0&z-1%FI1J9HY#CK-P6i>PE8shSO5BMl=?52U??s=| z!`!FwC?3!WdD$P43eyQ z{vFwZo%D>ad~1MnYc+BqkTQ46!TU+Dr@_nVS#xS*EviOFeb{fQ3vic1zq~7Jy2EP?~eUlo@3O~8+Qz7^H) zNXk3wp7y!#9PFb)joMr{=Z1DpT_e{2s~)che5BU8w0pqMzS#$@nxYb@ly*TI%GRyS zFWnERAzsW(7yb+T1T72FOF|Q>=0|lBEZT-rO`Q(U!`P+$q2qRdRrdO=cC*VSq_9EY z!Q2<|2MMaqU!o1Pr)PV6ZI#FN$1-1;YxGlYw|Sja^@}Oy+RpY~hOcyr+ujXi>#(8t zuaeZ%NYQc1C)oGnCsK#u z4_*mt7x!K}d`dbKm#n^=S${*XvCwksN1T=R?CDlMXy9*@+U-R-1Jck z8e2a~=Y$FP&g@as1TF7pugZ$TnZvtOoBED(Uo4cWPzg+Tq9}GE;Lj|MsvVMHnfKK- zUM^M-IL5Vh4}~V16gFg|*c_JwCRbZy5DPUmG-taJ$rmX zo?Ba(&u>kO;o*x-Z!=?2tGIxa6^^p4Mq2>c!ApS>fSI-h`yx1bx4qb~CxMF=5Fe@W zX8OZGjN@n&y_hixMFF-}vBvWP?(t!N>_bYS8c)jAwCm6h`t%taLSJpYK^5*Fg&G1# z7WS&zhk=<1@ozXW_R1qQegM0GUd4A&tWFToCE8`f(ly9$^`-h zIOU+9Scfr9CNUVY;{>-R?qhD9Eb(;cQmJxF*b!~5BDE1(LwYL29UU@HPcL@KU(p`{ z0f`XKCjN<`EMl;t2<_-?>FME!J{C68mm8OGSeHX(R3Y^HxI?>;2}0)t^fG$mVDpW# zsVh4uVuv_XN)^TRX|e=T=c&fkETmfEWiIXzWwO;Vn{N^}qCLx~{s^PcPnxTBbB&Er zYDTdIL%@T*i3RXY|M0 zZmMNmoW<0se`={?sm(LisdbcFx?fR(!N_DJ2Tp75t5YjS=+btAZrl5qag6Cs#3gi^ zHDLdlYk6!ewOaPpZ@wGdvK(uY1=i?xoL&U%jE`(1Krr16S<@-zGVoS{{J^^dPS zSamH6L7t@*iKsw?=rArUv7VdxVT2s^2Yd&T|6k2X?5>*-n6M{eXyAdYRZ9NVS@{`4 zqa#_Wo*VbwU^cczW#~rfQ&@4Q_EmtSNPkxCR$M1HejdGP4=*03AxXoAGmpvo+pal# zwoM>!1HCCrVVA6Kg@|0~FqN`6|L?3ZRmsV2W)-e~YUPvl3{ta{?`Uo=z)X3gU$iaJ z@~C2X-7X!cVlZ)xA^ew%{fOgytF9lH-IFQ3uGNhEjlS?0Z>I_Xx(&}|T`Jez68NQ1 zO5WU;5Sx(q)$#p9_?6Q&X_r?GqK&diR?US|2c)V)O&uz-Z{Ub^wd;z0ThIHzon z#KQf{mAPSL%vuav^T2R!D-B@^Hu(T07B4$mYs7ffe4_IpH-G9V3vno+CWE#xD z5zFi~X0R635AgfAtxNw#BuX=)p z45iPzzq~pVVeR`hLQb05ET)5J;02SF7T|!g*+U~&J2slMt5l4b)ldAq)RN3ep7MU; zVVvUZSW}Qs8|AjG=gg^?x<|bbdr|+~DVI78NkeNR&%*lK>>2+83H}<(4Z?dxg>v<+ zT~)UIy|Cg}N;R$VHA9Ym3r+LuAE<2)%g<@u+&y_!&DfS~t^^)JySJyd?TxKFn(b1U zxeYUQ{dTu<(_L7(pHZcr7Sg3uM;^2vYIa46{(0C-0!=>|R~c64n0MxpPOd$LE*7`X zHZP~bHxzFS<9)n7Jfl5srHU5m&M21Yls}YE9Ls`?AKR4}+TPM+i{bX?bp{@S*XUrp z9x4&NA=T0V%WgKACpeh*YtHF(g0#}xl}^jBraiPwk;ky241?;iuz3w6SXiT?BqUep z@$mhqLYPg#X!X#JLTFe7Dmt!8N=?uq(^0HCC4H?0yf?VCuj&yq4bh+xe4$1cEH-o=y;?x9sB&#&>N6V1qFZ*Vk5e?eghJJC|B))ApxA@$W zKh^Ac0|_ZdRIa!IvZk>9+ftJaSwG5S%Ga)U5U!Z!{y#JbO|-A zajM@Ht7UJZDe;Wb$^Slh9iCI6b~hyC1#C{Z3=BWxPP*m-gx$OP1Hq63%v?%0c*22} z>?fZlld4YQ;fVfnt1)ohmzKBiuiCPcaP#PKS4e%=SI#%K62DGIyM?u2wVGt z_O;kxsXuc30yZG&Av3fz+FG}$I99gA6D=fmiMU!jSj(KnUcXmJ zW7G_$KZeNo;1946(!8eir_rW~riEgLlFOY{h#$H~;Z*mrpR;!+;?@1dz{n$+#A_I# zrw`dM;Qflc()8SfW1%C0sp7UwocD@1>0Kgf0Y|GxKoV@(F3EyUoqU*M(cs}G?hEp~ zit%@a2@L|4@GD723gYfE*LL$=3@;k-gynSxjZ*YBg190o6Jtw0+Pp4>I4>UYJ~OAF zgrlllu8O+3a4d-ECwIimY8G#neY{s`p&x9@X>;gJP5Uz4UEwD~>-C6*s+5F`E~l|F zA5|^o;d!q*3x(6n*UvN8by1Mhh~@F0w~VzY)66J;THBg7{G<}Juj>&Qk+9Z#euQ(k zt#5=!g>b*k{Qb}!Ukw-Otncu6-k6E#$0jFyNf{|~@b9x4=&PdAKd=9t=KTy+Fv@z} zANUt5zfZ=4ggtL3G+AAa;UMfqN_s@6U;(28P8L2JEyMs z!Lz`(HOKiiN3N`x=E_-8|8MT&yBv@DnWir}T$yYw@Z!-HIs>QL9|%{SoU&g?J)c8W zVyZx4w48P5{nt-u7Wtf;pF$}x=g7Q`-2~rtrn)}Qy(gSwZKhU7b=Y=s zh8nNqg>3Phoa}E~Wa$i2pv_0y#Bh=l`7in=L9sAt4T;zW>1?d{Wy#&7y7@%Ci8$v_ zv$M+vvYa_q>-^V_&ifWOKes)?nA8gp#tPhFr6XZau-kPC4eAWkp1LNI7#HACsiqtq zGLG8R_(*QL(pcIGr+gDaHOV=R2ksO;b4?aDkTwo+usZoq)xVj<*V0b=_}{7YpDTrJ zm6&uEhO@tdqIJ!#IrLh?uYVaiTaEEO1o5tU!j}8x0nvuP-}5Nv{CS(tu7dhxg=lOs z(rm-pyq*`U9;>0ZQUeg+fmoTWG6`sbIw4r+Hi$334BJf)z3J;R1JAn#<+{2mW;QuG zJf4~qdoDx1p~qTH%Z=vIgnqZkjDby zJ{a1rz%s4WhdGa*mursNLOQn;*OyGXKBGsUzDSkfHA)EoPG8ic`;4*9D5Uwm@Yv$Q0 zkO&5vvb@qk5$F@V1~Pl^x}&TDd>ah9Q?Z}<2|nk1bMKwMT&IN98Vg)bQW_y}`A zG85i_Is}bUdgoSh#*HErq8vOxLjL(U5qN8CRQ$3Be-F-b;eW=&i?w{^Vc8wJsc2tO zML9p5y1xi(hwuDLHZDyuw$80C_>^cUM`keQ5pf+rROuGX$D^8uH3p&{ZH#deWiBPY z&-Ms8y)YZ)6w2K0QToyc@}U0n4%Gb)hH;^fX8Vk}X1>_r_OHmI2p-I?Yaf>Iy zZ&x6k^<4$f*kThwCpfc}8Mrn?fEd%5o=U0aqsnb%BCD`B!%Hh1q}Hvb`;%O4Q5Gba zjDynxq3tOu4xnnYXho6Em0aE$M|=3dVoWZZp$MODY_>@>!z6zmQY0^n7hAUC^xdec z0s}$6c;UyE*CpJ+>?PpEHX$9%shm?S$NLB9XOQ0pZO^i|FWta2(Ia9c$ApneE;@gq z{pTNRilNN3X7NrlkjP?mji`8EjDe%6p;gWt+F}{)T$J`$7~~ogI@#&a2!0Cum+dpEj^8@Y*y6=**(!G%O}V5c>(%7{7 z?jbG!cyB23O^}AGY-m#rW~ts3%LpPu2mbl?&dRDsX=asaBJ0ROS)m-JxT^6gocqwP zKSmD!f)o(Kj;^zQ$m3p7o3&}*EzwB$98xD@LxT#%KgayNEw)7!;)5k7+FujDjexOM z*@H$^ascC&j^XaKqfm!kxsutcE5fhkvClRF_qCVdSvD$vayvtZ4h~J1;`WlA{Y9R88j(*j>VZ86I@UIg` zjM4a|`cpq*y=ywf28GXy!dt)ENfR9X#D**SBwvZy@D)#J@Jt&gLW+#5iGPr`Z(7jgEWZE zGl%J@B||_q2RIw~HPW>e|0Z%HiYUG%`6hGQ#=IRvJ8ZPQ5MOSTIlYiea{DRUHE%8Rnrt0Fd;xKhk-V{1Uj7zGTtF`6#Jt`$7&GVvl z5N=Sux+~DGo`w<>e29Ve7job6O^??ch@45s>2$^%ZX>w+bjAhpd>5Sbbi$7Sf z`4a(3=EVhqbK>^v@vCx;5pIwCzC{p2iJ0DB(a3iqlj{2Y-tv#%0x^I-K~yyn;N*(K z8&A@?s7G(of=pw-c{njo*dj zUWnmC&>O@gfe$2bLut93zh_=hACN&Yh%c5FL7#}uD5mYea%m^mJ4dY4(SA#PgXx(2 zJkx!|Zh`zHV3ev4>3RcEECHz&7Z!^vTbZEn#$GARS-4rOPmG-`q+aMQGs=jz#(QJ( z!RFp0@{wK)WfIXUt^wArhnsaE8O4d+A!rq117T(Bd4iWoaIJ!$fDxJ{!z6k%6r#;Q z(GT`6#GB;^j{^oF6+RGC9vDG-W%#?sAB~vr_tRw&DDYa`x)4e}^j*-`;*Q1KtTDb-6oNE=4e!xQCGq!Q+l%tftj^}O z|A{kcDVsgAkDe@cEn1&zoEgBs*g((uLbh~a|GuGh!^ji)26g?z^t`w6YWP8YqAFv1 zuz3)ODNLgy7YN|o>{uV>yHo_@5v4$C`#ttF<_TKgQ!sh_5KM(5}y&Thq=Du`LPp?wIA-h5`Ex* zP`abjMBOgEEbNxRKO*DKlxDgo=OrVTx|++yykHR1xDRe+8DAkRD4@^m%NCB#il8G8 z3d9$lE==Fq1&SLD_hjzbgLe?kk817J+_!pR|Bl%$DOb6yL|g{oEfHl`3_mg=OkY`j z#r!QhlAWA0tz0*%#-J=kt1KeQ??$epw8q>%mhJEu5+IOZ25AcbQGcgZe!xEa(SZ-? zrD+2Hyw7s;^9PY}FEaC(+&x|oqHy@jq0xcxKiw|Hzf}gyJIiLv2~`Q_3XcYlA&)el z=v@=36m&9hbxbnCJ+}dqMx_s>{N}Yy7EKjL!r74uA5I6K#a-64;W2`v_|fwjZ?YL_ zOw5A1KGl!%=f>v{I+cYCy9#zj1A@>18_Hj~OdQ~h;ivs5_F&lr0MEkPfhUZfVH&oMvt@BCi_K1i7M zGVWnNpuS+gAOo-iLDn)}9bf6Eq_Zk1G~`;<)T(rr^=)}ulQ&_;PW+ccmQ$8pmz^&- z2mZ`g8U5QMl-xwjJS(gKfHUqnlG6*TPRHd%qg{Lp@De~vh(ktqk?fI z%VJ)#c!33cvA08xD&%_w3kLyylyQ}CRG=KcrsLJd zU0I;ae}@W)CLV3x!`VpEG6_g;(~o-4?%8614tQ-0Npq;fS*Y&##mE zd_~rdN80nPtZzWR9uvQNCO!~WCN7o(8aYR07kaAvxI?~2Zus}j)a-)8tlB-WxZ?nQ zq>e&Noz>NrQZ1}WsPuoNV?hfb^A7vsTv8yGLrUAqR0>gwH)B(M9x;f;mxy zBX|>fL~d|46y15m+GCdj)`(ndfje@mOpDI_8gvy6sv9xZ8hip#9xljgwWh}d`{;*R zbQ3P^Nv4hldsEu;eK2ACg1ZgX;EtPO^{<3BKX;!{`VRhLZ&tC+TchXT=HyyFb#-su zkmu7dNc;6mdc+JU&4Yn(7eGAjBj}K^WYVs~Xqf^mT(jY{O;aV!kfB6C)Ko1FDPWtx z6bpf*8d1v86#es|N~7&#IA|OcQcSz+ptdL7U#9$2n-12Z|D?ni<|4QM&*bqGPgz_t z_;RmvNbg#JI~;rFJmk^-+D}*u2YYG9^bLu%HmGGDny>@uJ=>%1Cv<20Q0nw5f+s?o z7R4QDT2q{(Uezz@LUCD43XMd%_1CzFhFaJd*s0YdVF<1+D8t6Rqb*UX!y#oI4zS2R)8y-??>g2wGNh=Ih;o!-b>HrT2Gt|Harj1!)qj zX?WW9v~BBe+qP}nwr$(C?Vh%6+n#oxITw4e5qq(hl@XO0QB_fyAKvGk);74!?tOgk z^fnIu_Qe2sR~(=W8dBO@X#*~;=Yc{H!^`WId@E6Xz%4em$5oTRtNF~_cM0-GIJF0w z-oC(g;0!S;L5Ob(!oIma^a(LV-|hR?Ry-CRdTNX70-1fd$$}ao&pJHb z=otSt(Yrm83fE^}YPDK&6zE5Am!yioikdiAh2UmFkq+|)Gzy3LKqnc3#WFeuf!CzS zlXcd7pr1?=7a9o3o zMW${g5fnZo$@-S>I19JHsx{q8TbPPN|NGZ6*-n53{sCE_RNAfA>rgt zOL!cso zUI)>5f4o^nazrpRJVS7Szz+pPKe6`IRsw3HG9Smj#58bYOFZ&7Q(v^c;N5<`q1_q2 ze!f}1DWCBQ5?&~JDCM_>@5+DG%b|aR31%UFDtmxOw4cTAJWbz|53rv{f6AjefSN`n zkMwU+Z3(x9p}jCfw%1QzSqo*4WV}4PXh*2UFW$P*qJGD?K^q5; zt?oaAAD^cG7@61}QS&e}y!N^ffhPoZ0Lstsm-cN*c?9!+6zMml+op7oXhvm|0kp<} zq6sq^Bj-z}PZr;S{fSrdN&S!Fb4Vgm^G48^eN}-VFdyEcAqja*3vSU_Zsd*e4Y>XL5%;Jy=%g%ItJ3EVucP&ZA`R#Wqzpmy|M#> zanDB^zK1*1zek-oGE{d7lFqQPjN$xg?L%QFGG7#iu}f2$Ba|>07HuCf8bUni2Pxh%uHnMh30kO9B#VN8*{XqI zD^NYXpi`hY&S6keCh0R={H`XdSgTztiKhs?;sDFnC&?w<>u^>j3?9TxM6*t~ia!WG z!2JUeIa^SoKSl|fQy{?@%7$-(3Y`_Gf;XDgAR#O*#0|fMuqf6oa4$oE4H_lLP??Du zr~#Lt0+JxJNfTRbk4c=+ctKEK>wAvj#a+GQ*o1^;wYf$xTw#Q{1y(19SrlmJ_he#1|IjGqL=Bok-? z17q!X90b(CpGoDU>0m)_d0|L%K~eKRmVf-zWIok5FD2EzKk<8JtlKgbt3X?cFh4;~ z%i+L-k~j8bY)h1?05krS7{i>8PuFJOPAHdqGX5WF{`e$7f03#Y7od}P1TjxF+(KMK z-9+&q!iE&mXmEfIN0dZ61sEgjItFRdsNLxRp(TNFRm1+CG;zTm!^21O;exedyFy4l z!BKMwYDTf>0r0<_A@0OE2mqDwn@0+ydrxy!#sMk!kY`+>M<&^0<4?e{zu<90)>A;n zC0jTt{YTd^6cy!v-nJh#Z43<&2!+ zYtso0J$HF#0d6W}`vWGn3uNkRGT&Qlp$FPb$@n*xpfr_rSiW&Z*!9aUi|~r&GzbjH4tjE+>np z!o5eV?%lSmLgs;(rVSxF(izyefHfDRMu)L-Oyen00OiC;2907EgX9ui1-bL+HbE%i z>4QnuEtf9lBZ<@Gv>7Yh{&U#MMT(V?J-^eqZ-M1evhnG?adu*kQGwhImv0O0&6Ts!pNLIqDAJTShr*t2-E3VJ=g=#14d?{evq|nv z*Kd-X!C|V(Tj~F;=l;d-zz+-t5*I`+zgCzoAzZxmxGRwpwk6kD+2m4fKX^N{mNV=^3sCGNhTDhB8LG z4h)*eF`6$vZp&iy@C{9KI9v@FFjB#SD*92M^uAm*5ShCcHv6S|fuUVN*v28@DNRaB z-)*6GuDuvugRdlR%CExZC{+$H1h$dqppRb4>vq^g90iUl_l%Z&cNL}`cBKSTmg{CQ z|H&$n$P1M{iI7s}p&!84xA2VRfqo*&m36D&)bnI0|JD~slE^hx11Z#()I8m-m=BvS zKaQSX@3}R^j1cju;6mIO6-kH12&r0=CX!UvZ&aIrJ72TGhEIJk8q+!v=tB`f0JA^g z*b`LglRMrIBN@#=FKLj)0MsCSm%c1AT8z|bjB$}l_z`f%DIQ1!QGWu#!?ETF7G*1c zIW$}R`cT{hN&xgb&g?USxs!VHSvQs{v5@@vFj3F1=JP!bJ;?Av_cQY;H%)Oo5L)ic ztwbQ|D5CUW- zkU6Dj*-cQf`bvmST{QZh;nmrPyJ?48^ErKGbr!GKhjCZ5|2wVrx`X)`G^+_o?05M>?&i*`!^Hru1PLr%M zl}M{zqNJNId3tFDm>5ZEWFymg!m=u)#*Grp63ha;SoCM~Z%wme?TY#!?RPAf0KX>f zsvftM=xJx#?M8Q{LoDfCCWFm#?#r-<(1+GJE(#2q~>-Pk+di~D{gSLS@Pgd|4*d<4^!Xfa#pUCRFcT@vpG6;j4zIH8pLRJaN# zKgv2!LJ$&FpuF%QL$tw3J$ZgISj3&%c^1&O=AD<*07H!+`7}1J0UV-;;$n41iAJG; zGnT55MK+`3yX2i@L8S(#RHtyKzyz$sUrCq+lm)k9qDbHd-$`-h8n* zT5B80cVgXSwS(1-=F>x)`0o14$J^8fUmzI_J@rAE!q2a$`$!q+SzquvP0(m{$bpjO z7Go3=#f@f-m?`m+LaZs%?a8vsn#Y>UcFDI@#nY)8sidhM7AIM@-`=HUo9EZnd1@X` zFLPCuCm zO7kOAM%Et-8abEujRsCzs? zWR=%}&ZLilrzGkl=m?l`R5W6%#cO$E*Cti}fEV+I6{N@@lJsQYZ9hIRLEn7rnTr^t=Fr@G>IYq%Er00+*F+= z{f(Gatr0(m(`}{LY-MEz-$`9NSDJcqZZ#DWJN2F|ky2-Hrg;TK$>Xk5JtbYf zBCNc7EHZGz#4wgLZF=?}5V|+}>ruXLDt{Y4E5*oKLFsF(V%TV};pG}C1!_eNY;b?& z>IRuXp<{ZPJQl@?oVP5rM8fo34r!rk^{9o^LQi_lg%DPfOTGH6fC3p&p~;cNk83X~ zbPy!gRGyD`gVll#Dc6|lWUn6|?5zpO1lE)@t0^l#EJs%p6*O0Aw;;`^XcVv^Wcj^CYMYRHtyP=j z7X}BL#SKW>aN@hq@d5R`XVZN2I6(H{sO5{&=BHA~8>?)C(yulbemb`H=Sk!)oLmRh z(ICEsAdD1ok{Z^DIp_8Ue(8;wqwyt9t&0=Ad~B)9lEKO$O*`F0Z($Q*ioIlCF}aRc ze{3-K@nKG-x6L)rs$9-`M&)T`QEFNq*z@*2Fl<|{0md2Fx9y)-RL{V}K`kAEAP{Bm z>4%=NroV9U8~NTIq2N$Sk(8fSWu%e|GZ7fpIX(~d0mJezUomIIjzPq0muyj-=^T63yG-;nvr>SSN;-;NaTMwEm|cfYHsik zG?mV16b}^A5W}8B2={5@M9pZWL~e;m5Z(Fq_=S@fA)R>lN;;vjw9}v(*wE76IHfo9 zn{(VIu0OBKXAW`dQ5Z@ES~lkIRx@E2KCWjq7DV&aC+xJX>zn-x$&mEkg&i5k2J!w$ z{{{jY8ft<6QlnL-nbR@QS7MJLEYaJhv$@4s)VSyvR(V!EPYk2Nah>Hvoy>3CF5^z&PT^PkwHoMs7~zUt#Gb^-!sx0umAdM1@t{Aq&E`nmmUonV z^}>6a+)QNBzEy3|XT7M)Vhs#vyIVT$gz84j?kWWbwb>T10op1dX8KM|+M7g*ri_+S zDp)ZzD@_{l*qLlx+1JcdIIwCIPads(k~(^Oq2TqNq>Z?`rgCz02vts-EuNn}Oq$VC z-IRRP%69m!h`HAd;8lU-(3WELmFG+$iuxiH=NYM-oQ3f4rw%_cL|{@q17=rX=-f<& zcxvq(h1;BxX@ z6mw#C%4>F3ETUd~hh}Fl)PmJJ<$$F4czE+6JotF)p{j2mUq3YZI2_tp@(%Bj@9H9A zSsPCkO<&6Ty+ip51?4Vgf6bU74EM1z$d|GOfwOyqfF7xxmV#6h)|E{n=>Yvw0yl1h zbmTG74-zCo{7eECD0Z4>3$vJ)(nEu9Y!u+3Bn!9Yf4a9j8s_-Qf`tCg-pV&t%w&rw zWeZPW@ZIl(!AheUJ5j1>t%%#^_y^JGxj*n1tW@T7q|7t=2t>?_rZcA0ELr)JIHWdp z(eBo_S@{9|(0B z_$`&xM5KfSB{wrA)v*M%qe3kSlNhTS<0@C!W@KIE+P)9q)BT9ug}YcY7+pkn z@GY>cvh<3_q-BJ&ueGeJ59G>p+j3%id5Us!3v%!qs3@wJJ1iAsknw2+xq~u_84BtN zXtr=fo{9_=@{b@3;j1=iD0Yh%iC1GFMUjI&C8;p5Tr#yt{46p9^n*+=!OpN@n>tN8 zf5{t70E)1N#3%g(G&`_t4yO+!?gzSEzP;oOb`{)1+C} zs`$ik{`qhxonxYEq84^*S>=l zRE;LMN;5fe(Z^1)y<`9-nl_N-#?8WpFbq@=+u8cba4yfr#zHphI*mL4$rJlQ2;Eg^ zgZYGD&~hO`Ewq3pV5l9MK&K)VaeglYv!9RJMi(}V43sc9hrcd<&?BG*j3mH1!0ces zH5tHRH03{qOrpx4lXw7Pp3XA&i&ETKZ-g$}c7vZJn6mBXwt<<=Z_(-Ave!uv2)CUO zVu;)vKz(=nT@Gp0y-|KaO81}hCj8RAvvgx z@V>}fQJ`#CgCpR+bDO|CnmEX8-MtoT4<)kS@|F2%GZ8r!YmW2eu|ZH|AeD7ffdQU| zB%0s`NRViB)F^9LG?-|6Yv-cty6aFG!mio15HTdaYkwY%jEPS=lvgCQ>kmq!=ixBZ z$R9IMxCt%FrX_lnr|^Mq)5{^kZI@qD^45~x?;`-qBji1&S>czT-g1x*p}9J+W@wY9ydJc;URN8FbT^P+=(4&~FRaX#U z0WO>8&pf7SYG}C(GP8P(A2G*Gr#{al^UsD15;6YQ6z^Sgf)sM@E7AKrfxED|0yI(m z3Esnv*|Sp)_MVEE_YUt=A^GVw`KY6mK)rVbVuj=!{n7FklMaHVj6+ZnG~}QJuY~@d zGf=j2;0_oLUhQceZtb(}$mC#6*Qvw~GpWceA_tQ+Km0#vE$w#ds_QCne+tWa@^m6{ zEe*Yf9wJ6%1m3eLNF-e_YMsy)H0VgsbizMw+1ssTH_#t8sjYcK6USA+bC@=pm``FP zIzHkYIGbh_#bzt0IUPP9UhL~z3I-=Liyb>u)nDYTW=UJE%A;1)u$Dc+rhF}Mn6Kju zUaIE)@eV^8rV(azJ0j=TRnT;JX(Gzqx4)Qi-uyKdESYN_6WcLo{AmJ)p7)5*l2hu6 zs4_?pQ#eNMkO+-Dwr!O>fWc8&qIE})k_W7#?J$Rcp zIj-EdWkXp_8eI#Z184Krt@sSeuGG}J5|i2!lklyGGt;fI9UP&)<$zUAO1LGSI16dH@$OXn56 zwj)ez(ppzcs%wEU0_UWYW>1&fMdzEW_OjQOLR%nPpEX&v)^!goOIyBS^(P8_oycvw zZqwbR;eGpMmyJ?(iF5lT1t+@cv&T~{_L9P$u)Q>XdTXGW$m&XQQoKdID+`a>LuXRb z5ZoTk@=}-V2x2x6g@xVXRYTFjWI}jtt06hIajSF={b9KMeK_!;1-F7;13`w00T^Zj z7_FMPs;md~7ZtL&-h5Iidq$zpIGhxVa8_|cj0Bhzm6k;+<>Mx@>bnh!`uTw=Ly5RN zFIuXx=IR&K!<7xlmkW~Yzop&yLbuU*)_1p=J;MFhNR8v*uaMJS*hRt6<5{d<0ujM~ za>eHpL&4WVDf5U4V%3PO7XjaTnlaq3SC*8IQ(BgsR^vD76x zM@C0)!9cNm=%7nS_cED;7M65DeN_V>C)fFM*aYsV9GiU(yFXy(*NtOZ1yVLA99fT} zI-@wsWB>A9G}KXrL_HUPY#`A@rb${#x=Y$i8W}|$Igk_A87ie6Fm}xvX?x#&hK3(< zLc(R)nruMyFDM!i1E|mqw4i4<#fpYZnl=u|q@<;}?dj8hV?X+1JkbHoY=(mjY!KhY zo7)WH(KKM`g9$(!paN4W?bJe8IO3TO*rszk3bCU#!2cQmEf5a(m&0Ab5U}$2yKUQ0 z<%qVoG7KmWlLs4s0lER=Gfa{t%5`6#%`EnJQcpA1vr$|VHyQT%pmkF~QPGm>d$(3E z0tkC`H+fnv?gxwRw%iVxqzE1W&*V1`mp1A22+A;EYT&r4dtbua*fuO=k-=|`VZ&W{ zZnnP52mW1^`Kf$8mkYV5g>Y~Uf{}fN)ugY9ArE5VilX1?&$6!R`xx*#fY$=1u)v_8 z4DLnK%pGDUI%`4_fD_2NE_**Kn)2c$yQp>h7aX|wvUmS8Sc_3LO&e!G|0QcS07zA8 z7%6U87Y`CMn04x`&rv@M?g-daJ#wg(e_`?(mvwL*;5F_W`fk-RbQ4`OfOiRnL{QFZ znl8{NAT3A}l0E~=B#**y;75*_fK56h-HlHD_Bf4ahh zE89WG9R!`fcaYzfJs;N~X#9P+O%M_OGH36(1ynAR=p<0c6s}wdq=dKzqFoq5mZ?Tm z_gkm85KJ^;i#fojdfX)5s%_nl;e)v{fEgdR+9#QI0X#)n(){2Wrt zHER$FEfKZS`X-vm71@aE0RciUE=({VXJU&kZOA}Pftff#5*A)<>)cdDLZvBE6$mX( zymk?@-XOXbb?H@TwEm0J%p1e!45!EzJ-ODv&0bYs!XG+%emr3<%(i`!qFw=be+~~Q zu(pxx5Y2m?s%Z zB8U`Z_9Mw*5_A0rby!$P?!9`LRDy>7{VO!Yoe#0Sbi`~m61PhT8C==X{(Fr6d$tAn zVFqgpkfIT80u5m&R&y2&8+~6_?ygdW@g+@akooKxuYu0p6>zP{Nt8zaShr#x)N~sL zNz6|IYin#j=1x`%7K+#WkQla7Ce%kekLp(oTo-pk@SUHw58tERJ z4!fq?;zOA{;eje6Cyz`A_8eszG&|%>tl;ojIgT7_VFocGK_%GWGQv$x?qe5F_buA` z)NaKHGyCJ0YB&-YNQ3q~RsJ#?7U5K!k7f8I(3bKM;>O~Uw zMj06;j+p%Q5{_~M<{1mh(IrM}vhr*w$A=;yb-DV%@YDySt0CBE6z)O8MY(Kb;vz82s84^qCGk^vAaBzA^1v|23%mdDV z>okUWKVyyV?hS+JmwReKS2^w)CkPsS@iiZr$J5Mgtf(&5DK`2VxaBvR8U$L~DG_hi zS1j_qrAnFcTh2_9*7HScT~9GGl=h#{N|>h!Nj>pP`Lpee?LTE^4XqRBBa^0NnM3TF zpN~_?qgYR*>{-0dr8jjOsaRC}3|#8jg1eyUHA7?u`$5a?8@aPoII?0(^JngfoYJ^% zc(KtXx@CH1JNDiCOU|9|I#xQ&AiLb8q`B>@8rRGWM;~SG!`F!)m86Q8_1Y<#wnmLZ zE_GQ6H1+_zmy3QgH^K56R!Ax%Q8RtGWWvH7L*nXt_)T4GmQ7Y;>NWEjRWOt08YaEf zH%D6A+8mXx;?!E)A`gKfZ*`gGT+6~|g-s>&O7B zBeD)*vgKu0c-0k)OHEpqy`_n|?k@A(m$~RmGbhg=Nh`^rpY8MCraccL3!pDV@ z%T>DDn-J3<1U^oVKmV6#kL~{}+5<2#vatRS9h-oWm4g-Vzj)X^-Co|wf1V+@nPzwD zW^G@5y3u?-r6>TOP1z2{pQ zEe%P8gb9|Z>9uS^v5S?(Zi%XMkxq8Cjw$l_0%UIc8G-+J-^>;KuWy5@d#b; z>oj`2RVd|Tfn;@p0u^&lfDWzbx?dX=c~zm@uixRQ$uAE%kGSZyebG2;(Xt15L5H^I9PXxJ7+q0qeCsap-%?EZ+Frb>o!bxF##q(y3xC1hEaw6f0)LIMUwgMA?J ziOd;1vBwFo1k?HDDLG=>;0(wIb%Tu{v3P?EYgaZ-)FR4}Qryr5D5uZ@`QKHtAHYak zE1mNrV`=!?wMs~-QVPUZq{8OH8Mc=Hh*80sRsL457GI`7717H-kx1#kAvqQXCZp~A zl!T*fZIdG_WlcsNI=7ylXAO#sM?g%EET^cT;9=2LP7fA>)CJ{vG^f{5v24Zcxk^8yMuh*U%OER(y@kx_&9V|HnTVDHIX35uH)x z0icNU6SNONxBIRWsgm)yF>m11lZ1`R&CF#Dy8|D2EWEQqCQiH~??>Ew^jCKubQ**i zu8w}}8d7aAh5Xuvc=`ygAqlzo2$__C+Y=g88LrFkQEg6s_oz`%sI_2%M1WSKqR0vn zNQAiX+z&L2`RyzCO83v0=N9fV#@qWARm5yMX^?9WY#6Beu-{Q?g*+rm96q3Le5`&) zyQylLBzliUj2{iksQAd1G&MvIeID`sBbPx~yWC4-O*Y#f0Y(iyXWBEy)rTys$T9NlQFp@I$;O&cYL4 z>5;ee#3%Z|heneqn|~T=?15oX`ESj9b{MnpbdZhOVByYM9b4s~BrUeFu3 zxrn02Qr&K!xU12eUTLoO2zKYa_>M}$zn{OFIGdP_o6uXvBSmKjkI!$9O{52^xT}yE z-Td5<7eWqNXmA;tw3Yfd#V3Y_Wc47+#1P65nbtJx)Hjha8^KP5J;pu=DC!*AJ}z2< z2_&%R4Q8vlRka+XR7#i`I&I5B(jZOuJ57jYQ{+rJ;3OjnDy8KNVfhoI+mKX4ybCKu zq=fRMI7AN8slgWw@-eE4B-+OS>Dn=K3g6bevcgv|y>8CXS0OSbGRj1kl(DFszNRYH zASfoTSV>^&w+mVcFVlFUpy%3tZNi3dfrQ7V=CE-2=+m;1<;4Jb%Y^X~Y?N#Dw~AoQ zgD6Weenk%?7%1vVJNOw(`XQF1G-U&{(tW4WIB1}MM~)<~uzxygX}oH14x}6*j*vWc z$(J}|vRYA??{F6)hUOF@Jy9ys`@fIzg(#ZHJ3OD19!BFS3Cxw{fG`+%P~qRimB@$X z!Z|vSNsQL)$m=6F$ysNtjWM=Ho541fHWxMrtXZIJ}grFUq+!pKNJ44g;eLTy;@22d|=E?jbpdQ=)F3o&5aR%8IbyG|cc4|(c z%da#U1UJa4SCW?ylr{gODgTVkh7}eq*rLGtf&n&I462>yh zjBmNblj%DxIvsDCW?CmoY~i$_T2rmyv$isz3!(6rh>m~(5bU0oh>WBJhY4`fF4zmL^R241-8&@nsW z-ab2l{fj{sVKnZK1|ct&S&@y*73AH27r0Zv8%W|t71Ewof@F+-Q31PE*>f1d7wndj zxEA@$Gx?&}EMJ-k_exg&H@jSeT2zClRaVxuK-47SZjAkJ0RoQU8c6Kq?BMi9o^TV_ zReKLoR&Q4|Mkq`uMA`s(DA3;wSI^5dR!xEe9oYL6f&A{Wg678^5elwm4M~SP8rizQ zGA&}8SPfAR1zQp?gbabWX$jl;?P$&N|0Rh)d_)N)A$pDS^;GK-=^=q$UpKC zx2gwrRQKzW)+sTJk9|fbW=1CvMklNwSwaWyXF{aDViBw3sqJsq?Qb3JZ@ukrJMC{! z?`RRgltdqeCwyYle&Lz;QJMI$nRYRmvoV?IqSKFs-4)QtWTt3Gj68A?AdOtsyr`@{ zjMm>CYoCH^-g0YO*sN?J0kS_~M**+2q`f*3mw1y|CEXn9Ys;>3ZvKs1V1vi`sUhHlvYeQQ48{&j0TQ2EtsK2=MlW~!a|&{4xQ&V8{$N{l_Y z%Ol8hsEenBWpt(3B}kmQ(xbn4Y2iVEcq!q7fAP}69|7?m?1??$8~YbvzW+Vwcbnt( zNmITvJ=c>+cUYy`e58 zJ-VTUR|fJfW4EX7Z;JvM1-hY9tv=n}mY_;@y&;eQv!!$WP~cI&I)SsHaXtOT`n&7$ z7ac|GC$PRv!!f(xlXqvHq01srqN-?TH+#J3bK^MrIWel_b4RhPg@4qS7pAtw|=k8sDy0Y7+0s3DlR%w-hnF3@qQtZV$i<0CQF zD1*CDgW)?8fyAGH-~8q5b6|U?a)whTNj)19Q2+`p#_5J(Lr$8nbgXfIfH8=~LKnQG zk@?@nR3uMTg?C}R`T4@9S!n;D1+Q)D;y#x+{>zjdFg89%c~0mhHm&&Nm<8y3t{W0J3Kh7yn&W=^giub|HTeyiYM*1NEs0+P7B_a4^_v#9s<%You+&ymkxz1vo z#M`;UB)G}FKu4Dz_~vJwPpan^N3Q3{G{aF{cFC+|K?@_1kHs#i-NbFptUjU5;Pj5X zxf(q7z0KVJ-nL#@^lSGMeKC_qBgHhX{EoS6lp>?m9;^kpsDGrYZUB<>P-pc(k&26) zJ(hshImUNLbNja#EEglL;2MdVMc$HbiB5q|vCU|%Qmu?R@kabgLDQvhj)>U;rZZ=b zXxRcI?mW^7Mg}{XS-ZsP97Splc<3)u1`))l=+SUQ6j2x05!C|e^vRRlb=~sWDcpJ4 zYz6bNDcUp78I8ugg}Ud3j*$5jXlCoHVG{DVm~L=&X)oc#YX*CnrJ!4Ex!?i&}%ru9>}KXBjj4 zxl&5=;t-u*%nQ>%b88*NAw>nBTlD$!Ulcc6yMf`vwD}(#$mym5fz8SmQLf%d673PL z6ymspGSCZdlDMUe@Xi!Y{;f-w5}M}>HM({A9_$<6#>m@n1Q$AuePmFLAgBjejCjtk|`CJL^eXJDcnks+qd|Iv=+DljJJD*g>5i(Nsiq-som2gw%|d(Nm(9zpL0Y(mW<~xlKd|C@V8_IZ zuICNKo1QeCC519{>F!Y~RRJ;hN1Sj3B3=B^KXCSvo~%1&T#ig>kTi=4SL(&h&CO%Q z^}S#W!D*Xou`9zE(69VXbcSMAL-9c;vd(6y-O_jc?ete{d$7Fl7(X=BPkR1AtY9Bs zlpUOwk0iPk$ChJ-DB2bOh8KK`4WZBDO2&ZMi`|QcdPy$U-)&inacQ#xw!YfdqgcEHVm-J;fbY?m>*nT()8=c?X!ZB>3UzBr0Jafuckb$uE@8 z1p(h3H8lUOUN2Jf9NN zx>#wv+c|6_?)x7|IJTM~t2{Mz9(w;GB1}B!J^Xt6 z289M?_s`~1Qi?Hl>avM*vdEIy627dq?3H^Tw`gaOciLw#x6F6wis_ulV8~$&${mEh z+;lwMlQZob)f*wjUnTFn0#&SnruhfCv#h7ML6Jedxrd-5(6{u@DAl=^C#sc+TGiEP zn>_bqlKW7=M!L)l+2#0Oi^Pd19PSTVSIRDEvPXe^IF*o#poq|jvi*?&3)xySgu@ba`U_M8jWQ`c|)dZ_Cmm|gpz#GaNHiv+ZJ0Of)%L1VXtn`8D%4FYq zU)!KfQuSJur|`YvB6*|q`~0^ z=^djo&>%#Vjo``P%@949yz9o@bXvmr*jcSpHgyGTT|fZB%UF zm6x(v_LcbveuTOTs>MDp#mtwH9sh=CTX0h5XpNGsN9z6?xBh2vcAf2OxIf_IdNjo{i#0h-vr{L zt}7_u!`$l9XN+yC2a2~~?;Jej!6$;1ZC1zhZn0P&UR@R2>YNr!ZtJL0C?@A{x@`B{ z60&db+8^Sc*ls%Sz!pEW966^Wpa(S5uQp~m(&hFEIbX1GbRw^M%OA^l{`>g}PvQyUC9wjz%7dkYwVSNJh1Ix$*fe0N+$;r+9|1id*5 z!V5L#K61B&oFj(;3FN^q>u?!J`trdeY(OUI0VhA75d?BWJsYIt*|9Q5a8Ao3{hx=6ooLqUZGePz+D+WjUHdwj;$+1pTj+% zi^oElXs0-1T@PQ?ko+8VN8^h(sQzBjPaus513 z_laOu;Nnm5z|DdyL@fkC1^f8rPt?&$>iP6yCKmR^u4WkiE?%a+R>0)-_^)T~AQ+T*Y2%M%+kni{$B zwPFL0^(fBhgB6$$C)q<7{A@WYdxI%Koc8z_j*&}0G09Meri(eWFmED5VEWFYL&VM& zLZsq*OPE%$>9XY)wk18&5?P6HBlZI9)68Vd+W9|*bV4w-R<6!2Zl_cizC?w@s*PF* zwIp7!qs6fjsM?LN|0=n`s31>mFE{5QRMsE#>g<~qg?W&zud#%fU96k40%pInu#~fs~ zaX9)Lmuc>KI=!M}Yl*u4(lhP%nade+shFTg)xt;0RiW@tr@a8aRb0(_LoL42;idwC znRU!>AKtgQVa9_?=v(z>5;V%@ccY$3E|3pg<55hlupY;?kK;`1W8ZmIqN{&DYk&d3 zkFfzun0}d(5-g|eB=;WEgc9G#+ErjAQWJATJaza^7LHqUo8PtCx*v3JQR-gV0M*pk zMqqcoPssY<*j+?Zo_tu{eN+sTn!xp-hd%9}x@n$dI1aMz=<=wkL^Xd>t_&tanlR@? zN_aDX;}3>bvbRpF<0NDkSE~XnNPjg6PyY48PASe9e#9MAuZ1ufbvz|3t+wJs*f=yF zP9R6RUUlATbZ)H!-3g0OnH8^BH(zLqx4)NmcvHTV?~56#j){&{CiK#?pK=}D@+=P% ztK0qp>3%KlS5lShO(FfU_keD?wH2kNA(T~bi)pcoU&KbUMZbtWquqIg*lo0rjYdlenBaWh^!huh&~_xB zj;KLt(w&U%n9MNS-##cv56B6;6fadK1C#b4`!beP2L}TrRb%TFGs^d_<*0DS z*N)X<%OzPJ&e4=97E>ldCD%}s);O2KyH6Z2JnxR~8Pj;REuCpJ0TSjWsP?9Vl?0ih zSo)xA2uVzc-QsP;G%82{hyu|sZX}i%5@JvZNgp%`qV1r@VoN5CuumMEm-PU*T6>s` zxcXl<&BVLYSQ1$5IsTZ_L&kUuOzv6z8Bwih!1NK`m>Z1+UfTTp(&12^k2w91P(ve* zf9393^WF*sELO}P3(tP<2$Ncux;5*rt(V)zOB{Mmd{>=w zDEHp|ugY7cc)%|>s(i9D8L15fhWBPsE~2v|K%puREn<-BGc&1fj_4ONim&iyI9}w8 z#(x;QtDw5Ns7=&Cf(LgE?!Iw%g8RnZ-3cBfxCD0y?(QDk-Q70sZX0O6|D39>uIh`f zTDNQ7tyyEuImYw$C!h)06We4nXWL#7+fY?XM;4>Xm&PoUH9?c%E5)fA8QmpTQ0457 zj?fhM;_X455!76s7f4mLY~G|MEtFpvZp{nzUo4d$D~21sl(vrBvOzSj8ZF%1N{aCH z+9%xCNe|SIa-JO!J!Tz=U6OSb+>4)v(=HUmJ}4JUMpnx=wCmYh#4kJ2?-SS*xl)x+ zWoKNzzCQW!8u*H+Tdd9MJVXm|`I-8d-hUcZzwbzl#e+07>lCj*2vh|Dx6^`JEaE0Wl#Ju^Q1V~N)X%-yY(Zd zpm9Ar#qPsgtvu?8^3^s$gkve(1Z73qhLXOq$gmn?h^AV3xI}{6b7>>TbSeM}r+UG<5nq*s{rpUweX0!%yXT>?xb9o@4I%M-J^4X> zNw4EpUNEo_!@m$kG?Fv^2!c(=1XI#7%@*mj{9f*o!|q@E6prG(+wEtH(yVJKfs8qW z!OTLV56=Ys-*TI;xbfp@+)Vss`hAVUyiyM*Z}>kFri<6LW4G|!srYF-HKdpqzV7^D zUR~J#(uZHaAwY%;f{xo8;g4fi53?>NOXn0|78_fSR6iSAtyG4`W*TZZ1E|*P9*k#n zCXu{lp)ui0&~Q`R-v!*}`Gm&3lf=pnOx+x!9zpHZbf24lr?@>DYT2_iCm_XSOW_BC8vjs2eB0ayX?qV@~M zhjjhWD?;Vun;%;LQb760*IQ-u3*m7Nhq)@$manm)$ z)3xBEXJu*QrzKd~yC~8Hmeaxw!Zr9ZTw|Q?72K{ z$v-F!z>dUa82sBqbPc^BDX_q`lVXreE90nUr5`s^ zLi6{)NsVTOZRBrlq?9#FXqd!NtjJ{sIf)Hc8-RX*XW?B7O{4~d?9i9F(UZKYq2mP# z$#Rh9@^+DexGgADO5n;A= z4@QtR7R$n#T*VdbwJ7s;9h)jiGrkf&Fw=b)zEh^$*8~It;`|~5Gb-zk15p~b_KTb* zkIL?WNYR!Y3b0$SWT;;Ej_843e}*u7GeM1weHGCJ5L+RsJugKo=J@L4yt&}{T1axP zvNg?ge?Ob8`PY_1R0K3w%eK0UY!mNoagHrJo(t_th@TG+mPJ|_yF35*TODOld*F0a z;`A?nLqx~STME+^IdbPO6aM|MyGc3hsaDfGtaiB6-Pt|dj^i|Yuf8{fHU@H0{tT7A z9!v)n9`_GtKxRUPY!Moj-P zYpZjhG|(R>q1~(P!5ynjP0xrvY1_`S77dNMtO53Q6UbZau*2_B=irEu_^a!{zSaSC z95^aWxlQ0o+r(u*YLrJ>^}@|gdyNwxSYi*uR1~{P_(#4mf&F)D-G0J|W(CtOC=W3K zVSiUh_Wc%K>a&dkrwXr!BoZSY6@vgnEW8um%{Z7i ze!#IHFaVo7Y>CoOzDr(eS#af^N2_Dx#JSbSp_RoKUIUnuXZXO8!_Ncb8M6Rb^r@?E zMVsxNFngltL!_L1cQ_q@n5?ec&(pN4dUM^x!5*NWOO)6N_k4w2y423dcZeGLcm(IH zwinIhF0PK|Vk%Fv-`LJHH5m^0`;VEaI z!>7cQoRY*x=MyshXY-H8?J&HH9qu$Q{6qq(&gP#E1T6d*2~X4REYbV<_0l!FwhizX z?|*k4FP?8MJ;38~b9PlLH~h}Q?V@FXynQ`=3CR^QG?S`<_3G{hddG&>v0Hve4YRR#8M;o*j}ar4ykKdPtpf;LT>sD-Kh19{z~TREgx zs}AXuIa0%o3AO%UAN#WN_vfRWoGth1Hk*~^|i&I;@c^nb&>kr zqNmXN;uFE%qmZLhv|OjZXxcSdmxT+m=Ql$1zNF+a(BeRsd>3D4Ak#tk#Qg-)coY*# zMU?e9%M~C`d%mR1HYk9{mR?tYO8*2tFD!n_1m8)Ts$8?S_9TeY@u#beAYON1ihis^}wEsH-W=y?e^(ojoaRm{yeTch4D8WcmmI|+gf_}YRkC6 zQ!l;vKZd8vStddiofm-uTOe=9$trcU;c&_HBm#j2i(;l|IDEtfokl$Q#-Wo*(@H#6 zvoVRjw4eL)ck|=Vr39*6McpYRKZLTGZII#%89sY5vl#hCR8G~b=rUe9X?qMUt#F0J z;%F`s^r)ub7%2s3hJ3bMir6&tX002vp$31cHx9H>HV%1pUn}piYsgcXag-UKw9$%I zOqLq_!WjK*1LY9NpctT#-L+1)t$i&TQEDmHazhVhi$IoEE$fgcZ*cl3=;!u>R3W`( zMJnF&MYt3LKYVjGYpIP3yS&fhpOKPSPA_bTU$5Q~#N%PW{Yo!e5TE8Y*tj?rty5_r zub0v~@#4FOF_?qrZ_bg>)uYce2y4`vGElQ7C0jY zt?4)~z50^YzXV<%gc@`?nsX+XM=-RRPNY{0g5#JkN~BqxL6u`T8q9jj?vv9V$s69f zfX3g&w_3lqB<&zQLPs7^;%IC!W0^_zU+(}j*(g$GHS3mY>e0~o{?pcqM#us?e{rKv z`b2C1RBt_pd{_uF+9XVwY%^u#1i9+5(+z|RTKDKtN7&Uzxkiey7AaC!!4lyig~V1F z4YDdfV&?$YH6+MVEpRwOgH2mVX61McVH-*-FGJ}f_m5%RF!r>Rf8mfsH}W`9v5E}N z{7W?=WU%T><*%>6lc2hViy4c45mxzK<@AQv250EgAOP zn6-*vyB;Tp;rZ&wWHis87@Fbsj0|BU3&H^V%VfM z`Qm7!KqG-osI|w!MJ40>iNB0Nm1iV4NN&D_>y*ozxvE9MKiY%Yb6V>kY5V4$eB4ch zt{r(AQ8ud?hQ0hzINYsmm{R9b`NHWp{pA_KthguAppS|?5lANo6V7GCCQNxr5;fxS-A?GF=1MZ`?3ijSc_J`xLLiF8uld|*w{J9|`t)PMM>}k)`?1B!D zsZ;C+w=tdv9+ekhMDupMGU5-%a`-Yg@>H~u)An*U-G;Ji<}~?2XqlI;KfQY2C1|)_ zx=iKXa1zQ)4SeT4I!kO8efQH&2h1(cD2x0Uh!!($cMxP=*tYeCL0ux5V3T||W4;+n zr_4y93f`l0{K<(1B&4y%pYu09T4`jyW2X(5)VIj;drvF-d%v%%DF@g1ibriX$Ja5f zpBQ~m>?+Sr^+=1UU#Eon`$N8-c*2urvz&wPh>bQ%H_F&aGIM`WXnL}E4lGMVov+q? z`D_|)Kr8khTUM{IvGV?&WU}0k%j2<2(woscXNq8DemJTSO~YB%Yits|zMj@ydZBTi z&71qX{^q9D4VK_oeHFHD%fXM~Rrf&}v~U*J)@^$4w#sy#S;up+;wHNe)wVXA-S+C# zv-fvWEs~&|(WC5Q)Xyd&qT#Ng!u7t{*7?%?BL9&~Kq9sg*R?7;IkFhO-gVr{qt2=$ z1;%3!cFt115?Pi?RV%Jk&Xi7dqA_+}P?^5Ox_HTb$*=Gsd9~ffU+LBKoZ03>$vbbc z1&wnBP!i5O(tLVvTl<;sTe+y0r^>nk*x=vTZzFK?I3=F${Oc`W%yj~8i{|YyT7oBb zQ5pSvJ}+#)%An!GcRaSADy3j{qY(?zI#}6mU8|qEO_cJUzs)|xEQpb63iCusXc)Jw zOrs!S>yom30uAf?pY)q}JLZ5-ZJzSzAK-<7d^A@@R?wFwp^jdUJULQK$k(a{_e zQ#b)V?t&t+BNpL>?k%J{HK$H&=A9Yhw!TQDtCyR(#kLt#s(2T3F$~7%IO~mMkGtmJ z^7EuV{~!roAL$ZI<(}pgJkU`r?_^ClB^dwL#P2D}Pa2cO&y=minJGxqRn1mlinHk1-^AipS=7L`3La1>#HBc6qdAR+k=qqv-rFC3>% zf)WXt@jt@65Sl!gg$C4}RZ5Aa2u?^2ERqQI>I!fV($0Hte}N50j|VN2VJiWo!eTsi z11QpkF21E`vvOx(pVNK`vb-r|!VeUZ?MB`($p~yH^bG)=Sye`Ib-K>Dd9Py!^_}%y zwlf`_X<0AxR`TyoES|Oao(CclKd*Y8o~Vkn57C;@6}e87XSq#8^*r32?=_F0vF@bJ zu|9IHa=Yxzzm%osjGaASwJ||a_YZIFBMQw2ULS|o`%liCUF#9GAfnBmQ zi5xSu(-R^0UXx7sj?y_2zvQ*|%|O*Q70I2`l6;-G$9mY6WUHWrk{ZToyvJ9Z?zE|d2;uHw3Xb;E3#CK+zywugP)593m##s#P1fu<;1&ZQcxuiJBaU` z>9AqkX%JhCfIkEQv~LJUiFJss_wy=e%w|yX$}G9nRW5DBW1xM$hMZBRR!rV@*h220 zn84mkQdw0$Ic{I^OqS-1AQ_Vx_R`)7g`0eLG~aTf>*6k{!PBHmy=v*SFM?k#M7`$G zCB?6M7Mnj8hfWT3&N-kW!7V|#IECX{wkb+(32Lu7Po=l>oAo|-MesGjcq!By%K!qz z52I}be0sD@Q2ftjZ*!qIe z`asH~l3KwL2A!0y6(y-}B=tew3uud~aL-brt(nEB0n`Edy?~_AtSLk3mR2_=wW$S8 z;Bq(~v~9BXLI{rNVXInPC-1)PP`O-|eHcFUAm{VbZxXW>^FLX6C2(F? z{z!8vO77U0#HMfWLXqu~XyR%b9=Y*}B#cMp5qLhq9xV?&&-NJ$`x~Ey3~Qk#RrBti zK$~h3n3)leBN_b5wwE{C*;`>P2}#@HT57xEP>IuUd`HW5kpX4BBh-xg=lHsKhBbdi zLzI1d17N5y<8Pd5()pEB16f_u!D?xvUZ9w}hB)YVm=cXjIZe16A=-Q$d0MrkDuZIB zC+*aYGeue{=WxW-(8WMnbmEY)9T;E4x*lMmv>h;ZO< zVkpn#gqXw{gGhE02VEpW236+WWUBG*wOvi7j&IfK3%*&i8Zyl@yqPcUu9IDKQ*a;S zzng-;XO+4tSGw`O843j1DhDf9^Mn&cXLm2jBw(+e?eu{)lD3np*{a(r{pRQL6e9Y& z39Az=mc~|Uf-ckVvn~5w85d12%$QwnGl>zUa~d07MPAx;`U7R4Q|v9wG`9V-ld=_k zPt~P`1=WYD&g+3>SxNgf$C-^jyM>(hjn5g9Y@Xsi8^qS{w{xgq)_bqaNSNW%I(mSq zOS85e_b#&Vb#o*{G#}H7)X^(!-a(7$UU(`s)7NYzznhYtf|VQ9+gE@ZIfgZYDyps26#((60sm4RYFGMbOjZqI6eB4#JS?K+g&Tm)f&rW7}6UhOI3Op(cJopIO>hO9RWn_|B z1xwuPGr^c1JN!}YCE30YD~q)jI*M|{0vwzNgXwQK=IeJ==zGTMZgO^w5j}?AG3%5@ zQDwOe6OgKY={Oheeu9wPCJA>6Go(C)Etru5S7XCvnsQ1X(2rI}G}-BBb=+5fEgQ3O zkhrIl7YGqx90kz2F0I4W(1P?$R!sN1Ns+QKdb63lcdt{&thlw`L^#$ohDX=)> z-lD@)m+|l&`KRUtrFy-4kETPe{n}f)jx)FJXsaY;gPI=%K_&|F8;4fcJ9 z>e72t<;Z31UaCR~kv7Nso&~>h1!wG69P-LJ;E5V6Vm;#X$t>;62JX)@rf^^~i zArEDcL+YP9AbYY1M_FqudZaye;dHNQbxBtqR}tMwEgTvz8)8>uR~Fpg*AsoR`+`5o~DG-Wow!z4R6h? zsPf(0DF{UGdFekqxY!HWN*vh%uGXDgOd`kr_rd^zl&jMjpqW$yBNNBaXJxI19XYZO zz)u;KT8U#8%tA6Q}S$FTJ= z;PEBZNiZQx+?p#AuUD(D*=5uyH#J*qn4(v#z8y`oeYd|<-Aw5WJli-n+w>rt zaaG4w*D9oYReLZ4?**MrGK_VJRotV?jk|gi6t^Ug2Tn^{fzqk?OVz2jW~GoWK+9~rfXr(#gpM@da_pD`QMm$-}!$IFrFWG zIz2u;+tEgo^uPLc=(ezEaG5k(3GShkQk*L5i#P{s& z3y{a@aSX=d<7f4PW&;jdkTET$L`KzPO_pp-62{76x^?}=0o?B?n6I5@QAtCENcQ%@ zeH!Hh+eAu;bc_mC$=^s0Vs^r%_UrlGTctiL8p}NTpV_i8Nm|z;`3pbwNC! zsx;5gb>+i91Q~xyG9pU65Xfqx#h9vvF{8-?6;A)OM$U*3t*e?O)%|7|Gvf-h110mm z$Mji>1Xzj(SV{#XsZ)k6Ci1WDDm97XO~Oz$XWiUTFxMOYBB8Qk;)!Vg4#|vP-m0NmFpSg1w|dmDeL?z1PDhS!g#$}VKS8`UF} z&{53r#KPMj7$WPTwrEnQVX=IhOChpNMj zIL;ltACLC-F}kewx@$_SSqjWzXFFi?bra1{bM)Lu)2UlQZTq~3dbi|)H1tZo(uLC~ z5v{^$0%J8w>YZs-x*Z;zQ3;!cZwQfIF^7h_Q2p&|t#V&_?uL z-|Tg@&$4tqbPgy*EP)cck&DWuV zxz7q5#cP8x5kgZ3l@Az9@a1PouD*6a1mvKn>@fzBp3JU^_2+*}zE$0I#z$u$%|vS# zqxkEdLVwscN^6MUys_;S#FD8ZLU8TUzp|^5;FWKlVU^J`!&k6kwMvWZX<#_@BgAes z`XGAIE<0~=`tOi5|c;E zjd92Y=fx;X{t{BN7AjYWs)`a)IPEvLwsm{-PGH~*98}+1X7FjelA5s-?EqGi;kc4X zlGudiWx$KZ>9cPCpF}E5faYu1j#C>_cc$LMGSj)yIjdh`k{}bokI|E4L#_f7up?r5 zc+k#Chw~!G8A{4z2cbLA4&676T?H`}reiG=gV ztNf}DW0oHb!CksQ)2*0o+BB!Yvu`W2=i2t%L6ZTu1Z#A3eV%)H&=eBWRMz&a9mI1a z%hRW3Gs+QVQ*7ZHV%m7M?a7$Too$nD^t7+bxkl;D*wL1}bAVj!n z^*7~@_lPyPdFIxEFiVMjx$0H~{qcxBC2?=(O`BO2`_Vq5?>sr;ZjqgB61#1nOE}F3 zi1ZZYJR-7nI_4$Oj*FJG=}L^${@De;PTx}tsz0z|>i*u%&`sLS*v(u$vgT&A2^YJv zJL}2Zf%vRKcOt8PC9-2>^L77+Ex{>$Gj5IE$ioQfFC)weSCrC1MCKz%i0&)g5bYN6urzqUTfvkS zQ1i00gU(NNT6;}VcXge6hou!OP8O+0%gT2BXw=-lb3MerqoMkbz)GDWTeM3bQjzX4 z`oXDq1Se)#ef{@@FZFaq1M5!xoYTc@Hb2o}jM{^|jq3}CT`84RD%Iuc9zD17a8z;(IR)NK zpCpAz^VeTywXeFhKn5Tf`~`^qhqBUS==I3~wME4do%(Z6W%7&uEUqVKTe*bnl}2wmk$n-=<__x_v$(p+PLoD9ofHJYF@G{{iC7n zivf3CeTSd6G0Vx?0b;lt7nhwVRkttAVR((ZGTa$SN1Ab{0hS=5Mn`@nI=C@9W)-3} zU}qJm@6;u4k+(WMO7#KM7_D^kR0H?kB|Wo}pnKni4MWsj{~Zg%^GsQbjH1_U`!8Ge zvn+-UKNsQy9n+3Hblx1vl-$1Lq2WAoKp5YBsIF_|hi1uiz(%pIGcDc?ya7t!5mCeMS6vJ0Xr61BM2K(hYh%B!H=U zQgMXnuC6vZ3w*BkEG~A%u!Q`Wcwno5eLVkSbJ-1e4YNBcz40CWRU}(S$@z8$S(-z* zQCMfNEerVrG5IuH0d)UHdb%U70_YIFSO7Q%&<~5p3ZXa&srOncT~{j#?;Q4~I72qH zULG?iR6Uol+*TyDRad52-bH`CJ*1~9=14E==Jx-45&(GItCeO_qNkTG#YjB!uTjO6 zN2VUixIaMf)qalmF$h_DfOg_E@bn~`!#>Dq${Kj2CX-y6dA0Q_FTTcnt@?UFc|!S6 zFN#wJi&P$EbB5Ex_7SbVS9lSG`PF^Hcw*jAeI$m#O3 zW0ukJXE|JkQT?7|i(Ws9x%$hfQd$;z^aO5^Kcas`c|e*oZBWk24arhjNp2ngu~VOi zIxUNF#jZ_P9rBlSKQaA)oiIqV*rk-}_~#vr&G7T`Jav}KF=qHZNY#hG>keySviOSA z3{1jDpO_Vo{eb69zr_cg^5uj^bWMF?($5z`Xjd3N^1RDPAlculTNEH)E#UdEzK|yj zGbZ$T1J18dx0QhPX@xO-N_8}1!sl2;{9p`cqT6+=1GJ!`s|{X9*nLO!6Yz_AeRDE* z$A3rE5zm)Bv~(p;#AZ&eLd8(1)~|9;PNLPG0Lh0lGn?%^K|cdNE<>Ch7zI>_P_}+n zk_KluNfC(l*f8Hxo|e{ITbqLuzP#=%Z>`cM#D@DO1Fw3WI1UQrEGw`>|HM zuqr4@Z?_OJi)~j<@!UC;wLN=z`|_?|y+zWU#&hV>xnKe`cJ1Iw2^W9NJ-68AQSk>`LGiv>BQm4Po`LU(>^Qhp;h7Rgz^U! zO`XXniw@RGa+Ng?K0Ho2$*SM9)Trj@YCftgihLh!W|gYs%oI0(T;p?6dJOF+Ws$hX zN+fV;H2({M?Dh@1CJ-{xX8&;7=aWalS6z|f-^5>Z?>8*NGgm6M@YHd#s$4GuuCQQL zWJPDH1`g;iMpiEOdDX_%%M_Al=@pi*$__BjMQ2zKG;ya(pqd7Y1b4zwqXBR`qd}KUcnFu04m>;@i@!yw z7*k|b@~oOGq1})|7L(D*jLHI|6M>7FJ`kRxis#zdEZaW8TIoi>>J^Bqx(Wc+U&_!@1w?~Nk)kemoYad8rxYsLQ zyD=*{_uKLZ%6w>9$$r{+t!08QS+jx92FCf3U=oPFUqUebgM0q{-J;{8+}-TPSk@s< z-46NB&CiUVK84h?jr6x|4DdIKzkpY}8I`svhsgcobWW%bZvU)VQw$;T8%rU=*zdf?K017{}cnoOiMSyO8&fi&v)l|F+)W*-aM|QGi&zi{Ga2> za*<)IDs?*HX74EyuEDRqDI`?(^F%ui6iol9pEDwuoX%K26!-l|>h&GFrD{xYaanb& z2UqXF!##nh4tP=GI|D?WA8q0V^mFt%3Pl@pWhK{Z~~mRbMVxzpjJbbD1VfaFn7e?UTt+lLadVdTQoL^ z5c2rS^*3jX4Vk9d%5@FDa_dh3xn`j#;S?Q<sl2FBA`CVNXa0>Ay zESn$;pWZsd8p5=_sPthX`_W7Ac<20EJD|N`J&qHjF)sg8ow)+RVg14Qg8^E5B4?D+ z5wzWjy(2t-t_c;oS)6GW^obOT{}d`%#>NB!iqb>&KZOdJPoYBkKcS+aY#$8cN8gVxh6@|rC)qVhL*O0VVHkzmsy8U`DO8Me+pFOKOwyeuSSNl&w*lGz;>{!3 zxLZ9~0`fSE?G{Gy7e< zT5e!i>_BU66V=7HhOMBMU;iYZVX933|J88MPi5MdW@1R+S$6egtWkD97zl({{;JTE zU#P2~@6sjj>NCrHTjWG}{O+Y2^ONh74#@_gHj~*ZIg=TW3WRAM5aZI8f9X3UrlArc zhcKo@WeWHUDe3c`zHg@iN7IfLLv|IsFCMIsoPoc0!R%hw^oV1TB&>Qq( z@y}cF&t@aw7E^n6N0%RjI9Y1D9!Wp5Y{N6Q!ZZ9ax%~O;{V41O<+i9;0TH9`sn3e6 zw;%oEQ-kA*gX11U<737L60Emi+|#m5@vt<++Eo3LevuUn)|{Bs4^-9V63*J-2&;`j2{G!RS|PWclqE64ooiH(0HST!NS8-4lmg?-ycPi;datKNOFc z+m^M(Sd}@98(A0BjDUb$G79mD6IB?1XmRv zX*-7pT(Qy{G3S2#(ke`|VXB(^&XunjQ(E+G;Ksq|;H22$kMZd(VYCTu1$k+zA3}8h z?g^%FOG_K#_kUIWsYG)gjS1LhN=?S;jpz}_VNbr+6y-^z1k&+>j>4J3-8oHBv$qku z)HfwD*aQB_P=u&km#xxZn<<(C{c8ckTUFTpe9IRN#P~X8u~vxHphqz=t_}EMga9Kc8@5!CPYr=Tg?9-#bCG0BUz> z`iavW#nn~TEApr6jpVhAZ9lzNQ)I?0sZ$MD)Jc!0Jx)qDLr_OI2(}gavn=xsZz1z- z=2rT2Dthmx`du{ZXeMIdwjzF<{WK$541`)UQr(w`#N^8_}`vfi*? zuZJ9RI~wyw)hJiZ3s!fEc*nWdHWBd`%VkVUHX2o`S}eiyMot#6AGz{ID-`azic4`H z9?i3Uo&fO{OU|4Dl%Qz>=mIsjHKX8IcS>S{>eL;tZd*JL8*uR1ZY3L4EpW_D53pQ2 zaTr#lj)AX79lgskpTWD*J9{n#%GR0<4S$nYt$GAw$92{(B=#UZUTG*JQ=(>R-3J&0 z{(~w+M6CuG4d7XpeaC<=FJay;<;@G5#x`dcV1qn}LdU=LHaoCGU`(s_xQ5RxPhh5m>6*Ho3u^f=I3~Km{>Mwaj2DbfMB7BLGL|`JB ze1|0a)In}Xu{4Og!mh-EB!Z%a6uOCyij^)?^k$vt5{HqlbPD`4*+rU#NWp3*X7vwX z=}WifJxyWG(h}`GtVo|$WYBl3eaqWI+W)YMvbT@EZ^y$G6DiEc+LUhJr1i>CVGulQ z_4gQ&3^2pb+I`r|!!Vyf|0L*_SXzwLjp`f9>K$9>@m9EGTQ7vlQff}LR1O8}3T&?9 zePu!FSp!-(6--tV7F)xC1QtlX$NFnR?MqPvA1U4&!@LE4a zPQ|+?>R%1rTC$yqy0DdonMkc$+o*TnxaBsguW_4~Of=*H~ zQY2sa+sLhgf39#ozX$6-t%5Pm9O7%vl7WaO$y|-WuMo??Sz% zm!ErU{K(%zXXZff-BK*0txC98$>{cnB?14F=aNto)dH}S^wSU~tv?d_J9jUu^s|v) zg;~eGCZf9P)3)1BX&VT?;I_SZ&PtZ_+>fAzR+TRfIU$xXwk3wdZf zJzECL*RbMZ2p%=vfPd!U3$+yOfS`-SQMqGUB^4POpnV%8o_l~w8D_1`vO2KtLhJtH znl>rVW!#r4|D6tP;Wh~s)QwCJllB7D>lExkb*PY~QB6GyYAZGAfTG~jN0^grgI<9# zx+HQ&@eH_2$Pslv%S$qaXAsoig>rJBxMlfGoL2;s`ip`s_O6$LH3b}=KBBK8zxf$u zDLKrAI3o7v8;-D+1Gz_>z+FYD$4E2?=*sAc02C52OU*i%TTzu7A)Dp9XSfg#?D1Ds5w=iW1^e%h zaiJr(pISv$OzFHTR6&fcIHwAu+qCn0Q7QE>(mlb8MUj3werUjrbAy2R<{?|zMJY;{ z+@|tODF^BU(Wh21HbM^i@+@#a%i9&C`8|-SPghs!;btFq&$f5qidZ=6Kjp-uKST?8ot;`c@$^(y*l*+i&lL+{m8 ziMk4FfktM$H99aybQN~T-i)2$_Qj7ouixLWeQ|GJ5np6q=)DrVoh)|3 zy;>1l{CrYe=vxU|`C8td-ZNjnJ@Q?|u2XmXTl*Quyx0CD`?U5<)67T06 zc6n|v;RfX#*prDbls2S)rF+2ljOi$PPZ}-s3k9d%CZGtTbWc`qccHkg$(}x^mFcKe z2b|dUaCc7-?KiCJH$2g6nDFqVybN!tAI71uen`#pu@rBt>)g>^NGavKju3$O){P+d zvn%qx;cg6o@NVE+BZ)!ecV8YhG}sh2sfPqfs_D39CL66g*3IQHG>*RO0q#whIPQUl zZzM;IAP+G0^6TNuSAY~Txr51qzx=J5ntT}~2k zg=Hqo)ve(`YQ6>F71vyBD;;(4 zH3DTy&C}m59q6A(a8UFFR71PrNQF7vt6B;Y{OXi_58WD7F>oeX^v?fGbXOBI{+<0u z!j<+|e8LtzyA6rlAE`k)Tbt9ZyG=2RRH*p2G_vE%$^HPtX8xjLw(!{7zrL-wl{;iM zHcwVOD1+-+=*k{@-rUWK%|btwPJ=h!`zgQz$lCBcNOM3aUUeHEtqtAfn)sRe!Hch^ z81iW@M0p$rr9JUZkaQG|E~S zRUSB5kfqs~1j<25aSHXCMa=poNAx{LBlYx$^UQ+qEhUskSO`Rzj z(?A{?>t}Ni_%pWp+?n@`v&Wh>BMPQdp+k;&iR zk`zjrRk(8tyc6%3DD5P9@dv)fF$m zBuNo1jm1B^EqG#;D-(F@%5lCIHW?+w@KiWi6{y%-cod@OTG`Sv-ER2qFg1X8qOZ;m$(A#h zDJTj=?c`AGIpx;1ixB!_`yo zRI=h#((C6t>V-12Mh9soXor`T>B(_N(BY>5K@#QWe~9QU3?Z(L^IOUsSvCHHD_Cp9 zERFYnihsft(6n`Xq}?Y6<$Xa4}r@F_RvVfFR`h0c+0ng_tRNsOM6iES}SzxWlRB_0EJ$dg|tEO9+;*&~mwmgoP zUWy4g*5h-yPH`^u;>=CunwKv|h>4-}`6Y%Y>ja@+I#8tBbXQL`{8{v?!Gi&#MO{DD z%KNI+WGoBXzaxz^4fuvwy5d{BZ!zI#@s3n|!WAjH2B=L*^CHp!N><~BNw^Q(;#w9@J*9Fncj|J!b*jp-l;Ckz^sARm=b$G1e( z#~@Q(Jr$NIZGyv2g2Yg8?-*QQ!9y3gVd|6Cw(Y;^mhzEYv#UBX$oiVL71$t6^Qc@ck19x}YIP(zfd;qnN zes~&U@YGFYt^y|r{oP-?b!@|aRn~#5kwl+x3rNl>nb9nlIJ&QszC8wMrB5<7r%0(tt7##2>bgyPm+lf82c3rwZnN&c$5t+L zG01|JLs#@H0^`a8P7_Ai)?XYSg`-`PR>uDnsC17Z{R$QY#5})(1;Hq<=&xWAFzY^) ziNN#QBB%2=%i=oS>{qaWE=5pQ6}Fbw1VVtV7(0XDbTm@{Eox8EC(-n{LkHV-RyZW_16f}b z)5b2-qUJ1Ay^tYCCQvgFA5Eo3)x8Y6+^wQhr%*X-`Nq={AM=55=YZ>>M>m^tG%NfFP7vA=SZuS7Adknx`wp08;(1G1Uk;?@ab~xxw*ilYw`{@ z`#(m(xu18eXK$t>ID6E%-Xf^zSs$S1jN+|bjyxviuTg+e8g<-dlN5W?T1LLfeoD^H zw-}J2__O^@X&Ki8U&QZ={!LFSC*hqh_z%-{?+z7W^dmGV_9N6oM*0ztw7Qx!ja|~^ z)Tp&xnbeai%u7Y+Sj6Q1Y>+7L`|sUuh}oV#s|X<|7zik8V2euemA$56;qUuMFJ`K zaZQF-1__O|}sL+2mij~bLNuByuvg0V5z zENxB{3m7Hv1&Qf&dtK!BIRDPo%!mdF7%()U4J?}0OQOBULd&z1A>Th@#F1pUiVPm_ zeNA%tY_YcBx~+WMAMmdwBUDeHxkbFilR+r-()_?7Zf33m*2BPECm4&8df?f-2U5!zQtzV+u4vXJpP3Vj{hkjhc%W0$s-0lCr5MR*T1Y zQu9UJ_HXPZtegNvCnw`Eqr~GPd$e# zl0s~pKFq8&)QYUs3&(T@Vm6YWiJv}daF;?7P1^_tm<5DI=|~2gew~8^^P{OH&KTKr z#XrNvf8(c2%ViFcMTAhtY{6nK?9Th~jI?)jaqDOv$VV)mq$+21*6w-QIKXik`NqxV6_g|^%G=FNQrEKp!I6B+JeUTU1(rTe>ds?745fb)3xR_|Z*JH+MH{x;6W=qPJ;8u@v8`M6kq?Vc$ zj~`0g|Dr;cOEilP)nP6cFBY+8u@=lKB45qS&7O?3Mr~Rp%#C!7T#fjR%#DnVoQ0E{ zRBhl}ots@7v@7!(2F#HrGnoNAi4+QZ8aXhc$bJwygkfaOg3%lD`5SNJXMo4gi-9 zhp=M3EDO#eYFA{onbeTTFLSk$?9>gmmp+{*wGs%^(l+g0>{er*xJ}Q~NNReL7Uw06 zTlwWi--3vpo83CwsC%gIk{7Sc1DT>-OMdzWnFa^{?CxP+VIC-tRPKYn-BgM&&yM6u z^JKC(NolB!+u<>K6xa@)i$Pn+&jMTULow2^CQ3j94~7CKk!UODdU_-h!esnIFO!AA{73OkD}3D%hcoO+qvU z^s{b5Bya@9*i!^9r6$nno3$~%R)#^SS?$utt(VC2ulHB1KoUVY(d<1?hsTU7cHVr9 z?;G=eNIVfgUDsHIcY1D1@%t_u>Rx~KFGj*{A)j_y$PZ6G>m?(N;dq`kmy-B?-$o|d zUfZ#k_p+u+5iJ^Q(!1WzN;9tTH2aJyHihtzoE{frmfB~3_+`(=;W)5Ru!3m9#>1mU zz7gVkQlW+gnMpB-2(+Xbwf2t-pR|Gv0ic=DW3D2)brvrhf@v7X!3KuV2U@Tb%|*bs*ISJJDS@w1H_oFn&xBDQwEzyHUQmAV;eq>f>cNkA`p@;1PQ~< zBpg$iprXf*{LnKnfdt>giB`WhPHDm9#)%FeN~i86(Phcz>4r7Ge20IX>UNta+myd_ ziMrJlb|eJ8S1u93M{k1amm9ONfjUb%c^)#*QWV2Vw2F`O{APNA6BdxiKh0m3D+`u; z9a@n4D`KSrU5qcQRJvdRd18N2VF1|ar_P3yfdoFIZI z4S8o}z~~5_!y*P12u#=G46~M(Lkk|FtMSP^e@}&g>W0EoWm&QoczB}SBIyDFlh+>h zLL5`#pJ6)>Q?6r)i;nLu|Id4x;Tr{^QlfGeZ3LZtn|p zsZdRuSH?C5n+Tz*rks92#WlU&UO!>`;%oK6{&0t0Sp&Ry61jh3#EhsYT->B(p_s2Y zTL~->GcrxwwziX z*BCNApsx+QkVS0*aiec0=NLSXQeWQTy&E?-bU|#!EGEK7h+dUTlrYCGMsI}9**w_o zu`z0Z(hn*348?g4Vj-ppb9-bA1Sz*&DbI3kXaJfM$bZJi20vQX$8;s4SIBzBYQu%G z)#Lto63Z|=i0Qz=C$4w|Z~a+oYI2L!-mmkb$MeRB!4Z++!InM{QOl_&MRWGa_HpNA z-#I}w+AaG`Df#!{6?{t zqm(h)p?AA>sy;z)Au4Edtb_yoFCZXWT(W_Tc!YKGwI7-Ee7~6JV9@ih#@_^3{DuVav(-rQ{3^qt-FZ3j7#Qz zI0*iNr3u3m16Ng+GrTB~l|YS%fpDU&@fQRkaU8vDb`+@vtf^` z$mMc%(9u1>;Y8hjqHDEr*hp3FeOR-+X)hDbs=9ED^a{`%-QvCLUe{daeRUo|utTkl z5Wux9Qg5HrboM!~AgisH>sHR3Q*y7)ky_3`P-xMjN(fh7EWKb%Eov!kDecX~lg69i z#N<6Va5O?wx2Q(DXn(&^2^H8YM(9!@RCpcLrgjZYMeN-@`P^|X{(roWz>z%V=Cuy? zr@W_M?PDahDnn|*LP`9*!$POR@eFN$sQK``s^lcGDDE)9t~rs~PD(kCMXa+ z;yH3Dxu-;az0Ee9;NKHA@7(MwFZ{imI6xi0Di9|awJ{LiPdU=pLIH;)<+wMdiD3d^ zSA45OxDrF-OxvDs! z#}FIDRYJAO-YMUbkvg%_vj<57C0EW*K0S(u=f-)Z7C!sSA^1ra_uK5sVf2Y|_&13s zf&AT>c3|sb&6>+?`~$CBNjI^pW*;6);4CYz`(Z<4gF+2M$Lj>MrH%JJq3@=8&i)vd zt!`tP#aTfMbVzUWXZ%4xIW^G0tn6ewnGnTc@D4p$}+6{N9m0> z4NTdlbmVnD)@syh>35!Sf-G}syr|3hPxAh;+sb>-17jz#7jIbA1wV^FIv-=yy$%>z*zBB|dm^S8i4?!~vLIWhpQNuHwSmaXrUS z$zwwZgz^H$F%gA$k9#?^Q7TfD$x-NIWUR67FkyG?S0*eKIjo0dcWK7YoYs`u8Ivhk z5eoGVe}#{ggGUc;4|H6ZBOSbXo@f=#9En2t^}t5PWV!2?dSIs7<4q>%QoGfWnH4FI z=zcXE1Z%gwNwa_EPtCZoxlI_S5354tHR)q&iz-FI<@LY;)U>Y_7NYHvA_aA7Q3K2g z;`gzLAH8t_ z$kp&iB0s577(bEXg+@ECWj`$ZD^-RB3XDS^hzh)!F{jgSV(h_oz?;SYrb2ed=@w;C z*tH@L%sPkzsxGqm_el|KR zE#6*WBr{uAZ)$7nr?P&`{=^4iKrcNYd2#8lznu+!b}YP<&A4}ICW_e^=N~pBr8;ib zOhfnQmh)pdyR*n{_Bm5q!<9I|Th~#hNPh+TM*7ApC7RfzU(Q1a6jBf{EssrSzj2;l zFY~C4w$dr-(AQq`5q9%`s6C$@n>O-J^dNY1e~4`5HokLMr@GV_)|kp{AN~D8m`yRj9W42^OIZBTZDj`_X`cS&s)6h}otZXTLeqXO$ysr7GT|cq()%oCU zCbftT{MTeU!0?vgRjV_beX%I)TeRb*7Tw`zBR9%< z#j$NE6M^xVt&yF{o>|$R`gr1yrTU-nQG2R=y|ue_wRPd$U&(#$z2k|gG)k!%)Rl_V zoHeI`uX0GCL8*k2UH->XK*}!s1)?r~i>K6f$#$tXPgNLztpJU$TjWe%Jx()3ldau2 zFWAz3Kv;WfR+i?5+BO^NA72O)1gw@|v_=Zz8|->X4?Tgg!+!t`BjhF$X>86~Z25GB(HO%s0G;sz zDM6~v0*%G(7@A2oRMs0`B(`Id@jh1SCFwI}P!eGNs_IDjMojurEF{$WnrUeS?{rKp zBt7_2L3!*_u=GNVN{oY26-r8q0MG3RcoEvQm!J4$rDT$QIYXCMkR!$D{>ELGjFen# zsD>6$#5Lx(W@-lvSc((>Dq2D^9~Q5=FGS3xcB1oc} zmPTCwXfB>eRp{f$CRNa3x0brBR*yHJL#|zmy9g)JGIBG=UE?G5QYGP8ZpRzGE zsvWCU^q!TwrUP@*Z3YKi2)qQHKX~H5gbF)^Eo97~XmFtB6+P5JC(cm1E1~W0i6*X^)A%U_E2aZ%5on1VF2^`nF|aq+3ewH za}sgBmD;_AuGfev${PbPJ7`0(0Rsr?6ZTG-TDlAjB;Aal^C_N%(Ll+`R6ID_^hkbC z0B;;H)@oL&ofQ2yxf(dvpwmOc>i2!f;HZl3$B4G9^K{x;;ffoVfihLEo6KO>IkE49 zPHXVH@8f{BXKzbWy<`7yHIJ_K?6tLTW_r0%TLXOCJg63=U~Z>K$~7aeK98Bta&^N7 zmJ<e;~ zy!PchqZ$iM8Hnyt6jrjh@)%jB;;tN6sDf39Y*LXh`ci&LHhfZJ>o?+rR|wNlebX9( ztQe$>p=?TO@dR5mF@ht}q}X~CnSNx-9D}6@KT1hjae59c)i`VhI8I=FzY{)d$^wi= z%Q~xPi$q|#Wn-&{i_s0^&V^6iKK+X#N+<=@;PMsApI3p8boIl(cJ?go0T41j$$ z%bvLfbAaw#G|o7K6lyQ`6XdC){nX_%`Tx}8_K+sV;1p~3y#SAD5eZxz5@UXFnf5vcI!0S2yGANz(sBLy=~K1t-Sz43)%O{E zg8SAr8B{?#Egnqfjn$pD4bvXBjozBtv2dULjFHS;mToVXshzE}hM&9Z@NpiJHf!}v zO1Xg}`kmt6j>n|Oc(Q5f5UHxsd>+2PZl*(y#?gy}Z4z}DG8<i5u6-mr<-!(pk~=8!79oF_T!8+KUbMoHBt?w9utp+4F4t*pm#hNRj|2a+95t zV@n5aJmZxPR0=|=gJ!}ION^xEX%D4$F5r~tYyOqurV&~gNrj2g=|k4Jn+qFyz7!%3t?xMaPw2(HY`M|DZf$VUrCIjX?UMPTw*N|@R?r8PjB zB#!?H$VI3sX)E()7da>ZV}lm7vh>@5-YX(;PmDC7W>w9De67bHm9ea5r*~x)jm7hJeVq8?} zfaDKBNcjYl)w=Pz;Q&T7fF!zPOr}gm6A3n*@=@Ewrffmz=`h=Xf4-5Kzri7>x)o{+ z91dOpRZ-D5l~)$~6R?<+6m_2xqoO(^V{l6j8=%uH1ibHRJ5k^Q{XrxjnF-Nd8uE)_-)Wc z8;;k?kTRMFe%h#co&`_qS3%8YO}#N=!_||#_dgS7j4EYk&)N=%Ng)rC&bJ-F#ELPh zE~Hz^x6dHGrWO?6m9s%|4(=Ayk?u6KqKWBHF1!fEC=Tuvy8R%gmctdGH zDwxP-=|V$hu7czS|8X{pAsBj-30~ojfwqlW2_Gcc>Tfk@z@H)CWx_pj@U2_X_ z(j!zIlJBX1M!uehnh`i51=3rg`J+i`)-f7m(+>)wiHU5rU~pA*CYSZ* ztlr)~bzqmq>XN{jT{8J+Qy9YL6IzHK$P>$*95S9gjGSiwJVr=6M?8oMO;G;ZlmE}L zIfBORSP(?f-*T+RzJ_%)aQoXuss&N$u(y0GLo_llI*{=%fZhr+3(6a&T1#DuL{d;Q zqXfQMYC7@2CviQcuCYPeyZl$NL6e%v zf~c~R8$QsQYD|d4PX}PW9>N%?j!UCB`)~k9vuDOV%xUfF@1FEFPzinXF=RlG94-p^ zO+G|UXHKcEg-q*0ZW{NDdc@vp`_6UkyocPz>>lp&7QK929l1E0<4wq8%9Li6S75`2 zR#gPiP^usn$}C!D<){fnb?zAsOwG{temYzXP2S?@g&?%ZJ0eX3x+BdnQUtfjsS}sy z&jtzf+o6|AV;dmIP!{y#JceB>X$_4Ne5wd_OWH-y(9=jH1huwO5{Ik)0Py%a!KgZi zQp;$Osa2ze1jsxZGlvTDx5*vo)k+X059O`!SsUE2htfa}yE~80$(2HyK;rmiFhvuA%k1x*bm3@g2t+w0Y3j?Xu2(8~XjHCM-w@qU3?Dbghv$z=4T$a4G3wa9 zI(GAh@EW|$KlL?T2j|E?VRl=X8aum@k#b);J9v{L!d-jc9hXtf!^0-zCee|9#1kgX zsAy869c;NM#~IB9wX(vSvQpL$3|fiI4cFewQYr?fnJbm|mcW3&b1O6#xEi~r&giC6 z`pc0~-EG)XNE=_KXr^#8t23f;Z0j|IDNUt6yzi?xalRnMQ<#qm|Cq9Fj)=QGNH_|JOhR-UY zhP=~q{%gck$*erC*Px+|QG#hbb_%DM$X;3wx!^~xh3I9kRqRGXcL1N>=oV>(M#$j} zQUcms7Ml_;rc_#dSp);F60weqm*#E7Qc6E8?ViZ>!7B13@FcJW^d)exYV`zTlW&rL zhSZe}mu_ulD6XCnO?~u-9EqEtO=EBOsTP3^g8ZM2RdAKN!y!yly5y0cMEjE31TclW z{yPU>%8qG}<+UkZnXc4_Lvj-}v5J+d?rOSRHN^_+%q25>rzf88dW#R zx~J<+g!CH2uDfC1^;8gpWCM%rhS6`WnDa5v(VP~}`2ZB`E?%#T{pkKf%JZ{zuj&1W zmNu#yf3J(TX;ZWFd>zyXP90wPyVZMhwZkUyOeRy6)e@P}o`Q_r7|^Cpe>|oi z8itHByzkKS4C9DMcZkWFP6mB#4eS4QYxaC6ze0YRb6w5P7q_r~usoRh)~ZqER-()h z3It4QpFktN#ct7m#0*A4S)oziDiFb3vnn22=F}OdAgCvgU2*P6?pHl(f zV_{#!1fr0w1gHEMT2R6=6j~pJtWgzsACq28-ov(+x3wg<~Qmfdeo=&{yeOpS@OxKaR6K zY@QpR&^0bls|m)=xcxbIxvZ^KHVsxDo;K=s%N6d?u~I%?3L{ZXkUJ6`rizY_>*5FN z7{u33CRhQXv%$sJ>X(9y;q$`{FQYyhCI!zAzh(`_wO3A&N-blEQ8L5M-$Dv@NW$)j zicdQt!{xuAS;|1wQ0V4a$)?2)*e=cV=GZ=M2~@CRRFGI7HfD{q0>1NT$1JN%JTY<@kV|a3xuv?+Wb| zpb;6H2SSvzWw7pk>k;)M#ktGYEY>Mt-b0u40E~Q95KO94&fPyqFFs~7E+VHjWC~?%VB66{v%yiG9jf#q zQux88FtMU#66G@F@(4*DX$b+##M0D8?f^q04YCtpgC&Pm`28Ky7qZb99&rz9U`!v= z^|@gC>CzAd1=PgM%O19At<0Kr2E<@= zrXWILmVFG^gi;T#zK>?F=VL5m&E3%n;KoG~8a#y9+DeIC^rE+h`oxrV*n4pyg(OZ^ zZ^Nf!hfnpQ64Lxwb9SU?$;?xUxq&P|h$fIoLD)_yyc#h`2tFA!Ozc(BiIMxvvw>ky zvzmrDX3|WOA57FdxvaF2Nfho^j4DPdzg%0UPt7W;H1n9#1izCg1)57Xqr|VOqGsZ% z_VIphI#XPmU8b&-%s1uy0In zoaqW&ph_R6H1TY`1_PIrIU-F-`666_8T;v2@8(*!8%Tm%N^T2)y;s&@RyONuwVCPP z4kyCVP>%#Cw+tywhp+R0Qr>ldfb)w`md`A{?GX!gImPzTw+wep@9x-2SwW@D7qAm6 z<{tTOJNxJI&IPZ)IcP?_xC*EOgf9KKF6D5R=E6-TyFhvQQqr1-2jTGrei4VReh1iqA9>|C?Emq_sOe4U8 z_maYb-+k0sg)!Ekfu}|^W{1fjhxs5wTo38dYgdTOO3-&0$vyjvM-P{y(+;(3dC|=b z=hw-ru3PA5=Np0sh?2u|MesX~_+0{2a+v3{Po^1f4rqGKGc9Cnw?$AAE9PS8EAX)S zeG1S3GqL5{QCC$8AfdzzZ>pvVRHJkCTg-^vPO!@*j0>~DULDwe1zBAH`uq`63Gi#^(a&8VKveI{GXnL7bk%8)GTz|l$9CF{B`a*$CH;b!M?ng_yE)#A)-PnXr5?H)=YI&#;2~$QeH`eo8})RY z>mk{;vJDrxp}58#`Jbzv&6#B9p*RJI-d1$sW~YpY5`6pa6Sd{50PDUO34|@M6%v8# z1x2q&Vv3-J;PfnmpTjLVCaY&a2MasW7$HncU(V`hR%tZKEWQi)%%RN=>?-Xn{$0ik zaF4AJhQ)ptswEl`DyK)2MEhaD^y?R&8QA zvmW%rhB9p+W2L}+)IgBcTrTsVKB20MmdeCKJHz|T6wB;5otA1^-QvX<=RZB?U)PLs zs5L01jEwrbQSXe|yI3!xGO6p~IeVCEjNDFj6g1Wskl9`Bi#(Vo)QG#{E6x|HM?`snSR z@^?F05ZYlMO-e&oO7o8WV2y}VP$v*{n?8(+3aU{oUMamEIxM4|XXVD)#wRlUU+v_R zX_wdlAk+=|p16^(b_wNRHKAp6YS4-@k4!FVZVWcEuZ7Qwm7me#XppkU2wTZ3P!c)K z+Z3TLeJV54Eiv+-jPZWu=)x^1)d7vKk-hU@*1K8t{IP>{F8!d0)G3YtdX%q7g&*^Tb_lHTP%OocAh zdMHip8yB>-cEPtc7_Q3}A5-+0e8qx#xBvY+(aRL8Y2dm8 zy{&`Yy(N>~&6E)6Hi+EpIxD^Hz0r2Ly zpL7>76B~mQ1ly@6n)gkGEcHuo{)TyhtV*2?qweg#TUmQ2*8^!i%Zx z*BP487sLPkS9A%wKIQ-+0TiuCmXyMh|LGHqno&e-?QbBq0q+yM>ao+#py}KDvVHRK z?WW3eGUZ^x>3BSq-oBE_s#70GI zKk-FSfJwTNc7r1>27DWTh_TybsUi7{jU8H}8w~P{E!zLMw%v=d7ELw}v60-gIJibwq8aDT|!CB&d@J7Y?w$Ikq>vxciIh~(K_jrmfZpwsEy~v>sJSWaLt*n6y-pIUG@JC?(;+ z3ON*6IA~X9(rK8jMj}Th@~>!TV04IRzwtG{c-R3_BThemZ`*>V09K+)oLM?i;agv+ zE92A|d3US4+)?- zVLcXJ(1Ts$6dV_nZV{bd3nT^b%TK=#_qXF2nNKnXqS&kydGn}m(2s4GjW4*Jf}d9+ z&J?N32ns7ksHO4ah@&Ihs&r$`MZeRc)1$L(t0J*7fLPugN7NV_1!(E<0{m&vBVUX5 zqVp-B?B!gYfjR!)vU0!}i?sI9O()OkdzEC}Qj~>A^QBm0yvDeF-zb1Tc0PdtfkA;+ zq+QXS+ren`Kzz-CHl8b_XbcYT^Bw)3*xB8DtR!grq&8Z$x&-ULt4EoN1hXKPIZ&l$ zrPaSyXxGR}3GfIRdC(O;gL(R^f0M9`1Y7yxVReA9m@nyvV39%!!JM@i^Vuud^Vy5# zT`k|412goe%S>WL2+axCNvf;lvn_0lZ3W9b{SE*`Li|OO;~=EQL2l)VXB6pi`T`}i zz^+vU0Cyd5fk5O|Zu(haxS;y>75D#p0uG|L%KTH0^vz{o4Z@1MEh-BOs0o1(5mx|JQH3r~I}XlBef3^!KIzaCd!W zFGSb#5-5RPDf0pB*x~9~k^C_w8xyYtq9y%Zfn$I{5Pz3fD>(y1I^#}1^Te)TjT`#02rQ+I0cgGr+ z1znUciQNDqr~o%A0tmY##6ymH6+Ka-2psu`FhDo*0$td(pp@`xKtcdrT@l=VJ6{m$ zt1Lw}iLg;d#{G>A>dq}hoM~#7i3wJ^1#%!5vWXz1)9F%vpVE(npg&^$ohYB$pt?nR zkya3C-zaonRC*o+dKSM*yBH5tw!-ZPln!a;gY?Z&rp^p2cY>`4+m2IMkL0J`{W!bN zLhe@y@2m9eo3!kkwCjho3 zd(X<^1@n@7zswK6fu5IKKPQUTlhzc`uvAXk+225`(_u*HIXr_X9VsaRk%BgP2`G zg}iYgRiw6D$yS+{co9q?2OwMmQ%@54sF^uZq~YMdvP2ON{J64-qKLzY20OAaN&a56 z^meRcg5my&f8mIrXb+~hWTBEht_ksjZ6uu9F1aH*)sK4THW~(jk);$ldHZ6OT8WZ@WMi|h*?@{_%^!V(j`z}n+X4Lv< zVW0kJU{;w##YGhDdL(Xms^+EF-t;Q~?up_fONz;&l3)m*aRZ-7yr6^@XX8uF`>A0i6G7L!JX%)$|F-(ats-R*cLl~s?LKl z&oW*PIic6d(=7Dlb<1>Wb&72SaF_C7ExtFoYjn<(AUT_4%ctfqoH21E$DD=5(0MXA z3;cQb1r!UjE}7UMc-f>fJmaw(f?@PN#!Rx=je?>IQF+b^_wc#1ElkfAxnw|oxb@U0NFBR(IX=M6F zQsP8rLLP~A4GRs$Y_Kdr_NAat(M<;m)br@5Rn7~lbxjd%N~@t1EA2W59=n|ahfl#V zli2yS&wp{ndA<5~hT!SpwGjb)CRTvO4MtR0e|;)sZ+(uBP%cjKGu1a` ziEqGFyBT``l4^La9r(SUld+1FpHQ%~*GuXP&vjMM^$*~yN`SAsjn*pr;m}miZo`Y# zTNqm{h$mXz`j7?V%6(0s%JuyVVC(JrHHmiL$?LtfMqq2hqEc`9S+&@%3Tk@IoGo0n zv2SItmI(0WNguS2=pu z0(A(!*+&g5`i5YGbhGu)Ij?$GKups~<-j)@FghvKKTe<#vk7W^!EH~)Chh~TiMO0! za$KaMIK4@(jpvCdz1T@~2Ke6w*dY-Tj*v;Jx-D8LU#cB6Pbvjpi118TxI1c$V2&#Lau~0 zAxji(NKQp=jKu?44)0Mp^8P@HB7! z=qXdznG~(a8u(iB zB?or;3-nDHlrmnaNwLll^PiaGLyvppktfT0Fn6SfCH~G++coEbw)f-3rBmCWs(vyX zqS$!gL!^ol4~rs~^Wx+tFv89ZnSZ@F+)BNKQ5-f8z@6_1Ix(1>_kwL@*-4yE1wddwbvdX4QDo?Vdy16`n**7GlqO(;~O} zdnW(X=hhQmmhB=$1heS3-eydczIUHHUNw-!yEO)enxzfK3=!obBfFSFV5aENRy~**DTwvE_T_rZQHhOSC?(u zwr%q%+g6ud)9;))6W@vWX5z&8l^H8nuKj04Wb7;VeJ?`OiFW= zT0S*hC}$>Z|J>PUoNZArApQ{orc zBt8W#X7v&Y!^Y<(tmD6s@7Z=xaLLJ2c0bZ1049+#%As%nT!Z}1d((`~vTT;`|8ieRIA;dj;eE87q-vp&FJ{XXFGUyEa8>68K54!?Xr(&pz+J zSU+n&vwfg`$RL4N5ki9tb=7ONt70J^4D#*s|M^h~QJJaatdv%*E7uzvKP^A4OsiBc zw^#Sq{|bLEyU;I}uT;+LVnwu0%zSCa-mtP|*8yxc^Uvci_7=&W!{p2Bp7}0B=Nuf_ zc{NZCidjaIGTsn0%?#47yPpH}4+$@j9}P$mmL>PWpXXVN+Tr{{b>+h=W%o1Tss9-U z<#w0)2_Gx;iR&419ucn%-NH<*#W`-dEm#%kxh7q(YUI?Mof2Ph02+JtUUAHfDLVqp zlKji*NCsVAC_n4K_r2|dpJ&bHm0 zl6@rPe3tc9O)5iJEp=?7N)eb{0_bFV1|yaHhbn)Fdm_4NeIT~`;%Cc{?>Rg%o4)f{ z#7LK$CuQ}*j;Dyb@>~2#4>f}O6poF-eT5L97#lTQLEe95c)51U_Gq>T<{HkV%as{* zU^-aC;rI~Pptlv7HDX%hsflPV_!IXP`Ux%BTlmT26S^k`2L*$LjIA+9L-VV9PPjX; zdeCY0U_4V|-Ztfuq$Rb)*RnsQor^0DaWO?r7fziUErw=ulF++ydVO=)BxGwPcIt0e zAtdPooGXHHL!X=kBVQ_?07-jWVXm)i8N&ADhXCpcuxE5k_(h6lOQjxB-bz2I9{Dfh zr#cq&BdY2R-SiRu1i%_=w+pz*U*I>i&YhrT<)9}(wBGapz8m%>x*lN-OBmcB1je9U zn*c-b*k@*TA(5_-pUlqSq@z(zoxlgz50MRm(h|qa9{er2&a_!Uw$qV}u;Qy2 zgWELg?$;YL-I~ZE$j3d!NT zkfRJTq^>|^>71ag{7|ZE6~WLA&gkr(0m8EA1?>Fot}*!s>w5Y{0iwm`ANCOCO2Llf zAWiEu&J9gg)rLt6QP1M)W)dA86%8bq6UcuqLCL2RuBRql*_sI6r887o(_}@z zh3f!TOCG@@l9Jx}MY}kNPh+YD{n?x)Jop-+e@ykI&*Hojmj24-wx^|OhgRD=Ozz>n zvdeMKQ2I|uirRx(QqcZwh3x*T-vZt(+38`3IJj#C`{;F;B-Jhr9Y<)=pKc3=Y8N3bRysePjd#2iOC3!3{b9Kq|)D%^n?~iU+bT6jWGbD$S-xskpTB2R1kP58Nb(=WK(9! z%=}mA{R|!XUX>OO-Dgn_e1x&6sE$YrZN{=Q<06zgD%$l#lP=KULQJqRsYUp8y33j< zfloeyyuHmNCe2t09}h!$t{c*+?RKQ-z1NX<&)H#2&RAHF**i>5eT-W~fuxteIG6|F zmqPmAn#0x8&#bPFnNR-XdMUHFJpA(=4vukczQz7x*4&uB#Hq8VAR$}v?5L6Et|q^HhySCY2+v0XL^cfdla>eV%aKujml6@~;v*R7I~R!t@uLyL zoD8jhZnAe=s7R?STRCC3F$s4kau+O4K2Jvi^Qzi?Cu zeKDVy>Kol#zCZ?^lP$Sai3y%qpxPrL*(z7$mjFEnFe?l@bo}iYGQte64F50!fY()q zFNQY!OQRs#F?Z1f5ol2xnf(NVYHrH|?FXgjI9|BEWxH;$ZjCGg z+hWo+%^!N^dS!ZGBy1FAp&SU=} zAy7mk_Cg|_B)y$@YilHEFb6B>_lWCfg2RT8TC&^Bt@hQVp-wB1$A-|gOMJd<6U}xc zcP6Xg7qsQ_%P?jbagqG(pQkDN_YIVnwktwQh53oQm7S*J(SdGeW@6}tB#g3TJvW%c zG{(r|VI;{{_#mUVpcbI)OD?GlAnE%~V0Lv%t&k+=o)K8bAXKL9BRUU3iuVuTSRmxc z?lxqgT%x67NwBt6@{rfEwytT_f;nqi*|0)J*AE$gm<_7qwAk6jfVZPFbLN71V-% z5)5Tc5qeqYUw}u{^`8L61gqM`*8NiX=~e~I&F`7cjC_C6ywlL)=z4#EmPVNTpgP-k zUiUvXwK{NPVD_abBJQV-$__3O(obNom=A;CM&O2UYUOl{ws5@)C^jneY;NO!fUc-u}~nRjf-y}%3F zcjz@gj}t*h{KmK-a$C@|!@OEHfA8G6b-qP(P1&qK$+`-;2~U!RPckf*J$=(2qg!aP zOF0D}G>WAKWKOVzfQy2UfCrsaVF64yvk}sWHwzYzAcu@7B%(NBj)2fjPMpxhv^dW0 zASE?y`bmGK`7rvsT@m^!DDtG}uv?oYGbbY)nw0s~)MPFF^+sA!J_DF}}1 zYVDdVn76E?HkSN+-ZX`!`kda4U*TXxW!y}Xd)0E)c^b$uoViw`*J`3tL$%Um%i(uC z+#=G>Fjgw%5on`L*4y|1;di_b*pi|=ZX~@Id6AzCg{hmfWzlE6*ZrwTMSYVpu1U|b zMt0`KkE)n0didDZiv!%6yW-dI8$5Y6&cY+*of-4RtsbH}=PVukWkInTbNbuSRAf0@@Kn$cR&8?&|J z65(sU_?yBPv=oA0yQZ*uUPb$w_N4KSw$fUkNWvu|NhMps#4~Em;aa*aoL+Di{rK|> zT5{yr!3@12JoQ)O0{0otGs4FWQBkVmZMi+c`YCfoH#L`H_G*$fA zu?&tsEX76zMeHJPha+Qd6|qGK!c)lG@V7pQ@>JeKNK?)TG(Xw^n!291^j)pyR z{?Cijn>~O6rXu_96 z?P*ox4Bea^04_&Be~sK?TJ-q3q~Tg%gGG%bk6v^MqZ#Dc^99C{`sti-9$F~~TU1on=d%D6Vk4A{mjP(p~` zO&}=HGuQhgV1%?iow$!YQaM!aZwaXK(MzX@Q6tu9(5dV=Hxj*hCRK)|vvlp+d|v~} z&at8qLu z<48-JVy%0OiLQH&%3@a8W-*lMls1A**YanC$a?q|{xnSmQ$0)_b6d?t`!Ax=#6fVw zv=*Y~G*2?wxz4&EhIVC%mD*P@OdPp=17@C|6xyvrT_X2AMiMS#*zsq-B3kR25JvDw zqV;%7d87{q3U( z0Xn(7eCd#sgUrwu;W{-I7%VJC$lxWMHSH#xHZ42mHd^o4ee{0(K4z%ChD@=2d0QcF z`t!mx(5@*fLKo_2)DNzkYCqybh=^^!Rs^1AMxlZrc2%UIw<8rL>!3WmXs)|GZu{bX z083U0I0}-XcNosr1%~C~464H_pH;o|XW5rl# znAs-^1$eDQuFMy+T|NFiOe8xfu?%!A-q1e(9*K$s>orqYx#;TGoq4;;@~F@4XY2f; z>g-DAVnyfT`zLOc;hSv?Ca3U)bJ^`i?|}KoCulD%7@k?5wLpq$+nru#;O&&O(hpiJ zH2s0D7^pT8H%p;L)qP}(Vn+e3oA>hehOVj*G3I>ZY~@$WBiHp|$s7xh`n%J)b}zg1 zZsoglMX%ImZB82J9A{hQ7-xQ0*j1$$oci*I>!?(>w|%$%lfZc}I@RLZb~{m3pJUO3 z-k9FI#@6`^WwaH>*5tWTZ6L%?Up6?@7KH%mMbQ_$i->>ncN~~1Vfq*TCAo{qIHFJ zuyvbt-DSPB_tsOvCC$kqY1T-3l%$qfeX;~;LSiY3s-(7J73qJbGlL2}((U+&>=y6V z@>i>S3Suop%}0>%l0-QjIuIw}eBfvvCLvtx*}onQ-VBx|XLSx0i&Y!4Dz@N4jk1-? zS7>%85VcM74dN2&BqA) zf(e8Tnm|NegxmW4hAJ6Ue8*59) zjh#7*9SlW3I#QhN&akW~cQ}3Hu26$(J!^7a<~N)%)K-(h_p+6k_+6DWB2$nE{nO}w zM1&unn$>?icIMXRK_R z{91OWUmAUlaFNE0*8m#=Y#96_W!Uutv2XSOS^9$eZlp^Ub@BMlFz~{ymy>^~?A-)) zJubC4=zF73sun|EF&YEmi-8#rJUEls^wm(^;;=yWOl&pqhnT~TVT>b1cZDhu^)LHd z+B^Sq)#pxOb1-U1aM^T>(1Xp^eijyD%DYW&j#(?6_x(F0G9uj3 z4z#RmOWgbhrVlx#zsm*9o5nK_U;sP$TG>fr&gW{LOmo*$#WGNf(QD}2e$K!u%kZMw#6DgbzqfA_(T@|Umc3;h*d(Cg>B<(moul*(EUB9m)fqCTcw~KRfNP6zR zr^0Go?&m9xPhi*XpSzd3M4ObJE|7NYv&mV4JIb4C)G?X*&g>^Eld~is7KL78p~Lhu$$5Y-FI@SJPX!w=X-6R zQXG5bzro<-74a|kMh@Ldg9>~Ep5z3sPzR$A-&L`~`i4{eS zpbs{cMXrjgVmf>_|Ms_o>e2ZLQBqnaXdsO&qFUu8qE?2q&OSu)U_ylww1p&0jFjc3 z-V|!Fl^W4nMoQ5mCi)aQgPTe5T11!_BU0`At;seN9Ze!>!WmYX$G$* zLnQ3eNxt)O*gVVZF0Fz*oE0R8!7!qtze)$;9ma@~wvy*G%(`gajP`dVd#m4)}jJKLG9DZo38XQP=XT$m9Xw0d z>bISrRLKp0oGiQ2m)o7uJ?Sy-Rc+soUw6^oOoc$Ms-vl`)^&gQxcVykVN)mBcka3A zi;jpMn>sfg$6T+bsb6V&Z&t;qliaS?`@MKLzn%KGNvWos&RNg2o|x|Qqw8hdPx$_D>JVGw&l+d^WFCys3WA zhugXy;@if=ke5T0HF_WN<-n7bJ2-&h({HzjLiaHYXT-};+v=aTNr?>nHZoQ@Da!BH z0%cYZRiEsM%F4ue={1|_HQ!uTpQeLJQ6P(4m&+pq-NpE`xt#XQHW7t6CEEjlH#gtj z@R=A*IZb9sOZ_DpMrY810dd{L&OxpzT}@edd^+w~zcDvJm+l~6FX7gcC=46Djk;auTh2FbyOz1FvDkC~N)A_TgnKm7(@kgub)H`v-ZrxR zVh-*M3LIbXF;l^RbM;)ar`2sdpWu-1@+)z!TCz=3QqNE`tP2m|?vGsV;CLRNgjw!K z)h*s}>fU}IEcQB|n0}bkm-LpkdgE?YoezHA&c@=}w9p7(?|oVxeYczO^bji*Jv20ef=6=HDK6-*GFuCQpTRe^tA>LrlPB^jO*V?&-R`8c;c zb|_veUPIU9%Ps~t2BK+Zqtq%!fqsTV*dCa4^OR<GSa0dZGiSFgMNtzDY?$H}Z~O{jR@pPhJqUn%3s$`_phzwK6d)d{$j4(sK1V1h>Ek zt(-ab!_7Dyuk*J^hMuAT*~RgOaYK}TOkOHW@4mnJ)_uzM<%pHz z2P7JkT>7$QRo%Mw+5;71R{MX3#29Rr>X>k_=n`id#ckK@ryZ5izmKK=NqIXsn5P<+ z?dFnWo=asUWH5NYHAtL^9xO`ZGjfJXhUg{s(_Qx6^7ONZm^0`PjDQpuSzJ+4Eh5W) z!<;9VU9Wo7+ZimVud zCC)|v2QAD#ZcBb(9($WpE{SiP{~QjY7RUCGxMO(Sq-n_2YK+;)A1!bJRF5Gqz_Qlq zLW-%^drly!^h(BXTng2eBIFiIoH$*z{4QzttuvJ8QEJGz)zD!g-&ptwK)2&!xcj*J zV>)=UGqxWEo(8Pc)TD@Wc_z^Qrjsh;{iwHcMXeMkHLvI5QgZjPP#mkTE=w#!Hoz*h z6b~%N9>GZXQ7WKENUEB8n*9?@aK!g;-^=`=jo3o=Zy)9uHoTee9@hQKHRa7Q#$b_(Y%gEw+tT!g? zNG!Ky=q2-|{1IJ*U))|yO1wY@aF;TZ`5^i7^NNJ*?x*gfB$=>h6mzX(SPX6=C6O9c z4EdW#l2H;ZS6mR4pwwFblT4UWbahhk=TEpe--opU-eIr5fa+iw`J9ZP_DNfv)rWNY?mp|C=u!YE z*a-OIyuvGnv$|$`9KZ1A6IidBe9m4SoGaPA`4NYjZ!cvEcH%(P<=`ytQ!5a*qOLvG*KGG#qlBTZ%CHIOoo+ zEVzeutE>sV^{|4Gfk@v(qO%n+ZDH$NjVtwB_PXn+kJ#tfx4`FVr7yDB^;kVy;Q$#9 zRc5u{Y6|)3FyD)k9l6yN$n6<-ocR@#zUio#Up__8oGia+wSVpkVcbnF0PFa-gU{h& zY3I}#-*TJtrnz7(x_;tSNSJh z_WGo*T<|l@Gm`*WO>5iAt#sx&-m>eqH^m>PcFu-o1$cQZ$1d>*0IM0RYpX-w1oDX_ zPJ6Q~g(^xTCb8*XMQV#2jZGwB<9(gVl2%=u%8i)ZFfb28ClbZP6Z^?Cn9g%Cg1%fN2 ztr_Kp=MBG=3SjAd$;dl8OL)!6cm8Y_Qp1MLD2RxXdjh?ew=eH1jM-b{W2jScZJ7zX z4;IVI0syr(Cm9*~S{$ZW&E0EH6bOOBIHkHorHbaFaHLj#8J}{kevsR`hzU0H3Prxo!LRrk7oF^tS5`>yx#jNB+Rd zRW0ZLjh2F$;eS_4!N$q<|0*e1SlR!V>CM!XRI;Xf1{MYeW(E?ovNQ%jprc@fq$#<| z84$;93=9nSrp08X0rSZD`6tX< z!1w6)D9*~iFEUDjEYYE|B1wqnX~-ayAu#GQWZ*#H-r;QF;IgH`So?_xJzLE!F>Zy8 z4&H2gnIxP8LfrYX3}KCQu1tOIM~-6B*2E)T1n9`;X`p~EjPsu65(5Q!E}|xONsS1w z1;~DblBr>a`pU4vi3bElD`?_FutDGx>Lb$)_9OMrBcK@?u0fJ)Xb5AmV7XwyVFeUS zSXdd2nb?ilSQVC5504Gk+E|TQ*pQ5oP=OrXF-|c>Mp0!k!BD{jF(h$jrKd)=XR`Da z56?QgqFOjq>}ir=$L8Fz{ibDlapsrY*6zsyrj0C^9Bm zCjydK-2f_0O|AgdCB@Zv!zu0dyIs^&%|3ue?~fL!3p(>q!VdwKd%YB+19Lbt1*mRW zv6+>TnGFdU)!=A!@Qj7|!HHQAc$0*OmWD^r-7b4NjRXY~6O^EE4E(r}0~oNr?dkAV zapAPc?rjQ^!S1nunXU7QKE37J^f|AmzZbqVQ(?d@Dl?fyj>nVJ6Y z7`?2iiKU^Cy~n@2|1=jHBNHJzC)0m&6%3vJr4lkT{HLfAA-$5Rv%Ra6v8nUFg8!K$ z{x78B>0tVwM03;sG?=NKiM^2(%>O9;pF18NLV7Vv8y8cje^(nr7gG^aV|$Z-Z^)S1 znY&mJvM~Lx3ZLv2Ey=jU@s!?|x_Mg)!jb*nSGAoDe3KzDS71Js52QjO(oPDc6QY#Q z%hMV`rIKtmuPq5e^yrws&$m4W^_+G=GcUW~_aB?`yI(H^LmXSlei@?cCxA_|$6&(1 z0RvRK$a(ofpCrf#0<`lJfak;dhfCw8ZnF8#Ks1DQhx>U~o%;>Xrm(FT_g()^EW5Qf zexttlW?Sig*}(H^XCqHt+fVit0|Jl92B>@7du1iJeyJkaZs&K@b(__ql(3rGua`qI^xF4XNe z&Gj4)dtG@Ja6e5GL>BZwf)i+f%bDFZ!}ChEp)T@%tgCjLxPMm^p#Q^}dRqXh4FXo& zdqHms-jlvrWqN)Ior#(R+LczJ`b$h}x=ryUIQ?Sfv2kiZFZ0F@kvlP?=sd8v`mV?U z1F$JCh*aPa)nB#QaHzJ*=1Oh;_SzZTh5%Tf+&7z_E#M**Yd{ag%saoIjN_4sQDukq?UG*1u zzV$2XB}8DlwbxcKd4{b2TSWcQ9mNyxj&e$4uOrB`r8*@$Tp++rIx^aua9Wnn*_xTo z+U$b}XWnrsvV@Qg&9xddFbPq)dEpy$7uZPgaL8jksnT$-^hG4ufMiOcsur^9E(?%l z`sah)fZ}$3E&DXmdW9$*Plvb9oK5x2XXD;qAIIF})Hl6vFOQS!PC9|95a8kk@4&UfJ~^HG%l^^{dLT6@7AMv8%WD`>yblUyDP99uj?% zQXP(7Brxg0-FoMPxfWyty>g@cH^8E^A%_k2v|v?0&M=1G<+Gt-l?rZLZ^U^5yNGv zo=eIwr3){XqZWkUugK=-cC2x(Ep1TqKQp&mfprx?T!h#qVxrj=>ck8Ul@!sqZwbv% z{_>3vR1Skpuz~+1A|S=?*@j@cqDUSD5T0}1M@J!fr9q(qkjl)xRA2(z!Khnst>aflsR&ZdoiLmXw5#_Dq1|4s3yoE$jkt$$*s zVsV3^Z|LZ&f@|Cp97LAlfC{`nm2+`*h<6A_k(;z9{Uq@kLF0SN;gCi%y0fKt zyf4}(=#R~hnum4KHzgB*vQr6l(8V3oyVm|0;mm&xjB-3VH6+P5>D1};bJ3H_cILap zaUlYQuB^i?3<&jcJM%~BP4r;hIG&=(zvF}D2;lEqdmM{lb2Uj5Mnh$?=OB7uYy0o4?nKc2f-Z>rA# z0$zjz8=|R1AMkA+_c87Bw>_gRSOBCu<%K^;_RPrubt68-C0JlsG&Fw_t>2z#k|p=u zj*F;XVi~0XxP5SE+7~A%iYO))+_xlL*h^zkDqlvL{JvQu9gpV`>Cnh;t8LGmMZYJU7)uEVmW%7fHq?9tb?$S%TV$PD_;D}Lazw#_I zArX#^eZv=+=6rK;XwFJoTCr!voliD-j6(|l*+)Om_rEpda*bf=l<5x!Vee~18387I zzknZ2!zXY&XMmKhIu&Ik8PT~RtH+=)U@`*dtv9XVExu_GSe8J5;W8YX&DCJ8Zrj`V zR*N>lQBH@7-xUi{@}^sEvbEzTml&_1H7((M3lWEWu@oQEU7p;C_zB3h(uefwl{cB4 zM#Y`NB1BBHp*tbpkRy@0&~Hhi+M}B;9P4K`Q+;JJCI1fBywZExR*e(y|H88n-r8<8 z)QCr&o42YoBciKn<*)PxAvq;#VFtSNg2YmwIUi^iYJhC+t+j5qc6zGfP5(t4TjJ?7 zIG9shc*l(iXE<27!RnS`*Uh3~5=+O!iSk#_4P?xA=dRseM?!D)|8YgG(&+|Y>* zn;FI&z`jGPjP5RggeVN|+$9Kf_x#+R`q&b*Ps<`xC_j=Q#`}Ha1=HuoF2& z>W(`@T0&*gAMk&&W5n3ts{zZ6cMl3}lp3`k0?8?50u-x2!c3(OA`3EP8$~kHCknNT zz!Hrav&Imq`NI}`19sZ!n-aIq&LyPd;Ih%?;XePkQ4ZZ$%W)5 zjR99)hmk8hqoVLm8@HkmYe-JW$m+@j^3h5YZ*E@;%};&RBA}JgbvJ}3iG(~7GA6D@ zLTVaGpK{(y*72r{5w*^W4J_>8F|KRLgW=8v))vpSa-3=jHS z6pUj(rOxh>)UN&#;e(g0wnP{G5|7Su%=oWcI}FkuXLQPLr7PJf@Rd((sb^nvgw|DG z;9R#E1Qa1s3p#6%;SLK@7E$$RU3+Cfx4D|(MOlQJST0zk%`@1%%Sz~VLn*hQg%Q5; z!-!I-dcHt=LcydC9H)41z%-KLF1J#xOb&!x5`z$8Y)N^PW=Fre55C{bBr1{Vxr`Rf zn3jq@tZMeHTQz#ge+kv&yo# z*BRnx)mRMAdoj4ZsNo*ymm5xM+gylyCgJIBkEVD3;vbjJDr^PgVD;=CbRdx1qbkD; zN%W|X?u(9ZQO|R8FI^Quy}6Q4Mj!;(dFBBoq z`1}Jh%pYnSODF>rQefUR&@X*-wdmxDFjZh>&%s_>x8;-^yM?kK+{=(i`c86$kmlBq zp0w%rth}A~#V%XD{8xk07RL{G@LC55%Ej4sw@#_&64&S-+0G?~uRH$la2XXY4XNl%3&ry>)!l`}P>rzpWt zh8l`X7a(Xe_aB*0`bV17G8>TV@}R?@)fdMH5`CLGxepn(sW2htLy{XP%?%(2i=P83VGs9E9{Q@QWS#C2i?^!EG~ zDpNv(Ln>qL*5?D6$CEdI93l~#hI0603Pt@C(A|JLa2H42YP!HQcladcRyhs#La?Qm zo?jty!(8gtA7|H^d`!FG+bTnGI+x0EH2EUO(CB0t@YgF^hnv};0N}~?LaEz&=lQu6 zkd?uY2$BbR%NVos+eTVK5Rdd6l+^JeECK4mrgwZD?ffuJ$RYX-jy(gYKBkVFr0Kc_ zPGvimx|z!@P{H;+uoe#s#mEW1;uM%LnZ)aI+WV_v>$6AsWYPnnxx36y@hYri+}sc% z{s14%d^%*%z7Gc!^0-EMus;cUiu?9S-DFAvI1W|>{D^bv@4p|1RY{S4<{f-$)ue@|g`*q!Y7)(UVDEm6KiM>GSvL~P z#o61^5>_s~(cC?XDRRm$XPv0yJ$}V=ehv|i)PI;?i?O1_t9^O`<*$4VwzGK++f3wD z?k7ndeLchSiCzu`6fD?dG+s+L{knlqszImBj(+{Z)+pBf&wfo&?Rn7Mw|J=CDx+=$ z*{sPd8v-A23)V0*(k-Ej+_p||-UgE~W#3B*T;*t4zPqF#_6H-xfHit&^#|(&uCu+Yz8y;jFCz%~<*qH_`ZkIYn>{HM_NFz}ezQ+ypB|XOzScKc|Z?Lm)>Ax4F zr{`sfjzCCcu;I0bF#n?+|C49@7lOj_KOo5e5kX=7 zf1t|$2SNEy>HoRn{~tkNWn$&{uWT>7Mn}?pv%PWWRNX@|2lWA&u>UtV`L-mNq`c!z z*ZJc$IL!hE7g`0GDEDlgzrJBUiB_RU?an9>BU?5TMBRy2MUC#~MXw>>0RBEmd78Na7os>4Bzsju)rPH>h zRMXl-8`>2g>QYC2Dk@=$)-jw_*Tb@mo>$A?6TrQk{#cCfZ^Ln zD`SGFkIt(zlIzRv+JDvu8<;eeRJog{qJcA7=am-MA481{y6xNGR9Y*D&-?Qn*Nr!m zBW}~)t4Ajb-xrm%nec8?yQ}1PX1Nd}LiX>! zO#M?iG74L0QHDg_hscIIQU$p6F{qC>+%7{l617)La``t-5efWS5fMXrwe7SvJ)3MU zZtN_r`TQJI>PJYC8L4q-q{)8EZ$%)NWzl&mus^#ib}c*rZAn-|5R8EMuuK8+d#UR` zW}+D}+G9QJVSm=pZWf%Bt@Srbdbiuh3cGU}c-mRQmJm(l7Mo{-HcRz77AL<_-S^IB zoJvk>^K~!9uv?wNUXC9BK>eSLg6j-S}t6} zxnD9Kj9ALg?l1hneo+CN9pHr10~DrC`#4bQAibs`>VO>LnTqH}ePT$1r~D|B##m)@ z&e)i(xpt5xrutCgTWfF2>rv7|>XHt7ti>g?)(#UnfzP2upTB5Tv!Zyg%F2Ftw^v}-D+J8nX0JB2w|@xv5w+lQ zT)N5bMxS{!IJcFsNf-E%>ACsBuZXsa__NQ`G6_4aef}Uu z>+A-B#ndp2?@=o(ak-sx9bts?rqgl?j_li}~o;i?BwTBs!yi!>kCP*2HAXwG@qS9N^SIa885|t^+XD3~y zdWly$qun2lSteyR@pafm?nMpTDv#zgy<-eSsD+9~pJw|gFX~+bJZtC10CA1!?==lV z=|LX3?X6b>J8lQExFL=SI+7(2gzemM0%qL4+V2&X1|FDiUTG*^?DJ{UyAE%aP-n#_ zn9+a3$yL)@Y77Rruc!R!D!W4XBqr`LCiyLPZ{sp3$(2cr7H*eKl^5MgKt`d(a!6KM z)t8IB1K?)zxvsROVb+%g7_aa1^%>3knJbuaDnG5R2y6jydAVN?(`{yJTNLv2BvZm} zjQYGE4|QiofmDgAN*su#`xhY7@uf>u-R*wQ9~iLq9ib8<97%pATsX@;;f z6HgD7A^*)Z52W=UND2=%jNqXYPot3%PJktsi$E=VZRQx5*bmY?dAB7a4G3@gH>lb; zoJ2%K6%pd2a1B<8Y`)Oz%BCU>Lj6H)`A5SU%$3jhPM%90RO`oL^ECKo3+OiG=G;G2 z>)E7M@#FGB%TW*3wvj=;)lyI}xK4}Hv=EuOA*IngOvyHb;G zsj(pezDoxO;c3ucg~cPyabm~Zd{kN+fj(qnCptnu>~dLbcl$M66ebhsJfVZ;YE(vK z_!B>!716{$*hh!i=j8GCG#URMbfYp3B!Oz*9`TxLavf=7pN^{CFy1^A%oxDYG6k65 z)UOHpCJY`*L0ky&sAAd}M2ip=+~hF67skAnG32bMvkZ3Bm!e}hs8gn^Cyv=~Y#ga@ zNy<(r%vtM0O=Z30NQ6I-tkq0EQw4@UN_||k~sNxEN{>H=+SoF z#e-ug?bYMLbd!mDL!$dq;6gOww?CYCx$|H827L@35`$bKuqO=G;}_qW;H`rO5jgKz zYv2TkafqEl^DRjI*{Zb>x!-pRmCTjFehJTsCXN2}nGZmdo8nzS;n~f5bL@!w4HZAk ze3C0TSMLE50uYjKM!_anb4Bs$w2cP143Cc~;+xHTexz$66h8+PL_yXs zt5I949PT~QNCp`xvUurjCaPE3s;Y9sdN0!vbPM_`*SVIkNv}TjSOGosZKUa|6aiVL zO&Wup4li2d2*WZ$vzm01J;1;N_d!g{-&p*GAN>jG`S<{8xFRHI4eN@=hB6~q^1L&I=IQ<#knPB%yaztuDCLx`tGsu#Kum5F>ibD^K3M>%orLH3QL+103cyHXkSm&E(!HQJGYv1vYYtV_I`zxU1~b!=(`z zWGY_Qq?X8ME42+WC_tMygFzuA8GTl-x3jAExvULs>0-TRaN=KkfY`MRe^k3=a6pNoU@io9cv3>I%EqcZ#ax~c{2 zZ>j#)=;jpr>(mAs#5U;0dU95+yXJ%OdzC|~N{v3XvZ3X6SxQy+FwDD52kX9QFylR7 zQzkuIv3i*C#N#*m%K&Uo&~gWX7~YqmWct_i z+xRhtZ7E;wt;#g=O9sOVAJSZI<)aTr>7F53w$#rHT;}41F-CW5Cr2S{qrF^7(_V~n z7qX5?*&;DRB%pKppP{s$rdur>%*MeL)M?>I=yp+Sh<()QKQk0A1ktEDKM)4u*5R4C zxhiMp_YV3Xc6@O4zHUc&zs^p-k2>s^eQPa09u2?g!RcLmG`d1FS5E{f*@Ed~&JhlC zG9!)3liGHh(@Ga>^=o2}%8IH^YugE=ts)Ed6?D!n+dO+;3D47RczAH*!-GjqSU)bH zMv+XcXiR4g!=knxnb6Naejv0#9$$V)Kkdcg1LYkv4jjBxlxltw4Mw-7$#VIGiwx3Y zhO7%xl9IP`+U!|l(l!J>b>y)5u$1kcj8j14WB*PFFUQ$sof4(*Y<114t1jb#Jl%=O zR1PNx#_8UIPLHwllp^ppj9Vqr@-j6-8odK@lz?&u$ z&X6I9>+ z35q1}aXIdbw))3r_T?%FhL=63Gp98KV` zXB{63fdMf@Av2rR?sb+aqxqr?7(|#R3T)GthM!-aTjEe7DNNANaq&=23?dxk3FGn| zgCRH(N#4<{+8w93pH81mc!p_;>m(`HO`819Ju}UGf@(%7eu>T3FzpZ`WBeb!F@$$0_q^QQ!0HTVAb_=2#Yu`kr^tWNFIL2aVKB{vmIkyRf=Y1O7`Edt)F zzsW_ptS(2ECpRuC84(IOJwDD8I*XY<}uNtQ#33sQ^=tTX)4; z{CE8BJ$~1jIGE+_q;I9^GdG`Tvi5>p`+4SVERdaN7luD2R>}p=Yq4eY39g?fP0UD0iaE?Q@lwW1 z#KSp?vR{2RBr&ZoNEV+?z^i{iAp?K;3GE+`Qic>&lQg_Y?h3mvmH`0##+j35T}$Uw z_QqGKWy0Gr&iqg>d zv@Oe1W;Z6|9bcPSMdkSd&(fub(pE)fvl2fmg_S5l&T=valX_EdJ#Dp~`LJ*t1WrR~ zI?tQpU=@sp*yzVZNy(ZIFtUgTu~;2K?h{tl2>bH&m-_>L(cXghIZ9JL?Wq$(l2y2( ziC^Zvz=B$ePhQXq*5VcibHq?;(eLwSE##qz5_j9aHHW@XrUa!78BA`jFDyVOK&yJb4Z z65SpMsNxA?2`QvoLgF+K_`sNAT8E8#gIY?iWFk7S^BB7K&#O83bA|OPinJWzHkFax zXCv6G9a=Y}&fUrlQ;I(xUVlg^Uq5BE+C;%td1nT+Nyr5X@O9F99&brrb?ANY(edPF zO5BhTIN5{dxKJ(TS9nW&xV!~38fC5;A;k0G2RC34$Ru!S8rLy3ta+KUGUyd`E!B+c z8V&L9(bggjx6wvcr$T$`bw}jp<5om>FK)iZGP_dym+{WM$s=Br^T1C~KDcX5uEE4J@*7J`pX>XuD&8 zi7eS&1gQa%Cnf?AZG%KjtT&Z}HM=ikEn`Fj91U*o1jw#V2|JQl0@TE`~Qq1V-5SOT7c2 zV;s$!5sF#@;xBF$n`YBD^TYcV$&OoR%3+~#(~Ai|vxJ->b#dJ#0c9EQy;L4UF|qw?9P2;)rvKk$w}00D|B>C; z{=tj;|0cWrW9t7ERLk&}HTRb#=j7~YVqgR1p53CY;kGH-==)N)mqC7;4IyfG8oFc5 zm3oBww$t;QLi`9zw#8FdTdr1^TS0)1$niwz3jS7A3*b;f?0< z-P`W-{K;xyTsTM)`*!$jOH?F3!ua{epm_MumG=$Do9ympY1eMo_FblsL4v|t6q!jv zecZ6+sv#7QeTvM_veZnOY@p1dFtGU*(6@rZz3S##^2@l&S8K!Z)jOr6!{*L{swm%K z&o1>$C*wHwzHg~f*Yu@VFGF?R)55BNVCEz@hXWEz2+g-gI?CR2 zR2j)ToeA^QSv7CSjxH#xOgk;=vW`Y?Z_A*t=4|vfZGKI*ZP`NQa1AM(iVw|u`dr2! zb+fOEMUUl7a8vKXuvSgQT=iva)69?iVMsn9Slb>WEErKOY5jfuWVdJnhiZ55zDZaH zPiE;UShOH{&5pIh<2kQaM#Ruzuub})w)AN1QfscC36d0_-9BBs71J#&xRB7q5e5>B z783UmH{5}8vzssZyk3dC?zz8!;nDjZgcj>QV#bCDkw%WU|!r{z0 zxYoNT)>uI91ydGk>mew_p>;-vCtf!#)mObUYsjolTUH6%3AU!TSq$~3O~!g0H9}Je z@huarnuy61J+{tDy-#fqDmo5{j@9a6TcJRQ^!}b}CP~0|lWy|aXE$|30#OnNn*GKl z3w>s%dYKE`N1D!phpSteRL&|U3R(yCS8B;~5m={BC_mzu=VUm1ToaxHBFM(7#otZ+kt2L4~@MP8yG=H@#L7>1ESKF@02x z=cv=mUz@%Lfu+b!Wh~*xY4kLgR5fd#Hd&_DHrFh^eOA-WHm^XiP^gJQhwsVk6rE#2 zh6Dmf5|JIwdl1g=hv$MBM&4f)+Yi~Lb(*Q;0bjXK*SjxD%t@Hpizg=r7JB!3HrTlB?v_&@Qd027qUJXH9VKId*%21@JA>a%7-f%r>z;$Eqr!{gD3uU z?COU?Gb@w2b&Pbdtm5-2bRBFvu(QiLMwgJtt)AHNsztQz^x%^q0Xs0UG4+5|Q$5|C zEhoO}@Xm({wR4q!5&liDFk+5LJzJ|ohvv?DU8gkjiSR((Uh`uIN$=EGKS_&V)LVZBO zZ=j4qIx3>-O_Kv^>@ny_zJg`87a3b>UAme)Fjo46Yo%j}5sLQObE+BqX8fbcq|V>5 zMR6MEk-QwZ`of6(5^k;asDopE08<+le{+xOLcUA@DN^z?EwvJ{!pjQx%EEqW+3nNL z=>dc6u8+9(a$H6ZlhjXMe$Qp~_sxNs(bk6X%CAOK} zt;SdNywZv9WdLIgI722I=%*x`Xm1)C5aB`^-6Ooo{iII_-`VZ`@iYu3plqbNi zfmLtx&>m})?)vBV_E5kHC`2MCuPFRZ0l{%YB%j5iey zE3z{SzQvtbswa~g+{)kyG=*J~eyOL>o2z6Ifz%hPKjh=bFE5Ms0uy>#M{J0z2T(u= zana8sqvPKz!UN5H0bQ-^aJME6W%p}JplM6Yi6CZ8h0Dk9av44m>ftoH)PjHe)bHXsoP*Q7|u`in2<-`PZHrS?dP zgp6^V>Gta`^cHdHZn4~_Bxm$k(ja4y%r4)Aa8R|YQcl=n2Q_w(A*T+iDUrA(fJc3WD5bq~bUpA&V7VvX9C^{wUC7H;W5KArflT&?ryWl(fPcS$X^m)7*ur;P3rJV4p6Pv9dnw=p)CpV518BzQ2W8Z) zxefqo#djjhu_A;sB{+lS9P1%G!DWV{MR+~YI0&2`Dcu?kA4sL!vco3>t1GD2v%_tV zeL47T{5L3l7o|7vc}7dXssX#Xl{L$r6X7lbf=eEP*`Gtde0UecMrfPrK77Ok0&5N4 zkM>foaIcikrIT)E8$~Vs@ZWJ7Lfq-V-?Kxk5P-J z+Vd1Sm+%00)#6Cua#Zv0SaFllwp|Wck5i_}s4Rl|u6-7@fjpFfq3}fkxc~$-M|tmbT;yVL^XllrQO{p2%2VEQztuXWO1JyO=1v2Wbr-3rS_6^BlN9 z%Q_~kn=VGnUf(YcWv*{Zhw79@0Kb0z6fbd|OWeYZj=~UW^)3JsB(6`FVTf{7QbGDI ztu#*bh@O%WJ7HyIV=y0F%+L(v`FnIO=$*mpc}4yy^qe#s4XED<3IuhOsW;U?<2()4 zA)Mt-!q(aI35~TpZO4Q>hthhGSANRqqdZpt0y5SbDCP(`a3VQ$QHF+y#OvOwUG~Si z-2iVEaGuu0OVID*!-x&(8w0^Gg*thD?FS!8lXUuU${vI`1Jxudu-S3r{t6$yljoNk z_J2C4eT^*2z{995i0%Yg?G7D!iJ!U6cSJIZRo0l!%HZ_#rz&?)ycp}qy)aa*#)%_TK$OxKX+NyjRKS1+eQpNHg;Vxv4}LOeIPG9Ffjq`-V%weS^evyw zJjbY(wTNWY3;XCD zqVi)iqPI$K)=|J{Xu)E@f{b*$^L|Wpma|;uDdL$pmRRI028tJicBT8?;%;PkHnsw- z&2(<{@N1CnfxU%i&dnCMdqsv7d2S|r&YMQi_PG~c0!f}6S1mYfxZ2h8!*H5&7Eek3Y^lQDGIAu4c~ z-+ZGRzL*^Fd9aPLtUfL?JEnkvh)ZIx#By$sOBh264So%!l7CgSyQ(L)tb#co6!=w& z$lioOD(6FbAbmi=gyl6g?m1SVh`&pSiQ8FXSb}$q{$UEt&^dGR^7qM zlcmXP``n6ozTjI_Xo;L_vgJGVsN6tgFi35y-Bf#FPerpVrIvbg$e+@hclCV$ZKhyg zy?eU%4W?w)8fG1MUJ3>Sm=sS4po;eu; zLqMxU6xRDRGb5fs)fgrMF~Xmprl5G{qdy8Ec9xhc!biGEK*S+?I2@f2C+SOgZZU5O z0-s(|($&_ou(le}f_Me@U`=M_sw2K6f(29c*_G+5Qq3K^Q2drq2c4~vUmnWxXYG!O z8vWV>1-5n|*Uumj#Y1z}X0xia(ph)8_JZ1;Puh{r`)4^iG%g#EUPt>;0y?y{{Th&Ff8<(!KCzS?zKw=lgnw{#BcPuR!!w=)GRL#VL8G#NL<5Nlp@&{ZdQk+$EK+Cxvb`#mh-FTS2C~Ni+z) z%Hyb;rO)3}RhbOL%&cyD&{fu+g zYMM4TD$$Zgx@4Vn%y{9lO&==eC}*$JvTp6yFGq~@P~>WF8*dS}wTj{V->OEYws^Fg z0(2gMQ?s`A<9gvKaPaYM&A9cr0qA7e6?SlGBg3;CF(*l$!@2Uh#wuRjSHy3d>79hT z%<3xqBwYX8vL+XVZ_>v#E5tw`eI}Z@$z?64$B;3|=n#N=0{^C<&I2pDxqhq;CDMv$ zr&u*>c~Qt82k`NJG(F(C4$#JLeF?y0@k`7O=kU%qYw{ig=1Hfe9pOVhE12%}#y~&A zWV~;XpMyX;Fyf)T=Ak1;6z`5cT~m(>JF3p=YJs0p;3Ur~KLYA8O6DC++>?oA#2;90 zJ>KfAA5e=l132xu7&nimMDs6lv5>__LKSC_5yVIEz`5U zOBx8DLcGunb}^t3kVnRW}PZyWKWBoMjN(D6CE*rn<+rNo@p63-H~6(Kc3e7GDZheJ?`}J1aGq> zy8g5!<-)Yqp8!C-Z-~IhbNaa-1DOQ*GBo%JO%BWPK2znF*QE$v-!#(byZm|s|BZB{ zkmfdHxgopN*t0d1MWW7L^^0p0vc&+~YxlvY&B|@^XVVw8o{YfZISw{-^T4DJKlj{( zv0CsawA#gv+!~|X1+qZ-)-i?6DGk-DK}Tt=<_7C+N26`oNtq{4!&H1P8Rgz6tvqwL zOZ-j3$|Nh(-@+O<9i%=m-G7#y^2_uL!sZFE`r+q~E=1~o2Wm6LCt=_tj;C06BuBw@B=l8c?0r(qZv zru(Po3Bw6!b0q)5T9RQ9;#K7o+l1Q$e$h^|KW^7L7dxz0J`X)}zR$idzPl%H?Q4&) zZz%-gUgcOw0e{eu03?BxondB50QmF60T3vpheu36hWZQq42bBEDWG75h>CyE1wcVT z4Qd%x5RNm;f`kE@x%U7F5&h%ao8yC75Fil1fJ(kagbRxSD+%NMx0Pq;-`NbtA18>}Z@lT*e1_lT~_`^XQfVlMRAi@L!!zAcqpd38J zA#pd#Vnk;(=jW%Tr2&ki()tT?PCncNcI#u1_X|;AMcxMs0`!4_od#+b_>mzK>;qwN z4$S-63l#&8LI(!|)dF(Bz=-76F>@mzP(a!DaQ4e9VdtNKiF^`T{Se*)e&@pSi}RoE z9(|ksP=N@2%7O(9h^w(941$N+gK+fk$@}MKwgQY|4}k*^67+==aA0}lz~6%m>=Are z4c^QO0I-q*1DtyH_Z<`rP^4pn(fWrD_{C7XR!u)q?uzj%$Jf@%gMi6AR`ID|Ktc8L zy5bM;v0nu8dlG(r)8Xhtgb&pb65!bejzXk&fh#LL(H~(Ey~7_vi~tP#EAHd7JOorF3Ab7;c@!uoUa*ZqWl&J)N{00iCpgS~#;^z*3+ z3km{2z(fPC3E>(*6#gjpg%-g1{a#Ir3-kb{30U1g1_JQ=`u;NYp{0jF0(^Oc|9bg? zvb4xN!^GJ6V7}`!nw%83^T+S2NAcH_l>SSuA)$hckBq0=lFC>KXLo~fe6wzu+QrYwp#GiuZ7JI>TU(x@2e=C=PO+a5Y)%3{lQ%s_OFe} z54P_{WA2MD2atb+hZCwA=q^C;!2r8_6l3zUKhRSR8bXlwA;EgS*mTSR1^HIH+|bic zySM>=*G%gZnz#D+blUxZU_iQ`G8;792S9)Vy(a|Hosv+OhS?)`wh@O|{%A`C3ZH8jHDM&|UH-3-aJJhq*3 zMetXvSn-}GH8jfm#=P_8z;tP|^2Y7{ON1CZ$feIsz$1*W?Z>%q32_ogfQchkyjYY* z-g*q1A!%J>U&E0+)3*~MjCq>_|r~f4e z@9B12+93d-*bojE-U``vRhXyEQ&A(*%zO^dCFxEy8_VxWrEl`mUnQR9JkXkPfF&l^ z$}OH*hl?vTjh;`G{?zHO_-Xi0HSo@>!n5~RG{I@{qEzR|o&B|Qth}0Y8C?NuS-Ii8 zLR>SLlTvZ8>jL#wFpB8}*~#3<2jA>lmz^JUnNrlYysmZR$-0i5(d|=GqZFOeD3M`b zLDZZcv9RQW_3(zfslI#Rg4tjwP}i@@swbtP7OG8@4V5J5BbqxM(BK=cawa(7UKg)t_7? zOv1Xo@J-IKM!beMLB+s6XGMx#R}6`I_EmzoMnrV7Y0EyIn=k40<{O)V=uY?8bNZd@ zwynKmVV4s)>rbK98ATSGq^_juZi_{)s2JT~<+$M8ZYQRA3|C$sULf$7OeUmDHqo_A zqB5z))0cmF0b{sc{OBW2kct=-J=NAF-bQXx0U|3QRR&+OF4<0xZJP~W_jKSk@82w1 ztVsuQw0!J1b}rEj@}B)BZdT6l5qaBmAF3KAc%f#>7hoQobM$~6jdIqIco7uKTBD7| z3-wU-3i!M7Ft-Ot&BVps3JH*Ajk?o^Fh&QfUxJ$<8cePaWiGdAl`Z>1LQ=G>4=NBS zX_pfNW3^Hf+KOU;_>T5ER-n6BFri1iX*Q}FE$kYn4ebcoik>{V?s{Km02O0Cy>K-F z&tL>HjooZ0RDND3q$G!`MC^$89Urk_!^u|_?P29whJ$NVsoxj_x@|M|2~S%&67tpk zinXT4p*zeeYWeieg_x3?DQF?S6gohj-+muc{3G&U}nTPRq-cC%}TqM@EGa z&BO9(7wgiQx@#jePhJOu8ZhqZ@h}s{-6LzFlb28Ulc?_NVjAho(Z*wT`QQ*qRnCE$ z^$aLLC;=y5P^oIBD5{8ifN+;Zvax%C#MZWX4{fv6@2yM26ndLy;=F@m|3L>qOnevf zu;M7q{BCkXda{?C6Sq5ecDD>LXuroLJXx*dl{ixmI18JqX!mfk4L;fxFeKRz68AJJv~Y939a+4;@tBCLT_E%S^jw!L$gso#3unnWbU zlD-dLJ4Q7rD{cX(TBfFiqr;Pv)pEM1EoWIf+#d7gDu$VORHXwB`IJZMn`60WWE%IJ z8>?=V;gU|phx&Ro>pKMJO3_Q`Mpxg(XYo z6R0YdT#ogN=e);Nj%=6-jfQ5}n@sEzQL8VMeM!n{QaIhRbAD#mMWD0i6mWh4KOA~i zL27We>8iGVn#IQH4B|PiX7JHv^k%*lfXo%n92WQmnL0^cmzS;7CcjgXxqaKT0D}(z zTJoDmGf>Vx@-c}^*7$4v2G=rP>!qg}hDJd@06pts!+nM*5WY?5i4XS9+L*<_u01tw z_N1X?c`m!-pUGXKEPUu}lX zUxCA^CS}Y7gBe{5^YV2pHg%uJJD5|P8zT(_S|S9L#eMgt4YY5{?7a(skx*kdqb}!& zzTS6@T-jK0uOQ_UCfXb&#i+FB^aMXL>vM)WKDsBG!i&n~91f*IYuu?h&9V<``(z;Y zrrtbIQ7Wmvn3zDb7j-UjZaOn_8QjGWY9bCYs)mhw7t~7}n||>lfC-2+?wBE^;nY2*b36-}y#|v*oPkOOhCB1#Vd!uRZYrd!zWmxpk*fi2-y)43k`F zl9|1P!s$FD?X2(?GqL$bVDiN{U#9umnD;xgM=J}hiYNxAOC@t3?EHfQeXW#T93!yoQJ$E6K*Kf+M#C zbRAX)WPGa8bi3T(*@VKvue(Prbv#}pX|xa2sHj2$CoTC#M|wrLzNIsfWz({NB+$_r zBG@%9A{)H(Svl$a5nU(PGZz44TfXzaDS9SK=3Sh40J^fe%u2Gpb*>-4KE%N$aodlk zWYln9Pe|hJA*d7yA3VY8udtK}f(ffxG^awYP+nzG(N_@MXp8n`(B1QZ_Jt=}dh1-O z>l{NmBO~x!e83~`3N~JzUTaaR+9n(_Ht3^$MdU9DGVXaKEdNp_zBTk8Fluzsd z2MoTy{pQ?s7L`h?0$KMA1$`MgDuW+HQ`Nv7ufXIamHkr3)kP6tSu2~#J^~(QG9tHn zp6E#Ff1=zpLySK4t6!4BDQ0W7l$GJX#_r3zZuSYK91G zO36)d?67f&M?`Y*U zxd|1KAQGq{O7lY(m#n!Mcpzg5d*X`ihpMYWtD3KA#|i%_S`LWN%Pb)e&u&{?Nh&7% zg9zM`SEKhT+HkVde+m`l6ES$2Yv1#qLC38O4#aT5Sop2tS0z$ItZFT^ZEG& zY9lAbC9vTScS$RPR@I0uak=Np^5MMZ*0^MbX9+s*g<(LE6ZYR!39J#HG2dD3nNDQm z1<{Zj2upOxKi1-2yE#8Bb(75yT&sa-f;Iv(siuqh==Wu#MOp<^E}^QO#bj`~5{zY2t51;DhERpqlS2)7oe~zG;u~nJ z_l+uvoYX8JvT0_co@_j2w<8^BCLYS|z+OhZ5wezjRrTAyFYPKz%scy&A4u!E8RE2g zR87LF{jta!#?n(Ha%%K;lGI&e2Is!7q+GjvuTr$YxIN0UXN|?aHUf9N;H~gNP}a_P zAz`BT_Ym$_I9%PMT`Jf$Suy=yZVL@bOxDVx=3isDLqp^aSsgX&2yF?lU}!~=q33MmH;Jd!Nj`HR>6c8!vR;)M&y&)?3rv3bHQ+alkTF$sChB9YhW%nH3;)hWMtp%Z6OJX_!rpl& z4!oYJ3A_o0ZySTP^1?YQecqpk6hB~Dr-UMWqBOP9_*ieXkl30bNd2O)s{S)(t%7i3 z9ZlrU+9<0(X8rBxqjOCzz8v(bOz#TfG5w5^n@s(!&~Rj3apn&5fj*`aTq#Dtl1WF zpY?^`D9D-1j^{teIITy@kW> zHE7$v1lAUJUMpe4Cx@<!S&DxoA8C`SUkCdZR>qemwQY&0s9 zQ9CuZF8v{L)G|`yWL||*&1jh!35o1yzEwPe-5$AJ??e0mNkdr1fG~fnwRc`0M+k~~ znl6dZ zO-)5HXk(f# zQ##bwG8ljqVGI30hW?CqIf2THG|lnctotT$jEy~>RX|X;S`-$SDN9B4lOR4SeBJ>x z0njy%0*W1$3VFoQ0Bd`jYle|P!ja2mQP4BkI*x6pzCa%_lOpGG3{6fC-y*|~ip3gc z)p*H6eyJE!lsj$sh|u_;%Emq!S>M#r*;(FuD|SHQvgg_|;@Kl&dC+iGtk9i_E9Z!m zwQxxo@q#atpDYz!`;AA3ZiEOoxT(w%n;ly0n<}eE$=mCAcZJ|V^{pxIOXgRc z+zz?CA>e~7$Ls&<(X()_?2E5Nu!}X_T4lQ-{Ol>(X}vyV81v!JvBXo&4DNDqKSNQ% z_yBU^mbak&1mqw>xf0Tj4ZDAR_+>|V*R$*8-qOkU0%F(cO9gqN@Gij6;Oh9UsGHQi zu&*NctrM{{Oiti4B^=n4$6ioFsADk77`mgox#aI`xnXe-Cu#IaXSH2@|JQm=q&O4o zbEIl+Om5&5H$}L@#JyL3Ys#Z`7&`YbReu{zQU^O}0xd;Nw5!n0!wX@oK8Lor972Q-)43VomoB3JeH9&d@uMz3Qb{WIsx@H7?rZJ!1?DxtSHgRw06btHL zLQV*lxW||+iCMj44m$4-590_n{y}~ys-f%1z1We3aBLoMzHCrMCm9X%)}YP;QlO%x zf#<^-{b0xI3%QSM?nguV)@Nd!0;EoC+0Y}0FE-~Y{I2{2mCZyEPrHy&G$zayX}UR5 z<(Q0Bfu&LW=#(oCK87knd+`UO=@s`B^<{Z|>o-;l59c;IA*T4_*TM_ojW#mw!Zh#2!u4vZMWT7-~bYyUn+~t@0)e5qq;lv7nyh@+lojmmAWy{jo_;X z?MelbdrXSx-qakUdj~`t>JvB#Qcq%VX^bZH#@^exDN9Zz3H03!-4c__5C?AzKfRTB z_f)}24QE$U=z%Gjr&(cfk%EMBi8gWW&6pMgO0Rjfxyc^2xhrOwM9_K$3;Ueg#t{Gu z76+LfPwKKh+@w4V9JNJ~W1Q2|9;bS`Y3LmFdc&?jYS#?q1c3{RQnS`2d49{PITo>g zu&6-UTbrsAw%8A=yTnch=b*gB)K_-h(GUhJIpb)KK>qN4w@joFt=UUUaqZM4t4LcB zM7{-mgVo!W^X*=PpZ>U`J|-3~0u;{7+_X%?qf7>6XD6#98?PD~V!;A4x%lJLNFtiO zBRzW8OL5Op_!86RRZ2x{+5$Vm3u5omenC__@cNPSSe-PVET}J~8{eatxLoeDVnQH^ z4jZzq`I--u8P+)&Tf^inVgS_f>md?x-`q87rxKAdrh8e0-X7rBfeVOh-*C@iBuo%o*WclE)%{l5B!?kJAfm zUMoSRkv1hCLj&|1vfV!lr&hT}#PljoFVF=T&KQk?#^`+!40KTfst%W6PJfP+vaCbRb69%i9^}HO4)zw1Nh`1o80wW zz!KXciYIG{1{PABuGO0!?kh7O34I{@ZB+Z{k=vvD)@Uh9&ms~|3|k(~WSmZFs!6l< zCW;A+5OtUzIM;hZsGUi!b5$-0pD$x`hH>E(D>Ev$T1jw8y&m6Mi+ri<9HEKd;(k=7sUkHz7v13<3XQivQ|G?~= zd#wt#SE2sv%UEC>GnR(&C?P^$eOofoJ)AbOZJ$9J+l!?UVZ{ObllxXta#7v02l3{L z6f(h8wvSMo%z2^5xT>JlHcq-Y)ufPZVQE5+3D>k>F)^Iy57v;#+n?HrHcXijm(mlh zV;W?~$BvT{-niED{05wlTT_66vEr)!+0Nd8iNVm>9RL(ShPc)w;%+Yh1w@+Mlx7CV z%Iy6ttlz2RNMzi9QOs(Zlh)6MtwB{)EToJi*;p=@=Zd zlCH0P;e0+`jV!%KtYsnog(aqO>t5}j*KJ7lS%(cdPVr;-uKZ#`ey7e%)Y6|Xn(UkG zvypTrC89UmcTL{F?7Bo_=M;VbHH&Dv&%aT51R#De^XLisc@k_5O?y*|)qI+q>j3@{ z)n2OXx};m6+=po_GSD1Om|a@Nz7m#`IF}$&gO z1|>q8%c->Kv27YFE5^W#mbIq_>hU?p77+vsWp=mlW(&~nez@1RK+%ZAXkY!vrVg{G zPpa|)oOFk(G~!grgZ136{dJd#S_9juQ^OtR`hg@dr@?rF=Fqji(rq>fp3KIV*bP$V zQK(l+p|0@9EL_dk4|7;iQ*ZH`uixw>`MtpgrZmMJGi)TLf!Iw zd1t-*{9>6OL?ZR>+WF*kvRQXq!}hOhx7|*3aO8H4E{*(jczjX$%6wihF|lS60P$6SUg5#e(#n?nC`?IHnV zruRV(5+h;7cz_8GHD1i#8zmSNnkE<1GQY zbh+EsMRAM|>n`Qt$tPTJro~ugyE}GZusLGE)jMiPc_c~0o~42~_Fcz%JpHo9l0Vjf znGCRHu*+|jd|l$EQrKh5N0}w(wN<5>m8aycpVp-j1WoGYgh&wRJyO;Y+7a)=$o<4y z*b(o0Sar!MFTx;W36xmTMTMzW5$WtI*3o2a)d=nZ9LYlpUZkjBktz(Z`=BTMZnW#r z^6J#F3Sw5V4Qk9`i7?7dDinSLj7MIvlzm4%d!?&5^au|cl=jTaYFL-7)65<0Bh@S! z!}Y361Z2o+0W_Kp(k|lEMihBw$$O^mq7rY`x^e5UyZDg6n5{iT*__n3^cu)P*^v*d z)$(^=E9oWsR%$^_M-PsB7Pw=3rX6)cK3)|;nW-O%5wtHhl)6g3B5&0y5gJ<~YI}R) z)OPy1ZVOP~4%I1O;A;0d{%JTLk#v1k8sL-gmyFp7#Js;1Wv3WtM`}1rFWUcLc%Scb zACRv6!LDrl>6!h0A^X@(TmR{u{C;8k*zLOX(=+=4&-lVue)gMR{~0j;31j-;3%>lB zH~wKKn_En7!#qn}su|^T;}&i+E8>z<@>Do&DNF;h8)g{Y402t=uaEjZCGGC*dVO!8 z{09HavQqu;R`WkZOaETY|5qEu_Ajjy`#&W}|6$V?IB4$VE{ZN}$P#PKcTOJ17RuSy;VgU=vaqX6Xt zLMCn+py(25)80OE3wk*5!W;=6K%X!-clYG3`0jou zR{J-FtmJfZ#m@`fF6ZNygk9j`eM*z52g)F<0#XB9l0lV6DS`H+GRy-G!y)KWY1AHt zFA#5Rw%!k*@RajD+8?zd$H;-ChEwCq^O5SBSYWt_Z}Y5zthpk`i`+?9l)fs{!%~Uw z`R0X5XH5|a%CLL~7qT6bxC->NRLZmH&EZDd~OELGCxG}i5GGqs+6*Hksil#>k7DX}FhBbTDL zrVnki6$I(`LEU4k8eD!hfd-%ya0~_v+i`qrOqsS|^W2yHv;&R$d4seA=xJf;`T9Xo zJ^iF(!c#JPQ4yN~+7i?K+h+09F)f@F?v@QmA4;__(paq#pNEf|aEGD>)URdn*ij<^ zoU7>?t>~N>N}HT=Jw;JW1^vKyh{3EJ9oC8sKv-TLFqR8a&R8h+WqPUkp|SV6 z0|hf2dX7bUMxscHZGzBMPATx-_Q_7b-oyo@6)3=EkhvI17YNGG086&O$)HYFcS?$O zokZn3T~Isd!utRxVu4mDkfIaRM)%{vpP=#GnasBie}ly^T!)CHDd}&IfW&fr6P{Yt zqF8P;M4i|AKH4a$_0vrpS7*Hc`5UlTAVY5~qlRl`OUe&5t#9dyd74gRx=wzM!?t35 zj%M2#`6E`~N- znm*uaAAay6sVhXR=@JeJH+#C{fgG(-%hoWb$!_J-;|`rS)PJ^d^)qy0AkL4Erw^1W z+2yD0Xjq_h7Q2iuCUW3Qg2mR0h)2A0jN_fRsZk^vw*O$lQIH+6A>PKV1a#cn%@$`8J|**o%o zvwyPu*WiQyVaNIh1&f8{-%Kke0uFX|rhm8oD+P;_o%KIYO)+ht@;H*nv@jrUY5GxM z5RL6!R`zY6Xkj5H4)6opfuOJ;Y54+ypzyVX?N!ul>6=kKxoW&&CzjIOj`Wl&=qe zHBJf~z=NPdFatp1Dh5Et=9efJmZ%2-0b&Q}AMguLAWz^2JvKN4l{Wz^AHW{CJ%>?5 zK)WN)g{8H&$ooqTD4j9|AS)o?v%e|-lXHbn#^wUCBpQ{DvS$w72x=Cv-6s!MhqC873BVGE^me=Q@1X!2 zEiN{`98LHu^m6dX&~A+E8yW%G+c|<;+0}jWeTz2$G$JT{RA_aNdPd2;5LJG?aNvF8 zGw-!dzGAw0f}Ie)P79UXwGuBiV4{Hcr1X8P!D*cJx)ppa0B#OcY6o0((d91AS1(r^ZPa5|J`+85^(Wd zy~nGMm|21M+tWMd&0gdq@%*X=Ea?Rg!noUQU&5E000mC_p>ZqxbYSI z`Bnb;7eZ7y`_+oi>MT6O<6G1V`i;dqGCDo`W;>KQlXNp_kh1J$gvhIepFJ*x(3UCa`ho%|!*kB7=Y9 z>nS~NaB}nD-O6Ep9|iTn%=z6>Tv%#;R`SmUg_yT~Pvt7g+dM{#tz4PVA1qi+Np?r)!d!Ly( zMHt39j2i&Xif5UMtn*d)$*@2lGum>K)MbS=$7z^;_n^)=6}IF;;a$!SDBJqs8mz%O zd(*%;#i|LsUCWcRI?CChVw^bCJ`(puf?4%kg|f=21t6E#>wsRbY-8BRd64cXMaq_K z#vQj7wP9ppO%LD3Ea2M)mw&vYJ$#QFecKEgeNFIgUI9;xx~P`|_euB*cs)$>b$c_t zz7F%ti27aT%JM_*0?3W9SF3dB4GghIp2xDc#(p1RDhbiTkzZmTto#9spHTLo^AoXx zVFPyBMyiv42w;YVYuR6xgj_yj8Sg=(iN1t#@bst6g4LlD%gJA$Q~4Y35uol}`!j*W zI0WN(@=3E#Z-N=eLqugUO0a?RLdu7)`!sTxm%v6HbNq`i4rtOM!~uw!bxq8IOQFQ) z!1QpMCzoAhh{zq8W7`&Y3M~RSp4Nu03oA*l2i=@~$+x|M-pUdiD4tIM$K^TXb@qu7 zC{sxsNGg@(JyR*9dpP@RQ0?R~i?~z{BPt4>7Oa4leJXr#a9+&z^11c+zcH+?Vlwg_ z3idI{3NnkxIj}ywensJzLJV5I0_^o!t z#jc4?sEjRO{k0f3kd`2~6hX&tzpfh(44RHPZW|@^Lv6u%f;C862(jH6IO&g$>WZFN zZ8?OR%gk^+5un=DF=5B!OU0(f4MBpQkTe72aGWP{3%xuF)$M3)jx=KKyu8;Cqv!2L zyV9%*m|MNu!4{__0jXaJu0ij~P+PBcs>bj2t*QFWDdsHxGT&Uz-HO@|<_UkavNmT` zzpk*8k93xk_wg|Q;3?xzGbnMFOnIUpdUAKvGRx*nYF9f|&*`lh2iD=-dOG@#Iy8Dc5k*@81^TH-86ss~3R(8+fF4F7o=P$rK58OGpRo}L zcCKAYO>cX}7|rni>8B@4Y)Q|E!iimlef2Oqgt?t!zFZCgN%FXTjz(`78qG35kFUrm^(;&xbq#V_Kd z--ja|9_A`K>`*owoT8I?E$Z0{NvxUmVpfHKUHiAFby9z4DF2EFd5Gzc&xIrP35!4C z=!_fAz+jE?b?ePJuQ+;oExQuM(n0rHELk&S$UVz|ZLk!AZK9>Fa?ufD6f!DfSiWE0 zXWZ`$K&#!rN9QN``*f984ClI;U+OkCsU14W+;V2~Z6SYoIc#`gxDG{FBXrd!kKM zVkDO#U$c)3v`qHZ=Y0hBs<3lmsY4o;*8B=`5}=UW1(G|LKY-Z>LZ@^)HWOB}<@>O6 zoD@hG?y?wa`3$B00k7BTylT057t&@I1ggzQ1CBI#wXuJI`-_B^2nJAvG1@V0v(B4c z{;HLbqV8X<{-CUs2I@_i5>xCwY8b7|tGNIrqdLo5F_dygVA6{d^6a9BMMRwub$JTI zz9G$r^>GzvWYEVQ36b5)kLYHd^bgpFXcf7!=o9BgCNDK+2I_`g*NWHf_nAUQtGCYf?pqSLaP3;GiK>FR5{VE zwUJ!oFS}V2jPts6qiGv+Sh|fW71q}9{@S1Qg}e0*{-F2c$vRMo6W+{@GL9IWYdBN#a9wJtc4;LVJSu38?PF1Rp?bisvR4o1S6A>)&>jgVO7k`L=Lz-C~Y0EY# z9eh&Twos6X2PJ{#Mco$fhlQ?3w7V1QGww`E`xePrJIXv9Ba_kA5jltJJFCYgO;Y-)}qp~!;A&n zq1Oyn@ivIUP}6J;&c88^79aTOHlts>w+d8y5|Eac>b;Wkp)FeaFP&h0UXcf~5i!hHJlBTe$lcKGtm<&4(8 z@_c^kd+tzis>^ShWTsI%>Zbio8W~}qpK8Tok)o%7=F&>_SvGYZu{V)2lvTzb^(VxU zl7TA<;wWq+R^QKjG_%8X-69bb8L^tB<*jr5WLZ?vVgGuviV}KJmJ|7Q!v*r9xKF^E z`iW&}2ELZX#u(s65XLK|!z(BQ6^V?$NRINz#5J95Q zWaPl!qpp?^I3Igpx;_o2X)^;kSypOWrCno6CABz6qrrY;4vq@=I3?I75u5 zsG2&yJOV%>nv2UyYvL+X{h;x&p*yrZcbb{0MFu-|{Z1{|L7d6yrI-{aTfU{v9VUO4 zVgh0SnU5lJ3VoeMy78{)J%_KiK9+};W)<6+Si7^d^Anzv33>q z4`zmg7ekobuPNL&7b{<95^L@+i5kQuTSI7b*EK{V+Itr?9;?y$X7vGU3CDV>Fd{bG zx+yROYL())EhRBM)cM-*{9$BXQuc>9(>EkM9J{HLZyiFV#LK=r>h_oJAW58_XD(sF z<005gosKeQo&us-PwwaN_>jRAmroqJXrG%xk~`|&GSZGQ!JD%rJ>&r=DnOOO?DzAR zJiEtt#^@@#aG&YZ+zoK5MZi@Noq%TtlIAh?Zvvvt&zXYn4IlP;%+ z4T2ENAoBixmm&2V-8mRRlhTRi^3)FTK*crapy?!6(y8YkGup8J-G) zj@o09O>nonrP!Yea#gca#~1-crB&L>Te(F%w!7k`9AWIUYcC0QVAv^AoCZ^E>W^=# z-EF%;2uex`i}v2Fk4RHO^LVcYeXi5Q{?-1=Xk7Mo+eFo$5-BNEOBZ>rbQ;dytN z;*_TQ^31IOp~})GJwlGK3Ua18W#K1ch?PBR2iv7UnEy-(tYdNUj#mmA5xU{k#?Ybe zjBU#o9t(8Hc?j7_IkLJQtJrmgHuXN{MvT#7*Yde^h+cq-ucB?SW5i{L-}teuRioQ5 zwE7$L@Ds5)MF|d`14R1oiq0dLY(H}Dnby(QSPOnunFJw5W`(|N%)6LI2hRP zBrSfl%=sqWcx_nBWZwjIv!SxuotdtEBIc0vDKj&)UY_T?%x;R=drSc)bUR&VRn;4? z6(n^Ht!%3WOEZ*8gX==L(v4B5TS9ki3aCsel&5SQy3u@F3lfu*a4|0D@8$iOpT;G% z!m4X-CPcEw1eK8)G}?w~lJOWg8BJ%+OL~hi)RV*~lK6U~YxyY3OGbSgN*K|{zMxd{ z0bNIK`J*0L@q%}8#H!c(xT0xwmZFv?Ob$h8ddI!4>iUv}e|}Hm%aa0ChE97cb?SWV zF3Xmq=#T%E)`~UkcZJPSkfzUPtC(1+1g{#c;c#8wa#LE0NLzf&px_1sBs`lVoChvg z7v^sbqAw21!4oE#@A*oVLpNMx75(|d^c)054o6UbbcDXVQdYcw+E8aCae=fRos5cW z&xl@!(@}`Wk!6d_s7*Nw(WT#g>-u4%3W8xw&R|}!$2U-cq=AH zplCnE)lq7S_lQ;5a6)^+xenc4n~FUc-XcEWbZI^}>yJC!| zWY448l_em($Q4BN6}Td`C+-t>Qsmqi7%FA=)AQ#UJTgK4*F3s?c50*>lO;NdWXh#r zAYF4Ar=VthI)Hg6ZQcQj@Wvn@8`4PAMX+VaI>hw}un4X2&$|v!u-c@%IeB9m3${eR1-T;>@#^S5{l>J8L=}QJzY?D$+c(s#b zEUsKbL`^1IaDt@o5*nYQ)gK9*6_};Q2VKoQl>v#e%r$3qZU9_qZ9}u>rVR@k3O3sj zMqfffr~;^rP8v_#^o_l@af9jWpstg7v>$%)`PQ8*guLSicBW{JKYDRF>c(|eN?+2_ zbx#&vAbyh1U*!I%4S2gDMNGPMYoKnc-78ll*WET&X_B#;l$nsixcAMK zRd}<$6MUyz=5}nSKKMwKy|0)^$RlaI4+W#dT@G}zX9gLNHDM%zshqy{F39twSr}Px zT+H@XswW+S-iXXM5aYzQxs|}*6hdlp-{ux5 z_ttv5@udcEwc>FcEx*cY7CbYMYAhQo50UetvXkmQ0NkL7(l71s#rk4sR+uaA*;y}g zsbYUd#*rHZ5Q7FGZ~UvIkFfrjH+U&3@iz7Wh&Ddtjk~&s_i$$aU96Q9FN0J=lb{i} zP)Ut}d)X@p7={$@(G&&j`~c^{5tz} z0Uejn!9~pZz-Ba&OG=l(mY7Kt_K=+0i~go@i`AS z#-7WkOLDlby^x%e07a{^-u{TW@5Dq^jc>?!g1JGzWyNNct0|m{)~l&W2DbywHycy# zYHNkOHaM`a>+J4H=SOxpk+HQpsTUxxFfzqc4l8D&XX&mp*hMjP5?(sViaf-CGRzw-wp;Gsey8R z8q4JlB~y{%!y`g%p(z9WlW5RAx-u-$R{p)-lMy&}7zMK@6+YjAaZSQrtic5%PVlkKo$@T><@P z9X1*m^ozzw8{M;&Wq0OraQ%ypGiY#f%h;$O@3n-+AF!=x0gDvQG?PSNNjs6aX{PLM zfdtD00n0%W2nkLPHw3)hYnF=_UE|=uK4b{~xlm*95>AwLu=oJ2>fJ^8hSCRaIF$;W zABz%j!nF4?N`2G&`KJ&wWR5H{J%x<`#=&SSk^|4-Uq-_rC}^s?%Ngc;+!5(E4k5L` z>Zk$ z!7B;4N{q8{Om5iN<|MczYNvcDOj2#;77!%}|K6Uf z(f0)js{N;;x4KCRft#ilMII6{*!Y82dN4QvI+&c9GnA)H>?*%Lnw zMfPC7gsDsj9SB203M;uaUbISo|4mf)Qt29YP@8S70kM=UM(Bjxl+s2^73WY&Gkj2# z-=g9`{JOQNd8kx9JQKva7@e$GHS+4RmX6DWo(v9i+(u{N!7r)4yPm4^u}b1ut}E_$ z5%F5%ym5p30<4EtUv$fi6a~_-Ra{S*AxEw} z7H}HWS)Z9ksCHe!E?Ef29RzDV5!w2g+DsE9M>9>|Il8EOL@gv7CX-S%oVoe#rkh^` zYzl<+rGpb!HL7wc^>4!G>G}BQL8}PcvA@*we|&N*=v<6!Wsylr>kICoq*^UW8+Z00 zGB8tRb}+oAgyq}RPk?6&9IH6QEgDr#HhGmf0p_j{7E-?fg-!%6Ez_vb?-V*#~vGa{SZ?{cD&e!%3X=+0(Yy&DBi?mRp2GMJ`6x;tu z?02qN_pBD@k#4upC8nqkwTWPiVLoC(n&2{JsIL3saNcwx0}&i(>4GPAZ!52hXQIIV zX&EtynkteR4dmI#Uz53^gM3(1Oc&@|+Q>#b87+mjPZ$E9ya>K|c~8kc>Jk=#?#Lb# zK5m`X-Jbm*weO@>lw7^P@eO$~gjTBiwC{p55PL-bLVB~9kAYsn=t{<^9|NFBnC`n{)lyqC>Sx^IPZimS` zzG~d394@j7GGf0Mb4cHsz9?v{k)oWQSx+nO)vyWfz!KaIJjdk0vM@m`@*xwgifkst zSzc308fq72(%gdUi~>XZH$UhwAz{SkOJp6F=;Nj7*k>9Y6vI^CK4_+%wd(O3Gv!`) zI-n!gpLxqbX{DKCvD!mhmt1E0p@#E!IN=^>bwbh4mI4&f)h!se#$jy`)l$;kRR3Mt zLN;)(M}}}!PLrNt1n{?C8zrXied?1%uD^}0UjebVEm7mZ3kY!$^I^LBfX7I_&v=K& zjwn`FH{@+G-Q}a7lRVI23=3xUUB@rX?|Z~zH*`XiZWX7iB=L^$@+Gw{q5b=!`346!*l*6 zBRDkOJ+z^Nq&59HFcPY&UK(APZj$lf#EH09$6#B=*Gf~Qs3XdvY(~<5>n{CsP>gVJ zWJjmLz4c*XPYnKqEq$sj2Of3`n)qYBk+AEO+hBmZiqc1VaHYsL<)BW=x5vQOF&f0c_%oV^O{LDE5 z%d!W@J3pY*FycpdJNK3NT(y^Vi|I~mJ?29!Y5!-RLjx`{4q3(Q;R>f+d2`{s+=!zv zRJ{#OI|i0a@9_byi)YAfoP&O_{{2R-8_sf(^z`Xk))4$r`1(@ifp+uK?Y#6r(=JJZ z%Cs_*h~J`dJB;RPwUd;$Yq0EkiMt9%;_WrZ!{|_H9f2rTdb^6z2IU9r~3sN zUuZ%6(#i5BzZq<3`AleD`dY;qfEy6=eEX$e6Rxih9YPlTd-x$V=V7tED^k2`p;KTyyvYmOdER)~rmin(11OOfX0o zbu6GEC5Az3kr?dN5mz+Ucvg^Jl>ISO9XdG|i`wRL7Y;p5WGezX>?E}6c?;@T)s(Hl z7&K1J*oJc2o^JCOULAk1NW*_JC_2Tb{e#V>QiAQKm-TNsAIKAPQI$3R>DnqUzSI+o zJum3rXA(nrG4bnYNmJD1XoRPnGsjQS5qi66)qKBRWG4biH-Az=ZZB3hKK@cF6z)fd zC=^g{|mUn&YlhRV7gD+*8;wAAkhM&a5_^ zI184JfpLR!ng{vdr9DRQ+gG&|6xzke@f2UD?yH^pbNw(8A>`(w`NFok-)&m7%BHde zB@Bv39u9sL^xkH7Z0~9HT7>r)c{-c=m(-#oFhz0ZS7=eG3JP*Xcquf9h4A=>#rmA~ zFEZ(|1eL9m>#qSJz6iEx%uW}(29&F##&J`G)Vs{U`MXkyp;oKwLoC{PMJD#a@OQU% zsGtqo)v}e?&2%5=ck}40IHYo@{NGFsJ8J3}N`iSUk!>>VXJBZyKvwJs4GQULH8um+ z)7-J_=BWT2f6W{{yFHC)(o8*JK)FG_u{xSe8tHvvx_f*ul4JE%xBGswupSX|6k~Ts zW-@I94`Zq47nO7g6IJ*Z3eNlqn=8Zl*!K1@e@e*R1Go7EX+8>B0ZH{}!t9s{(=1{E zIdD<47qb`%=q>E?y#MRwlr>_U&=9`6{z)TnA~WZD3YtROP`kGYq!*gLC_?cYOTibF zuDPNG(_Bt74XbBbG$@cjI1>gdOExojNk%MP8B46OkF3Z`+|2fp1(iFNRnE!h6*<$v zq*Sven@GjskrE0dzisr(@bw)TC5-)MiOP{;75oyV+y*8i`30JveAktpApNa?jCS|t zy+FXti4|g*vXwSzhKlP2Tpb~efQ>j1RbPOW&{;6mtvH#rZ^-n_-xH1OTW4Z54nuJ) z<>)Ojg;wj11#H15Wk`!!Z#pfTg{Cd5KnS@}tIlnhxgNxZ^}coV#M0hyd^~CUHDpq@ zCMtC$NruzXB+4AH*2LLsBR8fqC9`%L3M;9iMX&l#;rI}tCQNM%t<0hn0#Q;y0d1)M z3pf1tYUlsr&=~)V)Iq4deHOi?x zDQF3wU>bTGWNSVF90jAYg&OtMJxy#&hQVx4Fl5 zT9avZ=X2w8$Nt)rXK><4IaX{2)dWT%q`kn6zl-1AFQBA^5&{AM2=pHYPC5neycqAq*dQw{ZuFZ{JjG&FI-&QE`2m&I_ST2)BP}aeg)V79q_~; z`>^$(U$hu`pf3JjI+%0>RQ$Vu5nq#e;Eh7=ehBaYn)}er{%wTww6i+a1P}lpPJTHR zVEkCH_TPW1?$P|P?pz%K@O9^U2fwesx!{+cW0}@Qz)pa`J3Rz+t^r--O(FQTGy>6% z!w_u}|O=WJOz!1(akp<(NUQUUJ+ zHw5PRf_TO2xIp{_xkKOHze@LeNd(~`^#3#q09Fya$g2(b78EYSclH`ReIdxp`BA{j z@WBJTeLmijIWiAg{jHhN{q6gWN|T#cR~he1ySz>KwN6S6bqDft19=1F{u1N|fFLJ_ zf*0U>`*zJL)j!*H{|c?GsKkN3<667*nvecao4N5#^!vVr*$4P?rKds9=zs_K@LorHf<{C35atYM|! z+}#|dBZdYbg4d@Le7c)YfbRnj(pAz5JH>MWesk0LOWyPcK&KI}4>knOk*V56OZZ`Ldyl($08e87Q5fR4*jw_Dr22Cc|yf32A0)+MKr+w-V# zzHjY}#BE}*tjl`V42R9p)5DZ<{xs_5-(_1hYE_it0s4R>j5n(ENr8A9gt6tt+oS5$ z8*la1OZG{>kX!{-;m`Hl#Z?W>^g><{64Td<2->mjORi=pAz0(EguKZbI@J{?yx&E|fd zo|0N%HxbfO-kkupcd_j32dhUL7KIuHqxqdmn8+TC;95F3elI*s>xIh#lrA3a6xK=BXMXJThF1wis0v+R5Iw~H2W6xVoi6H z)KUK=ywATl=s-a?#s2fDo;$rS^h^MnL3g6ISr4Uf#L#3NSHa*9pJDXHr){>r;;p3? z;AM~J~RacfPJN14%*Mo)B<(^R`*(p)2E3NdC;Gqvr zA;DCs-vWbf(|pmm3^$AVHq54`J$j(OTmC#sq86i$LBuExxN8r;R*zvT9!f0bgx&>b zt`1?naX7oC30}2^z4fq1Z&jRYJx{ncXk#6XG*Z>r|LTa(;gt-&0}iu8CGPyADX``k z&g*v2g@!J-a^kqB{Y7g@m~&xHgYhuFLQD--ZAO3qdNS&XuL_+}pIEmzHe8l@G_MO< zqx{EKnC%QaK1+tkums$-hD6y7z!#W~2cHbo;(!cmbvcghwS=|EjIcf6TK5CPLxezS zj_p@n$L+^ZYd^&31i-ZqABz%QZ+w|7Do*hN1{;uT8z>3uIC+XguS`@hX=N~`pOa;G zIZV!AML!9#4-te5ocX=lXk++%tAm||)O9(yi9@+gDAR3L zDr7A)LZfQjnPGIdKO_$b+3~^u-owh_F}92{*mn=fu<}+&>71EOBPBRsczy}D`cY?J zE-1_SumU%jsv8XGIA!Oco#GUzz-edC@6F#52m8weXn%2qx*;HuNOs7Z_o#&>q>`aY z{nte*=%nxwLb=SEpZ`v=eMJ|&w;cw#%Z1X-iOQM(7a{&JK{q-yLSEF zG!X*sp(HfJ6BKAGOQt*iM$o70Sj1phxw$zvf%IDPK@DSOWPKt`4H6Ua{XoEnmv(^d&$9r>OySOlH_mUHd%Ka z9zf4~webBu&FoIFC;}+E%j?*GJ_jBsbb~k?R%4|X@`10*xyZIigLH)pbP&-aA5F2& zV|!Tu7ixH&te7=-gy2 zUnJ%k1{^?MJo};~)eMIUxH415o6D83ODkSq9(db0P0x&W*g0s|o+Y(>ET`g*_)xewdF>(KMR3Eqx@_7A;4i5^^Ol{0%aHySxn25P=UX zTwO+!x)aqSxPYN~AIl95IA%8>Jv*vT=8@bGRy^O9LCeeTSnL{fY1PTNNLI8nm)!fg z`B=DAedV61%T_%zLyS+BhxR2&7IO^SaEex>2krbPhuOt1L13quil20`>!m9H;7esU zD;;YyHW$VZED;wXryc2EzbUB7B%G=0ZdXmmEE#mpkA+`hJ*Owdi1oZgLln}~ zWRxz>1)g_4E{wm5e~!-R+92Bg!0%-_8?j!Dt8Sre#~8eGoPRQfmWyyW7@H9{JfG~( zbcq%O9sd#3%0^~UNjZ5*!c7!-nO#bv?XcNAG6l6LS%Um=VIo#H*rGrgfYB?X7Dxq6 z{J{k%VDmI~^4MZ$%u{3Re-jpCa!~T7(w(^yxWshb7?HPqK(B{|J{arq-6TCLDYuKy z-Ye>gSS*E))RMOtq-I=bIZc;KcSrN?RW%0h5%Gqr3Yffu*q!RUTlGZj36}_gClM3E z&b5*sLNFs%@1rukvH)HZU#5iPa$K$`Lzs)8P1q+`b5t*#e!MZ%kb56;m7?- zLk>o1+ndHt(0V_bBF_2F4}h5qUK+1Hcv|03)|CQ8wI44|O&d`2A{mYi4~^%?I(Zy% z82a^S-4Q0=n$@AjBuk9B)suTQd$PASVQFEWd@>u}p%kJWeO;Xcip6`f#DNp(7;h@w zD!Gp#^-bJUX!anjXNJhn+Ue)8%~8bD?6R&h=X1t%#VFD`6$g48$@P9CLzl`AWwc~{ zBb4Sh?w7HgLo8CuNY;%f1=~lp8n#Vt&iE*uqK9vq*$dX(J)Hr38@R}CLxhZw;WT3K6Mht+`c zVqSI9inMHwtt|Q`ED9GDvSJ|;p$;_$EB!XH0u5AWr2c9)&WcSxXjz6o-X{okX37<6 z?5W470B(PC;qsKp)0&+#Hg(8Xdr_7idqCZ-V^b+l1_QAtm!WLSn3XX7iCbO_KTj3x zaPwQ6QJYlAVe^sw!8~lxd!^dl#ql9rwp7@^b59*CCvAk0#JAwV@THAx%Hw?Xa+t)z z3-R@h)%>KUKGaxp9nEkyhWZ|kk#lX==mYPEb4IhEi+7QT1`W=g)&{c5;;-YZsYZL- zC(@!?1H4Ug8LWmYXhW&kj`3I<@WDvDEk`wakt_a7w39)@+za<%_jxRd2Gr;vr*0{> zrr2-X6D^~%YP2px6~)_EO_gcRG{6xYDuctZe*zv0J8{gAP&b=yX)`yKutSEQ&#n{I z2vLQZyCPfTsEhe|L?YsNgp4D8IPHMMY}MEcjqS1!8Pw}RFpKSEJaXnho(Fo?X_G8MQd&ky)+!BYKC@6l1Ej;w?^C9O(?P?c*@6G`~b!_irJ zaxXOFY6^zj!g*ZukBNHFSRqI^-`0QPVceYMN-XM$;~$vNdS;{j>oxZSz6oqa}2~YT|->w15ygYHvq--sr4&-`{FK5FNJ&A(ji@W}db69Sq z!V2tcEa@~$yMk>I%EAU6Et$uS8eD@)$^aw6dVgqoMq|xa1JzNFOTfZsg!1XF!X&JT zTvgR=Rse?ZA0|!pwNzhNO>~I3)iz^h*=+18Jc>Q;iZnf#PQUc-krv^ zGtjGfD28ORqbA@)qTZ>=5XNHWPg`W`bBHmHRX@U1P^H_++!i=m(?K#NCI` zR7Xlv4QY~(omPujuPTbu5FCtDEBYuJadfKGKW5T77h4LLH7>U%`oTQy8cCAw=WJjMoX8HedGPv?) zU~tnpM~zw>3O7#<1xU&sM^GWb410ll>OWxL7dPmL?35HKoslJ3X{h1U79&FJxSy}# zfJFGxHT=txMBq5>18v)~?8;k-bpg(#7RIX`IU^B574GT*ZG0M~M7bV^Kyvb;{32eS zb0wTq2BVJSBhGj-_sp#>yB0op@k|Vld7AA?ZaiYErkuY%oed0Yu)+M^YgwpFMW_pB z$pb-+=sH+(h39mqsAtiokP^q-KK+gCalL!^*;rs4tRIo|rJf5n|6G|?f5Kn0>d|VL zmv?V5*q*Sw{)wIj_#);bqwH0YM^a|9()HvhB^v%7T5~z1@ld$ULUvNTbqJ!--6f^R zLKw4Yh{9y0sSU+j3FWs6~a5& zQgL#gp@u9PLSBrYOWJK~ErsS-lazcn=z}et<+h~rprBAn0A}{XT2u5a;-G4?i8Ju5 zeUYjSji~6#RG&-T<+e{FNmvl-*BHFqD9oO1<&~r}YD*~_31kV@<&`Ta($B37ci%0^ z)l&055GN}fwE%&JQwv-I+x>)-xplo{vZrqU*(6br6-6jMKQl}n_fSP=aXT2E(firH z=1?hj!5V*rre-l6)UsxhmbDALKpeDrI?d4e@h8%x6^f@>rPV@jdw%N*Go%@ zM2Ym^tb}kX11-v(ZGKgsYOtV{u8^nWqDR>$!Q6=d!H$MH@?-yUlR2;*prBIDv*DGil8f4?%nh%-M>)#Ofm~iP_?*uMF!) znlPiwiGBcyAF_9ZGn{WaBHb|NbYM zRTo455OMbDqbnyFh(R}=Fs?y(p(6|eR&2SIT5hS}DY~h8;EWU&)V-NUgByvv zH{mCYhs~yVyb`+wO~qI6?>o*~A2K^I*9$Ex6fZVLNCey-rnBAmkgSUB*;z-<{OHS@ zW{A_D)!Hj32t?7T+n*(IBP2G)@T+U_Sn#UNR5VueHAykCAj677wfj2b7DPW zgO)wA=fzj>n5euL#?28kWWv>z;n2Wjw+?518D#V(;BRd|u$H&8(_Gvg?pF&US%s+} zNX5Hg@4>p9PF8qLpA`>KJieyIk4<}4q+x>>Vj2}OGyIRRpDa7+`UUXL4uK*PcMjt4 z!6%Y1S0p?=b4apO>9HLX8O5$@oTKxQg5P!>T7Mbo>_xgyWqL-vFI$m3hvQiKBnficz91-MRY_9|C3NA_uaI&1b)@}r!IL7{3 zjwh~xIl^Sq?*X2=H-7j$P^(2&F$blZ;;Qn#TYQT8mt>sqx=dXD0Gtve1pe4F`C{tS zfK%V_)FBTSJh^|?p?qQ0_C;=YS<@d%9n?l>)`VHl1ja8~+;&-nKJK+UF7me=@V4CD zY*v?Bvh7@yos zzJ<#2aC_w3SxRdQaLKtr)QlUvgIK3!mqI%a&EMay>SrF)L~Bp)itLYD;_nBH&QDS> z6D6^VKNP1YF#fbsV`oq&bx^|GcwAG*NODYOlY)1}p?f|3bndh16qb~!4I6dOs;8(V zhx!IQ>uQLQshV5UOSkMt=PrN>_g|2wAitty(4{#{qqN-KBD4GtW9JYoShQu^Yb&p9 z+qP}nwr$(CZQICe+qSLBYW(;W6|Ygv-QPP-oVDg0!yTu*CH$?f3vkS~;2rRjGsz+k zIEXjNHo=*xxvUcm0XR#H#3zs?GBNZqoaQX9S(u5P=>wM^XZ71AcS$)qS5 z*9&toh$*>R#?B&7XHmtVpjv73w(^TTrMT#^hf~oM?5Pgvv9aPjM$$$H#Mg-zOUF-M zv~4p7Bl#UxZYX&Q4K^`;bXlyK1*SW)Ml^YJ;Av-iwK9$4<5EoFs@Z7s zd+jg!-p|1gXe-CD2cl|Fald;}gJw3hipJ17`DNg093AvN9E1{fXZtf|WO6;7Py>4& z)g2a^+@eXGoRoTrQ11~QTTRbqo?tLjNC!R!t0FbxPzXbjXVK&GrRY#-iiZa6+XXS zu5ne_R>7(a1lJ+Q^zdz7Z^wROou8_uhq28#pD?IGJJ)?aTZrm+u=FgM;?_iqf_^q7 zSH_J>hf^0oIoY+-<35_?$k*&8V#4uuVzCYzYSlvlz_5f0!N8c^mXJfl zzrq*Ed^SG>3#(N5RhkZeDSj>W^}_-IlGvaH%A9y`gaHU`Gi^*R+gdx!gp*Rd!y~Xm zaoQU{_klw(4}qT5GQ!$?c-0<0rupvGF;kLW%4+0G%RKOSa(6|}2ZJwGasZObxJjPF z+{OKju`Tk+8mEip)pbT-G`?X-O#O`1CzYG}0Z<5)ucF-`%@hhY`~;{Gj6O-{Po{QO ztaeIv-6Znig;MVDJ170mJ$9g8um zcFpp+GkY09drs5K z+n-%vVj!^IDvC%K8Dfh^%=FJYIQ(w>W(_vb6p2@ck6JJ~%Y;TedY#67RsZ zjDlXT6eDZOcMh-poqi|Y`KtxfDzY!L>pO)bcoEonmtDv99Ag)4Vo3$urVY$v+3uGR z_kh`@&gydSaeGM%t&h}2A6~}Yu8E4i5dDVAh*WBVTNX@+pHpC((eklw$)xK<##i5% zrIgn5`avk6xe?73=T>unALX)cs?D&K##Ug6tZ_dzo2!X8gjp8G%LFZgcMc!oA3)iq zY?*RZ1Y|vLWWAp&O*Ym>K`GP&%_)W{kC?JYP(rgm6K%pMStKM!H}IwlO^afc9riiv zLEvs#vV7+lMammf_ZW&Fee?I6ad4R7QVpk&!tj&^)F;te%t+*_`@4#u42&8?*F>;6 z+hOfznV^}J;ycc=g^zZeko(4?sSvv1`4;Zr6Vd0{GD_n-w!QUWd|4gpS$ww2 zGzpQg1Th6gz$vNY0$d|em)#TzaHns{`DShTwb3m#*I&7>8YcRaG(x;a(+^szotQ3M zWb1@uc3G7@U8FAtH~ds3+H(u<1@n#Y_`M?kgH~_(a&IVK%bX&4T}HPuVJ-cjKJpFN zwlRB((f95v-Tzx6M2Ss+LB^oa;G#^%X0miF7=Zoe%A%)(+3a+4&4F&lV{_a(A37D3 z7#8ztOuhFGn2eUVH*A7J#M2Gq@zY=%O%qz7A@L%)ZZux@a$xB`*o^yelU?PlOXmZR z5>6bLp0ayg685Vcc5-xe>X=rPk_i;664cqazVFR)-Juqdk>OES-w+&kGjqjjmX{fJ z&m2S(+0=5FCLN2})p`rBXfV44C@m%OJkbUyM(e8+0FoPdH9vm7Curx#i+d?ml^KR? zJo0aHZVG}Lt~hfvSCCaH3DjSg5WesD^yr#b5S~uU$|^NZr*yPxYgjqg?=eJTkGlL~kQa zt)w+K2McI&T?Q{voKb$m9{YOzx?0(MrbJ0lL++2HN|aM>?IQl8bcOH%QU&t@S!?qU3P*~~N|04*Ajh_C0 z1yI~vl$C8(d36Z$gPFs?NKDP+(LkUBi~&q=aAZj6 zApin|L-_IkFxuK06>~xT&;et#L1JHq4(FTv02fAw2yM~1P$Ila6~lqSZawn@>MHt}XxqOAc>XeG7%b54B~Wj_rx+k8xNqJj zg$K_J6AMba2rQbp;hzhQ{S99I4UP}+<-!G^4)IlI@8|Ig6)gPg8p>aQZiWW20Upr` zjv$VVJ*TvY>iFu427qtAMu!h_9NO^|<}Mgd9==Vo=XQ<2FEbC|uig5l7Gx_=y)d#Z zGY%W^v0QvtN1ygu9T`etl%2D0?#9-r_gsPBV1ITz@(Jx0c`Q7hP2%;waV~3P==i zAmIT%05wo505Ki>&o4|dyugpjxZj&);U9GV3%Q;&v}d>JT)%n$+d7hc@E?qS(}W3K z_WoD9My-Hhfw@Vvw_oY^UD{v1(%;C3zNGIyDaA+r0$<>_AHgp^fN&#2nL%%0+HkM*G?9w>rFwU=n zX*{0cYe%mwKlr0Gn7*wzKQLtxA%=RrC}KidK>m95aQO9O1UeFjel+h62m1C`yG)=# zLG$R(5@>skPCoyB{Rv+gX<=*xSAm~}?>{8~25#YFcJFu|`MBy=CIo$CJ{rTj54YDL zLp}TQjrVluXV>y$jdLHjb|@L$AZZ`M!?mrqB6#uXw3UqRwdf#JlRs-{KF{XT4Ib|A z^Kt8!mxS+V%d)1<_+(iaeaT{s&W9+^!4O@bfPDd%j(n@z%{;=XEt#3rH&KNKFV34R zp*gzXZlP~>5bZa#F~xpLX~z{59JH=$q$f2S2!vyoQ69q5bR;B_v1l9l={xd;PqGf? z?FvbgHn+07CrqdW~S+jsz>?Pyxc z=W+*cf$$eXL2p-Eo=-?oEviamvYg&$FQbae=I3hZcvU;{fS{bM32*$|$(6DaGNjcF zRzHQMk~r&{;3{6di3(-`A(s4puBo7%S$_tXf$ zC#rJ7Ok+4oIuk+4j_fj(`>ij0yOZ{iy76U*=)a{>O1!Lhj6e|&7U|-E@g!YjL}2{v zx{}P)Gt=@@#f)53!og2Ns&j`Ux&BUcpyp93D0Wo2CS?fv9ggVJyw7^MEy+93o`b1e zs*X4;uVJuerx85U-FuxYA|I+b&8V4MBj8cPc{5$2xLJ~ya`Z^k^JhgGB(oW%K_d1vbm>hFW=bNWr{=L-V$(T{ z*h*}Q=d9~a4s1pSoLurKQ_FG(3xm9ZX7y(@1+UpxLzNSz3Q)E3kP@@nx(|iXbzW&} zDBv8*E#t0A!=H&~%vMt7;0iZWTYoIUZ51eNKqh6ZDQ*=wifpz?Hgl5@ujoAno zDs%lwVwLgJb6!5JW@{A7%x|i4i?_p(a=mq@lQ%7$3wtt6YL&-atT`FT466vn+4VZj zmfV3JOD67T4j{0o>LM9UUBg=1#WE*x|4bI6Zf+m44zLb-AT7sIcKWTlHP? znOE{|f1)vvu!#0332^Li+^aY9rSfuY_vBTQX8pE-b*klex;B2rwF0d6^E8Pwporjs zo1{hwN15g+;s~tY+XbPK$)fzHTgTU^5fhpo(C)dDhYW z=FB^?j2Wnnfj=Mz891_6#Ns{Iq;3k@X}zt{Hc%*PE_|3`nM45hu`x z55siGjNz3FlXuNR<@XFu05V*0&X_Q4vFNDclNM0qS`H0b29M434O+32RLOEC(8Av(U6 zZqH6bhY-=@W;)WuJjHD8AFK`{2ZA`)15Y0rMh2aoDShCRM8|pu*q&r4E$2*yhWo8o z)20Wt{Z?38`jX_Op2tXWVjQjea8WGl6Om&o5+OuPLwU4jb}2Cd9f(uE<=T!El`m<( z4>^f?cB{%nAQLw*!(qMTRI*b)W;~thNwB{|d#-T94G(LLi(VjDB@*gbJ#0Nq3>}Kb z2|YO^8RIL~@*kST$lo%F4;YiH?txgnDio*?2HN- zHZQsyRXm&=`)8C%I3)=VerPgop(xVvzf_DTB`#rw_UtO|o5`v&$(Nax?a^hhV)B1J zMfL>8R`cnPHQLxGIz02ZdN)t9B-ORS`osV49kn@^zj@?QJQuT;eqL*@qIf7%uD911 zy%9K5-+jGcZ+wt7q;W7LU;?l29|d_m{iK97sI04KE$_be4)YFC@0nvTZznus(;0sA z76cvE^s;>zFZhUDdluV<;I^Eh4z@A%s>n}$u2_iSO#hg{Biw(wXeEZX9EU~Q-OaA` z%O<=*oYVIujsXvFLOSnVfj6vu3zmGVxG3+K{lM{Mbe+I!&u7)bCq52jFSa@0O{?(C zuD49OZ^7WSaqg+fi#w0e(KL_KPlgJq1&hxzS*JH5+&>6MWF=&pEF~XZR2P2>K=#tL zHd`C95aE|pt)F_!NN5vsRzcd8a@}wuJiwK$nr{xXT&iEh6vEj&S@ZJdZ^ZBP17xFR zt#0;m$3Ue1=kn@0XYhGoc&e(ba^a?=DL^!~FWe~wDIctb`9_XhM?SK71O z{s;EJvJhmkDKkeiH<((CN_yo(9gfjmhW#&cZlisyL(4OGVuB9yi_=xy-;6~+EsXwj zaFA=ms?c;NAZ)32#S*+9*O|{*JJ&QH& zjEKATBz}E0ePT4XIumMJOkEDR`%AL1OVLhwtwog*fQBF#Y4C$Qa>SbX=bIUhk_h$D zMqFi@<7+d!nW(i_DnIW1r>24ZNCw@dTtiSH@;AeqD0WuYcNEs0LH)4c6~f6}JeE&M z;hQ9RG<8{F4x&9Zmr{J+#G{?X4N)sda8#U{hvwJr=`l#E%n#|O3DfC_lF)#Dcwgu)_;n-cON^76I2~K#O~C82Wn)Pw|>l`CZc`uw+|ntb6a5~p_M^j0YXZB zI!?NUtS)nVZZs3M8+*&s;iEE~d6K5prlBTV-1;5kR#nGB<2UU;FEPPFEl#~s2`NdkI&=j`U@7^1EEnJ{n z876*?^eEVKCgR^)UnCEgNkt5w_*j*YgyLU?|a8=7XiX8Kp!qr^_m zk%m5OIYyW!j^xth0W*yUFfTP}iPq^=w>+GoGn0>KVlVqis7lb5Iis_o zK(wPMjA2lUZLTByZJ&t`Nt1F8a7(C+^Q*_YFw@AS z+st#ECFC^DWmgdr(EP|8=dqFh>4a@{tJUh?A0O|wT;|neeHlRSaKJ4q{z?Op0)r#Z zvh}{SxQ+(9I~kXXx6TRV{jl^(_RiR9yq}tJl#1#pmwAzSETGrDeZ3No*jkWitNvGE ziU5sz&&voplZTIx_zCVr*!cogtD<5#66~>EP5ld!4lf=kJ+={=n(}(CJGz4-BR->? zIlLI`n9pKh)IX*OUsn{S_iXf$p?--3aN|a}CkIHIGSYJ&et%M)INZ`Z#3;wKs-{5! zK_sw`xRO67ZnBa8S57=|;4Xtf{=h-)V}1ydScD!;m66-LbXi;VEUX?kdjXq%`8;k9T$`N=Z{KAzDW70iH4G8V+AJB{@3n>476-$&;Yw zK~`}&dzLVkBQ?bu?NYe;TekyG=)h|Y^=0)?pi9fzGKg_nXboZD`#C16Ner?Fm4j3( z;+cwUZuq7Hgq6!`rC6_ar7kZS?)&9N5vn z$3&>uWCr4;dJjcGLzvtPb~lRF@Ix+%Q_!)>iG9`cGbrGCD_)?n;8rFl(JE9mcmAHe zW&Z?baK~x_z7K^H)+fKK8i*hU8SLpk+rpWR6G~iy;b92>@(_pY{o@3LYz;v+YljCZ!ZR~6b?DkF9bc=g%!mHQ z{oc`bTv3O24Hly8iDTg(Kvq=X+O#NX+_o<(fHvlSK~rOHk@FSL z;XRQNs?h*{mk6ZFViou;py2zyefz=*=f=7z4qldCaiu7igq%gveNHA*!q&uWkeeQ( zEmro>#@7K!zO*Sw{RE4w2PzNElJSL5)5Dh7Dn$bt;my%4i5+YtEo$js58lXeAaxCm z^P+f#MEoJj%#~!#v<^uK$`Fv* zE{@sfaD_v>gk28lu`1oy+WSb8|oLYYgjIT625s z6I0gr9pjWDUUtZQKw3cy+9qYeAk`qeOLPJ+{`BkWmH8emsV@4Ui z_Er;QyJq|)FM7%rR0kzwtv1Poq;mhNUN)sYD4`6>NVMdnmk99>!ByrJgJh#SJYRAw z&v{IV3|rDe!jAjBmf;q{E?H3F`Or~Vv3?VCxr5ac*_zEsY_Y1RJC%meT)kxe#tC=A z;@$sPnV1eEK_?Ii<6f_`>n>-Yj%t{Ecyol-?xx3i4<2#r&)Ep{i(0vX-X?1IH)&i2 zs`^yjUxy1d>5R<-WkdCrbqk}R+62eM5W$xb-FI70FG7rk0Z)6HQEp%BO(9)`s>Dm> z-J`3asAkTeUrl%_MdH}Glmgw-5*Ck)*!8T}wNQYdGhNU)ki0A8UOosBg5XLq8GE zgLQG$j*(Ja(m_`3$x#Kms}*{KuGUuhiBc%#c+g48bNQZ|Od2Ucj#7`p7a3I+;t-m! z9ubY!aR<;eaQq6acgeLoRj&~frxfUd6^pBhc~E=UART20xXg)>s;Q56pfHt2*Ji}Y zRPh6d{X?z$E~XxiOmI5BIjJ)OD~!-PkFi%{3X2b?2Z1ov9&2GgBt`^;FL+BxE zhto!t%A|El7r&h!(?vV${0fdVM}$&y7Vb&2^4(Ry-^F!=vYwT=-7Jq!OroQ~a>+l3 zmcDkeYdSmX;okllDK)e1)Rqj7_n=gEi8*;=eT3NZJjbd5k#ir~UevcZBn}CL6R^J> zUropl*dQ~i+YX9%_9?jF zj~V1CvV#5t!@ADJv>11BayTmqOg>BG18gSnk4octjko=Xmz)s9G0xEZcmgsM|;j$kq*C{vJT2!>$sov&QD&8>VtU1;U ztfDx+>dxn_87pmmn3=A7$74tBY+ii8rC;hpZ36`Vdz$mQ*KJa(O)ggMh!8!w{cf>A z_1?^{OAg~pvWA>WQt{FK@8 z)iOHIaU4J0KL8;6e7Az?Sv$)c&11OQthmq{T(E)PC00ji#a|Xp=I^f;$M=GC8t0P;+e@3ykq}~zp z+oOfT;Yhfyd++Lr;fgPBZuN5|O;MBdWtS!deU`$BzV<792{ZoOg7Q6BBvByKJn`*qa8{RY3J>| zYm{7}f#^eBVJ!*D_sPGxJLtNCXD~RC9;xx17N=Q0`huE&i$8b;Xm!UX2DY~sa$iAE zq?e>Ir3Ow`w?(T594s8wtgSr-y`whO)a4R6rJ5>=LS;k;852VR?c@bo^2FQp*y|W} zmon$GQrN6oYkvNwx;KdsAb-quNizGbIog1wP5Gg+hyB!C+Cv9u5kvvtaf@IGd{6hV5b% zNAP(vWWd_=1_pv~(XP{5JacJo^AyEw=Eo)!+)MFQUG?>S4}HZO$Z}qci1lhMQ~>4A znRD(e=k%L4!s8`X&!2mov#A^Ja#ZC$_Td{>8!7Bc>|=>=H)*LF*ZMhq3XA!<_GR~4 zOkuGt4n!3%m#4&3XxR7eoyIm;9Udqoh&Ag5c^`AgxF}qJCXB3=u|a>Ahihik{$X>Z zB!m$Qoac9V)e6E745c`zAG&7et2!l3Nf;Rkh{e$+%fvx?C8Rd?LW)ia!7`Vah=ZaW zkEo1lZ-Ph;n|`(@dH1xXUQ@Hf`S-0D)tSVcGeG}K%1yIocWprdS7-IOL9$(H>+Q3d z9PjXWn*J16yfav@YS!-xUaH`b?BC>!O!f8dJ&5%ElR6V$MKy2LVZdKUJ}nGL#ij~v zrgd=DEthcyZ{xL0&i7-u-TLH0cA}1^uvSlQ9a2>;uxgZ0U4;&nWL4Vjz<0GIV%^$V zty-HC%4%y!V{`1q9ZdZEb=iyiu(H3HKejBF%Qp7sW^%Rmm^%Ioz6iBM@jtEf{#)Pl ze_QFXFtPmqPB{k~6YKvw{m)K013eonTKw0r$qce%YDVB#|qi`$dK z(de36IMC6GM7Fheb!kNjIeeb&d}cr9zW(d<+A$_^Uurtib*%8np_?~9vm0OKv+|3O zgNzOgP0&EaFU}qBgV{ShIygE#7BbPF1?lVW@zHYz5zI~FfP{qqzyouz<+EI2B(;U| zY+CRW0I0yz`-j2z6OsQVBK|uKyLWto`kqFxB!i4^cMaJHY;F&vT!4_z#TkUXG%^Nz zv>W>C-0x<{L3B1i6;Ofr@3d~%S6X2&d8ZiSf=LXX8 z8*?deZJ@hi%iqrg0JaW9a5IaGi@=qSwfAoCpBu@^KM4x)zN!9g2khF<*8_NG|M11N z#kb=J6oBu?p|!aon4?ooz>6Oa+>Z+a2Goqa%we!Y@9NJod=po|vWD|<_3vWB0%>h> zV|}G`u<$F0g6n^4@9`aXY<31{>)&MG;NRsTcKif;+%y6W%fek;!36hj&Ho;e!8-zP z@V0G-UEWUghVu8|`~E__;D_gY=Mi3UYq!JT z19SrU@3h8h`wd=xwgdbsK70k_Q9;}}1#|*j)iLbG&aiH`Qw4}vH1t7`D67zh8YXa!UO)|h;Cv&$zlEuseu~8 zgNA?QTsvj`q%@rJf$0C3ai|0Q;z%KVOlji#zw;5Z9ir~F`urTc`;~s^(f<0u{30Fy ziaz}2#2132f2C!<(|!Mn*<8Z_eSR6;No&HdJp!5Y=dA)f^vT5d`_a{a&A}b*fAzIc z(qEp6#0RkZ|89%McM*C$Yj z&Z>odGLif$0_L%u_QfgtuL)lNRHN7VY4L&}81^u|$OJVG!6BT%HjCut1#CoyaHxc}Xsb=cdpTIgiAytDWBHiw-(|U3 zJ-3etzK<(r;5J^kQam>F0h?JU1-uJ-27CEz0dsaJoMbWxiWohp)1Uq z|MW`Bu#sSbhV?$h{svX`8QfU~i@xDi!mQWg4ytc-yshf$Nbay~lSB#qVcO*6N(rp#~g_BfP?MAY@s)(AD=&qh8vs`|)J_7`A@CNlIwd!_Y zTkM@Q`RKl}DSj#@B~FGwa~C>{_HFcJOBFU@JV+}~L-hgh4` z;z%g_Zir|3JPaGqv8ek2z>Skx6J#E{-_yCr zyFPbEn;_NI6EnG>r~y5gCqI;xVxu)gvsD8&ILITcmYe=3e~U{hsYDKxUy5&ph81^?|>0 zp;}{Q+MagKKIs5+&J_jWoLck!99kA<+<4^qvD)F>Yh|C)gfx`J9CVoVXG7TF4!U5cf@2+#|t}YKyjq@NI3JhGF1sDKq(}ppTYG5l7I}t zSELM@75fs~YBMiaA(vE+Gwy^Ew_h>a<*rZ#`3>RLrSdg(r|*FRy^@Hxk?R2x*l6ow zn(ZEj68FZ?Zz-AV{v>Z^tmT=_>7432mW*oMpz4?}NdEGovT~vLL!zd5CM5ASf}T38 zF+rNomk;e_1@-haqm(<%KSq~oh(_Ni60($;6{NkwLIONu2vNC>C|ahY=HZ`BwX zFs+Yxxhd}Mnj%AbO(!mR1`Eseoz=b9LVBr&ZUytPD$c6%4${ljnrMj=BZ^^z27X zc0{$JDHSJ5$t)q0YBHvhFj*byYw>;=lmLp2r{~@F>}Dfa>_1QNB5UF|#h$(2lM5XN zDeROebCN@~1fWo+<&k$XquoH*o!2$4(odim4DYx$^8Z;(2{4OCTt zBigTZK8B~XQboz=tc`Jjm%j=y(No`;lJcVM)$k~G+6+IeEv|SV!BR~9 zTFiSILroNmB(3hRPVG(lDRLG4-bPP_MA8~9qG}2lxJs979_pdi_)Lr+dFJ3(}wwuvAN@~mNM~kcrQAzLfe2|Ix6AXvFKlkjRogeiA^(@_a zGyX`xITHcZXawwqNT>(sLVKE6U2+o%`XPa$&Qi-O_A?O^21!%Z1QzKjJ^4YtA=UbM z^gw0Qs{VPS9YiGjohA*Y05t8NZsi&SGjjU48Ky61cyPFC?qKP!HgH251a#Vrd}g%kMHj`J^Me*pliALta^P0d7MtE#Tr$HY%VUdmS20r z$?85qEv6?hIm)^{3$-~pHYEUYr&c%bluKKbRFM9Aubr3(#q8!ZT26ud>=Ow~+z=0i z!%#ToI1S|UQt~fuO#v~nU|k{HKTtzrh7-PCm_Mk`9+ArY0ZPlo%<%GD>uXEcW6OcD zu{anvQ6i8UX@5s_m&@wtW>bswBayu*w9#fkv`xp>fTh2f;-E4u0;aj|An-J4Qne7A zPJ7#=nrMeUXB0f{cG%+-^JbTW5ymLdp@MGW{ep;T@?tWdoL%hd=p}F#4S;I4Uoj*x zkwbAjUZw8nS*c7xfoGEnxLATo$I`!`y<3a# z15oIttdVza8FCxO-vJ|*VvICS`cV@MR znpS>jzy-1k;f8?9T`g8d*lf)4y~G76uGw*Q$C6scsz?3AD>}r8xf7O6*-(%DHUnp( z%8x<=0(qZDBDZ0lmKMgJb7|+O$)c0{(cNFV?j!no(KsIzxxxh4kvdX$` zG;8QI-hhWhbE+fVPbRhXs=Xx*oqG#=u7CAQ9~*+S6IzTRi{+d#yUYq8+nbxF7VDv= zgKH#ks|AaO%`P`IOtud#Jg^pde27rm|a1^n!R(1+%*tfk=Cui@NY7P@Fjl6yHZaRP@-ak;C z%Ndnch+M|nedLr$*KGlnsVcSOr^d%YR9QAx zq1G*0t8(J^?I7T2R;Zn16wzv)VjcCfxqfc9Sls%R9xS@wS)fMkm56J5XVn1KPOh|0!tZp5vjrYw@r9c>OUEW@!e zfEZ1Wl57p=d^X|mjJ2^L%Dt-nLS=tuJg9R;kS^)#0j;UO z%Y53+FjDb^9bCUegE98-3TRBx4vg~Hwen3WEs7K&goBib{Vf4C;TSSL$ml&EfZuQ< zL#iOxtzM0yvb<)HkZFDNHv+%@QtK1Z<@giKc>>;R508`fmS|kk37AY{gRs|RYY8RW zVI;EPEP=77*}NNJJCLB#*Cp{sWKTyEsj`2nh8drALAbsA@Pn|n{dLI7r*t$)8oNRI z9z@M?d@EIq9;TXhEa|Bd8lPVJ1#yawpcZRQVxX67@|?^bv&D=Z5;0k5CE)=QOUtM0|VusVRa~{ z;R6RlBlf~Gdy9bl;p1foQ^u#ur`JSujGTh!?}xv#2_5WNH^H{S1Upg_XI$AhiO@z& zDJdL9b$lw+{$0c9NmS>3!`(4Ix6-OZQh1dvBaB9WgNR@=|5y7bRE&rL{7-Z;^_R5V90v#xmS4wO;_e zt0pWygY-PvEK(wq946f|u2{Cl6iUnWFfh15w)9q(+0fmVM>O?8WTqL}VP6->{rF;6 z6nzD+@nVZwyWdf1TG2Fk98uqVg<1xfz^gddyd&Upd~r)ayb>g46e!j4MYyHojshCr z?dvPQl{qW2iGgXBJF8j4gYZR2=U6EZ$j~i8^r{BDQRA+_)bzyL%vrd=7+#z-n-3nr z`|gUX>n#lx0nPY8YJE!9YkOup(wA<~+3!B%qpIcK-_;F=M?7bNh}>r!^e$NfRYboP zHcBU-uO}T1;PXg4+nSD!ALDS(UI1HbbF6``dF{+`Q>V+AY$xhA;p$=5CinY>8Z}(` z3;&v}@3stfp=MA~2WSrPQJ}!~@pZ-$AW5WpT-CTZkVo|D@LfN^PQ%D4u`(Eiv#~#A zTLl#jVn1#-<7;3g$2=QAEG+Gszm1X!DBfH8$yAPH@Uz@!h8rc)T}2``b5v3HBqLb! zVSe0o3ZyylT^|f2QIxM{AjMgz+cgRGdYI#0ImyWoCJQHjF1#6v)|Pf}zMJKYv`Lb{ z14@-JCtr4Yuc#S_(z_OTEU{J2Kvse z*s{p)mZ~qnj(#IyZ`m4qa^*wbl#tW0dy^$|H)rPYuo@1V9p0MgHl*#LJ=Is>Bu?Fx zUxvNu$+eEPWz0|@{wthn(d=vH?*}pXtrVVVvc<7Da_sW(D?!LqHymA~Yca4Dz~D z7+|;gM5xZbkR=-2v%_d_H&?I`3zdYwip?LPFcrs}`G+{)SHud^*?|*W=%!D%>WE+_ zcuwF;kxbT?ed{QiU9~~Rf@brE#}F`cn%=`xxIXOAnzjQ(ZcCJ=F{NRyd%f)sYPRI$ zn4WeM8;|)@`}bkG=g8G=qJTbe4i|Pdn-}WkLZ2xW9Ecj(Uu%2!pOa%10mFmz_#;3m zxgU>6Mq+|T5haC=XoPDyzIO;62U^{=AW^s5Tddb>Isvyj5oO(P+2&a)Sv;{dcTAsl zqoKUt2g&_>dW^ET$5l?5O?5Jws*A#01l7$z1{D&@5!{7_MM6@N!{P3!+zYAJg`74% z%q)d_)H`C@L>8ykb5NeD=kcc)huwLaMCEcn&jUOP?u``5AS{zk?)HmrNM+(yCS}Du z1G7yMM>1+9&ndm8Jxg-enrq>!zF@cL^HJKa4u%5CN7y8~P>H^yE}(;wQj=?oDG4dg zhHxwnO&g2m?iBCL>1#jFCOB;_=47>;m*1tOCnzrcZuuFS%D8s1KDkmm_@UgR{gKn| zVfYO|Lve-XBpS=KBxj=A=cdaBe566@(Bd`Va2(PFTZo3qv=jT`V^j0 zkUZy3yir~zle4?$^>B|xmSKg*9HmRxh*b9VJzQEw=6>bfB6|tABXb^{5~WUzS`fE* z&nz_-!Zz_nYizTY)eP_eCR+rM{9OlWH>XkL5&Jb3jHvWGfe{5$Jr}lU-=viHmx|>MHHP z;iQ*S=HDj}3UTi+sNP<(pFFL|i`RcbPacQpn$@Df)R7T)pp3${u#k&)JsXs>+4sa? z5j$lgYS%gDQJ7U}|Ma>DJ6w;Di`zMb89Fjck`-1FkVB5xEu?;4MvL^WQFLsBrDwl) zeujmC9Jy4;tzPVlp@f62r%7|QA-j&>;f&2@pBdb_O2qf(G1N3v8lmJgo$xg!Qy?JwIAH49CjkZAA#CWK$k{VU_GB}#A1WYj)k zhVgvo(M)QgvW|#umybtHEmuGJqthKVUh<$0mZuj_+d~7%V75KT8tnqz2SSJ%w#gYX zx>7nGscPE=0nc${1Kb7nng;DLT@{3tJG1yQuI%fxz%8ts6an?vT%YYjWgwE2&e0au zmS`PhHbfAF80~a0nAjA!9!ORS9nuu}##)Z}a)ZXaHl0-XG~Ti{p55 z!AAt{E}sKyIzQ<+LovI^Ko`|E=fA^?Y6g;IyYgHf8!s?-HRZh+Q;$=IyDigGrXF34 zpzBKsG3`%li*tEe!;12-mKw7(*6ofia$wOoW|*&n)M7SliXSCyg9a>^KR;tr4U91f zM+OW}r;2ywEoONkeN_Q4-6D`7)p?58HPXv#3hr4D}haaf2@Q3w?C;RU|I z)^^MtsN^Q|bkO!0bva>B&!@U1Y7&dD)-_+VjC1cyf~H*awR>Z%WLW+Lv)3@DKbbm8 zC_Z5O0Xo{zYeYQCK$4t57Zcp5MT0~_j-}wQaxUj1yDi@hAK70Vjo|wu z26pV~pAG4iP4626nj=agWR zIp}+Mp+VZs@iSWS$`QxYc=hVmW}Mh_(bWst)5O3Tvd8{hLg7cs`cv+wGp-B;t^)Hf zfdb-&SoMeTpHPwmivYQHzK}wBtT9T4PBB}qi-~z0$*{e)qczvgf`h(@Nau@@gRs8O zVl#?9L&}kTqb?D_O0Jo{Gufp6(A5npnAxu29oDJmTU&nRUM*W)?9{uYE`zKqS@Awi z71TCpkO{!n4#Crk?QuvpPR`{@?Fd%mDCTYq$+u8z3aJI zpUW?)+|&7GYiHs1!=SOZw6I!`eIY3D!!&JGMQzK9OIt~slZPwB@HR1Jbctaj{lHW| zuzxGz5?Yzc@q1OJDmM-xI8faElU?8P5+W~G$LTb3^9+qpC%ODK4!`M3eVt<49A%DT zsGdoLMu6jpO1L1o9JD5v2XVOOiJk8ep&{!Qk}VFezimD#J{aAxPN%2uRer1QKx~{l3FLu+HOc{@24kmj1F8Q!> z(!mZdN9DOz6?gqfj>dO5xmVFBD29(rvruTEJSC6OnRgr*vUtSg52!}gPwqbdW_)@E za7p zdwE6LC?g-Y3l*$&EF>>9`el?qc|Ru(H{GdVV?jZwEmi6Hg`Z@Cg#+XAt@U)Exg?B; zTY=QPg*_@ClUY#1dUo0=`4f(O%we|ueh>LM}PAag}81S`X21@64NL{tez6rJfyuvuY>hbaspF$&RY37w3}@a zcl2_Gb6F$Ps)0(M;-KhqsweZ?lleDZrK3krFnmkZ(HuV^)YxE3WSu)&x`IC5+kIkd z`r5(Efk)EjBW*z-Uo)7blZF~U9)ZdZs^Fq!4*r{>GZq=W)||xqQMojtbDz>~y5dH{ zyF$G{rUu$hd8H$~ky*va$FM#{r?44CzKIMkB}`(#Z1Fi5zWd60g6@RE(I2zAEybx* z?3|M!ednBndmnF+A8)oGvM#>xg%_E{CQ_EokEte8B4hf5go62$ZCn=S{_p8s__^$d z%D+(8(~5VIIeT7Idh#^1?Ndz$k54#690ERp*w%M4M|nZSX-lM;9F?hSt%T}TjoHvs z`R%cq>W#QKE(@qgbCMTi!$I6sLnfO1ORT1fNl~t+@;z$ikDb=m@XhKkizyPNM$BTx199`>ezBSg{x&!C^5A&;+CcW% z#3_6?ZwBb9@u@M7)h^>&@-#{M`;2#5wkVq;^$wk;R=><}VvNz;hsMIgpe2S?#6j zM1<>&+xo(lDpd_np`|Q|I`z9)5u-sV2UIbUoLKlRFGz%*wNLS2Cq7yq<5)q{L|;b5 zMtQzNEQ0-*FBCSW2zEBXU6peE)RDea_D*}#;r9J>Z}=4XWmppA`x>)`H^ZSmE}j_> zv7^;bQH-lmKjW^hxRZpfIBruRTd|>6YK*EPc9W zVW$MyA5u{`t-_A!CGt@sdg%6HGh@t;H<>lQf#L|F*H_b${KUtmW}oK?siXTyZL>Op zG-?tj+)>l=C&13tD{g832Vr(c;(iZzK;-pf7kubapQtlEeNkqhcvrEp7QL0e2A-T% zsiA9gLH$?U(&*vk8r?>a@--VGX5$&$+DiJa73(LFF}hk1cb*TurpJiUmc#3*D!dq_ z#)9jJudnf`*AxgT+n+_s8BI*(`yq|qJt#_icoVXu=ZP$)Rkndn3ai-G=(?ZH;lBbU zc=|-p(N#P8TJw3OGMwMwfcH%mB!tJ0c*Y~JOzL&ZIq39P?^-1WMsFqaC-WxSTGdvje(_h|g{;&=^+n z$U>*-zy0Um1$+Zb*Q`$c&?}>gxhdCL^b$Tggof}L&US}eD&Y}Z-ll#Lkt*AN^TFKB z<^_Y~d44L_Lz{Vc63_H5p&j5nzL5@|);w5YjH0_NFFWj>Ro7g>hQ2LTXmq%0ueVRv zzGN)*AXQr4cB9#Ix)8i)E4be^jZf`#n5@!>(U-_;Yc+x4r4jV6AV)w z5Z@7q`RfoBQgIdqs<)&EzmAm&QsVb0iY~Al8#;&@)mK3t9-mh88hT`oHuPjm}43Hboz8P-BMq6GO29us@&Bl!jQb>z(VeN=KmyAU6>wh86bZ8eBP zuC#M%2*IOO;tPjt5LguBI2!YRPi!n1>WwoSiv_Ib9_R%QGy2Yh0q1WX%3#=gWpjPL zdfGL6G@1{6Fdpi|S0y#|i(tgF5+aouY}WaIYH4BS?9Mp9%ZW>Rq!VN5fP52BFqmf@ zeIolPvdm&ES=69Ss=+*HUJSqPusDprNy1Y6;B^my%&@q2eeJacf(qKp)4mCYu8)lY5Ey8W^i%C~j`3p=+yi6lMS+IQE`lo=4u z3j6<1$8QSe+0VL|u4r?6j5JXBqj_mvSp{DK4=gdCAW8)Rdd0#V=irHcFDnaBh!!sz zvnpbJ9cE<1$y1ShBl^spgs<;jP~r%__3+-hoaXfGs#@RXF9D zFsk01S`3V6kO?X$dSb~*+om?4Pk2W*CNNPmlo(kPr=~KsmtmoF)ohGVA)Rd3&+*(t ztl8G$4-e_B49X@UdU~z=6EUy1Cqt!*g?blEM<3$&&>z1-PYnj+HQVH&zesf?-yHiVMoVLlzQf zaUpC;L_*sBPC()j<<){T&O@&or8KjL=+10koSchob*eC+-HcMUt)Li}l}v1<%!cB+ zEiHmW+BX>j0`1S!qGHkP5QR*i*o{fe5uxF(nRu0!pVKB2Pui97*~z2_oKXg;O@bJmsCf!k=lx%oFd7H6G_=TBH{SpMtby|40LN$Z}r~m zZ<$ivjDw}W%#xOI2@3uMmtKv0^gP9500IV?F*4aqOF$VMCgS12sp;hRY#JblugCio ztfgLu0D`zetmlo)pja}LYoyUYFCg>$daz!`?0Rlf{>avOw=e90(IUszAxV=lb@rCR z;n0G7ya>*zBSP-&;gXJ?NrOF6EBG<-QV@aY_>jgk6PI6_k{NHpbw4^_94@j4r+&s* zRbAkIqMu!cd8x?{s{Q?*Ac%YCK8g%!97pQy!i!?VY;LKPw*-8{{cSzFM|vNJ}U#mf7xaJJABHx>}I^d}}{Okp7g#`k90%9@A)Fdb^fpY|e zxCgAmp4;x*-`A)GMyWH1QG ziO9&QAV7cue0f8=LP42-`Nl_pp}@=@fte8OAQJqf5%&+S!Tx`x(dzprVjtuI5QKzS z*y9fr_!y_)RvtbU8{a0_ze*#KT8IXJKo~@PGLYl%TI8$+II^V3B0s;Vv^_{+b^sWBv9L^V5SX`h2ENe|jE7QYA0QULcwprBP5rPsLHCU|egHE7Fi`$I zF{dz2Y#4jsJXLV?3QIsIZT!1kqv~A{oxa>@K!^wEAJXmIv)u5!JKcU3e1w7ZVXzz6 z;7#DGJUR$IRpCgyXsg#i%{4GDn)BB%lkK_B7YIRX;dqK(m>(xPt% z_SQlVfa-ysL}Q`#%}@>U2q!wWnB`sPZ5 z1ESH60C4IX{DV#g1(LrD@|%17LwWQod(7MTr5pUyi&EY;sMq_I)eHHH_wPiJPrgI_ zRcbh)xIs4{o~ZhMQ!Sw0xjQxx=$5wMeY-Enu%4L42CwHMPW~Y8gGYRgCegY?ft&Z34%EY>=KGKG5NORpJ9Bwc1+kI0DU>G z1qlrUu-MqJ?0}p#if9%9=<+`)1rX@;4#6Pog%Zh~ApmrU`ugDvMDLyUM}z}`9Qg|L zQrRQ=`HlEe@jJl-vDkTPvSP*k0r6Jc$p6{@^IdxTs{gx64u(+IZ2t+49|Sh1H1_)! z@dx;qQ%4C1AHd#)-<^n{x8JuJ#=+H5aQ2thy5RND_H)%3*x!wx4+OdqB2KC7uyXF@ zQNx`>wr94mZITj49`eLHzJTnd-Afmox{GR=Ts4?<&*Ea=sr&umZd{$0*o}RJ7szN;%D~^=@R6 zR(KU>ELs$a6Tp;joSfVd6Nf^Dr}bZvWjZ_KC6vY37rptGHftn|E0Oczz2%hYTpMHX z3wp-wjwPynx?p7Ft;5IXLU1e;v!}&S#o)L(nHN)EFUy{Lfcu$ z$9D%Tvf$`P#|D?p z&SetZ35jXBdf}{yB;<62nT!h?ePeU&)-)I-cx=)VQa4SyGj|Vy$!lUgQg)3}7lC|x zi?M$~Ta%t#55+H6Zq8hNN9!f(oH>yZ23i-UF5Qmqij+bHfr%3UtT)8lHk9acp*WCp zwI>wY=;7)}XgxpJW@%m+ZZ=265BYP_y}XZz)RZJnmHINCBN0PbV?Peh7#-B_gTA zJJfGjEwS6RSJ^4c0c{cQr@<#8=v1!28KvG0lSDB=$`O9N?9s$e3mMkUQDxx`_Sb~# z;gD3m`&ZbF20W`fGO2$DS4{4k!+#UHTjF&4jrhTz!Bn!T9rqW9=G>L`f=oFL@3>DA zaa_LS-dL$+bS#jATDI;)b#&qGXKDMj>aMljB)vLszRqH*b~Okc*ZTe;`5Z zJZ2Q+qi>y@S-Fazwy?AK$J&dl$u8&-IL=?mm562k}-W_17tI#^AZ>tIqGENarUkNgYpiRH_zVr1Pdx zNpB&Lx4_B&o29&1mD59G#% z2OeH}H@L#~CI$z>JTPas+}Q%G*vC#_m2oXMu)fyAdbl|CP)S31PS1f>KO)w9=4Zwu zm6=Po3tKe?GJMjTtJ#mM&++Kau02tECre<7vh!W)D(Tjf)mts3^w}!NJ9fyaSC5o# z<+CAGr2fnGUh|ZIE1ESDxgQIN8_wAET?I=8H&VROK>EztKsipc)tG;PJLLFk#B%{j z_`Rwmo^TxDlps_M7pzzq#k0y=X9){F=W&@T6x4L)5X^$03^-icD^-sex(%mHOw}RA zaOckN<7L%)j76_6Tyk`GF67ZY$I9raOyKP1x$=8p{VPDDFcu?tzvgM)yyCHU{3OXP ziEtQr#KBhGMkxw0ovN7;uSzJ>!SMQGLvd^p4JdA$tJf9j0dZ_9`P###-!pqef;#Ft z-0;>H)#>SS?_uTR_(5Mly=D>f%{pXodiQg7R9jfSnRk3!fnhvHA;z&4_VHnwHj0e@DkJkjVdSBIaWHq|A zF-STmIZ+d8FXjRC?NdbN1%BdKy7Zo_a5eyGw}S?JJ*vGAoVSKq>X-RYlfOt2H_Cpm zFXT(7-Z>4r&n#VYp1c4`=U2=4LEqa7zi6?9Yx2QJ(Iq4m!SQwsRT)5X#tt7v@+%+f zeBN3+!m^H8fi_+bqHbVe({RO=y>mO6&voU1#ARiNSXt|r%(}7BciS*Wejt12}r@HITi+gU5B@zL^QJ=g$B_-X(CIcl2Yd+F4O#`ma;$oI6|9 z$vld+Z9eLK;C(A8bba3b;fOuRFp!C|Q%j1*{in*bEa|9pyB5aootC5f2~WUOx8kSB zX2fHIEn=LeW$cg^?|x$CDDVz*e|O0t+c-vTCL6UVMhYxyyq_`Jq?*^_`E*O)Byrz6mEDU1?l z**jeoC9{~yTR@J%U`puB4_IVH8n6C1%{-5ePK|M)p8r>OP%Aw-mk(BR)+IFwqi$-U z@??eHL4SseQwj3F^x1vIlOeT&ss|f|aB$yb!w+dj<|1dQ;|>@T&tLXg8k$E@gF9B> zTcaS!hW;h;F5XKQkm9zTNqCWmq05XX1y3!!Y#_GEw^e{~m#Q~sVc{7J^s)>~v6H5* zj}GcVAjTYxA?RG#p7lr&|M;HRg%}4tM+T3u3}bc~qa#oj!3)!Up5ac<6*F8rVKWCFLB*`dup6|RqO(BE*Y*g-^~nX z$pP@v>qhya0|h@m+6!|_16>g|8S(&sPg-rll3i;rk#BXtf%*|Q*~g@<$@;wHUl!V% zGxpeOz3Z#W+92+Wbvoi|;BQg20e(|y4==kR@y~fIxO2!y*2y-69&LVq$AdJk`#f|T zE65Dps|B`sJ&1Qo?p!d=2^%d+fs9$8#;P>uA(i6`~m2p;vQ{ zi%r^*jw3s0jrc#{kFz48Jb0Ho1n#jEJC9CQ)Mv(FUox#Yf@-n(fb$ z->Y&T02|J!t)<@fQG#x6V}GUOZG9T!+^9av=To(Gd*{$FQLglpvI0PvAEB}-C`Ai! z(10nR@6Y%6ay?NhXfagMvJ`z3mbrC8MESjn`W{ieX}bS+~va(6z9AO2e$6CS?eNIzwz$r5-d# zM+W_0sKjsPiK)Rjr{>iraKrz)Wd0Q;#|6`!V9^`0t_g>K5P#ur&e+=IH_6ye!;{Wn z^FqbNi5+9~+UtPb<;Ce$?*H-7}k%fYflYmUkcvPxARGS1-$a zryNeO9}3oG5!vwGx-B5d=X6ONncuAO;QujIRrF!tHE5XYHSG{~{(kbXV~tlw(v`$G z{95HDb^Jqjx7IlZ{;=}kGXBP{0u`P>4hI%#>YGU*#UWN{E|ptK3cI(9Bu8FYh-HNV zhFX#j1l9*Up6^b+wT&)Bd!vHc%Wek{{IqS`BZ&4*K6g(TD{Bm8&c`2MjX=GDldOMM- zyrb=Y_+jy7mlkCHe!Wm6Mx?`JC?r2H)Uzka&3W+I+{I30&3gYb|sDHEB*^eB2k*(#)?Gp z6w$OUuCvV$aGCSMF3y*zzn_YCVh(*LvazS8wKesI@xowJ$rj=-6hj$@4K_~kjPiNO z&wGAc-&63^2sa|W(Y?{h+k!^nrocC}>@U3lD%;JT1n(8*8pJObkb|J9I9QO5rR$4bWYTg!@K?j4CU(xdI0n?<}?KEH!Fj7Ynj_cz%~@P&DQu>xi`ya|hAE(bR!i z*+uO%y$|O1@ef^w!a8f!2{KTv7+YrC(?u-ePM0z%p1ziz^mtOqi=f-M%ihsX`LimF z)5#kmV(!KCNUS5TCKEsuJd9F{wQ=|RASexO&NS5z60n|wiMkH>5RkAwKNic$tpn#_kb8J`%@t+qCPES|WV(lv+M z6(aV~C^6UqZD?%GEI`SzYaf-y%)~i~`1QrWlrC~8M%OD24x3caXs#@36t^<5Ela2x z1Zp}kW>O-r4n{aPHmX6QpYw%kzHzUlnFUQFCUb+G=`xyd2`;&#pSHaJl*e6HA2y$Q zw9mS|<*25jq4P@CD!!X|)r>Bbu%qJ6D6aDu?*Jl_rh=+Cl;j);_B%PLYrrbJ$haYC zyRuI&zatsw$?EuQrg|_4w%-f;0O?9M>>K|?v||QV>p2`+WN!G`Hkyw4`6s^{p~BMB z^7B+mtbZ~w7Y7sl(NIq%;<_DwLWSxkW(x;=@;&ek<5_ z*udAIj6*=$DUfOS@0x78djwdx4k3z7zMe@0n2s&!Vc%Gk9B6R7uRq=pP1z-uHZLL@ z<4Qvs5M#8PQYp3*KuJrm5lzmBAAP*6E|VNTp~pfI270^g5>dD|$i~#H>4gzbNN0-d zX?M*2yK%?DF-I+j^*pCpLXzc4J*I+Kd037zkEMi{-KD7w7*%-QRr~k(;%((HvGKi& z-=i9TDmxCS+sMTpIRfNyNqbpTVo~R4@ZN~3aLLk@CZ*b=JW@8K+(r4pfmoMn+9m(mLcv#FoYe^u2(YTFsO)U96i< z_&LqPC+{Sz&@~KrSON}eMipG@>Z4LTN^ebtkLNHxuC{Fy%!~)S=FV;Ry?Yre#w;1F zi2Rep^0dz~dTXkZQxG!`A$Q1jElW>5Bj&I9MS;ce$5-IilXXD=rj~W=>||EVt6VXG z=1*VT$)h8d1rTe#c{YQ~b=(7d`Dk1-vbmn$uS17~h!~=gx0{?;v8= z{3eZ>G>`0;%0zDX5#3e2OP}K$Rp}Ae-yWG4CMvJ!FZmLPLX#qI5Eu7d6M5HnFvxZ- zsTg@>D7}!ulceBk-NoDRo|0eP;Mgj6gu<}z*mdAqPdu?Z-Px2D>$L?BGCwz< zBu;lDNN{0E!gYNlQWI(qc+$NW($-~VmcPcG^5l<@Z6PBahIu{)q6EUGkq5mL&ck?M z?CCmeC}=6Y{n#v(`w;HUvvLB-f&%N*)> zCliBvT^%U55NqJ=LtFLhdvsK8Nq2tYO{*iJ+ua{>{Kx~JuOAZie0d-xy#03$W;dSW z`n1;HYyA7K%Qr?eql!A=F4a=ziCaRmTWec4S}3bqgNsOKMHfHZ>%?8W-^YGTT`YVfx)aP7A&|FrBdc3H~3XuaB zrZcAo6_SLgSMkXDw7Pc31FWo*zdA~^*Ntt#toxt5HD)Wtdoo50a`Icww>tOwZo50U zLi8>XlMlAKw@YbXt5(G$WT~hBjb{?D7qWF?fg_0 z1`$XVyOU#uW}lHuo^?1p49?s2l9ZFK{q+vQO|?3e!n25?df{tqO9Agkm@-3Mb~4l$ zT|1W8lDj<3r&iKf}Mn{-!;Ue(1m$Pu+PFsVfr z=S*h8aK1O|AiQc|zdq)ozLt<;2<@b90nWJ@f~fG?HE1G?M|&yj;I6C4yrx$c-2Lk` z85q}hdeA8UxG+p-ryoD?@?-u&9chJkdtZd1Vil&hELw^&?^Ngib@OMYhmL(01OwZ4 zF*G|g{t})R$&#h(xaaBHj9#a`@Ao-XAU0zOV2t%zZbP$Qr3-sx>IP-kBaBED_x=YT zB^O@H@B2Rb^W()Z>PCKwmI?#V6#M2=&?jjMo)=0N2+QF1xv94r!2k$4mG~iq4z~Zxc-&xa!Wl{^2YV+l8NwlJv=AX9Z zr2YY0X7$b`DN-{%k!9bKU8~&&;&an$e+foyRt|aM?n;stOK=W%wZt#7zwdovU=s?} zB45Nc0LEA>;f!7OR)D(UX&w=rnnze$^1L;wm&JjFbzUFe%x$VFwRl_4%FNaOzMXsA*+B{ z2O-1Chm8!K{goy?xJYrgiJ4dE^<&eh0HU>K)|isF?0pR}Nx^)^r}@j=$0v)+$5qor z^vfoK-PvUD{JnF~TE|`fE$*X1!l{BU=4x5Y=>#gi7&f7oKh*OuS>BRcPl0fM)GV6W7uxOQRT(JI2`vf)M=K0yAq_UVWcL)NVY zRmvD~O|QIfBK1l7svZ57;yAbRmCK&6l9Sip)6>Iv@hbGGInw22Bt`16J2xoaaM^>m zcf1Xsi?u6K@bTSZ{muQf8yRE~;ZaP-8yHu%b*)~GmRSn_Y7jFE~#H3)EY?O<0kMEsNCobde=*U@-sUqH5;EEX6f5sw#H`Tck=;8YO@> zk0rkbF*fYlzPCh&sp{k1hv60vQ+)g>XGJYP_5NRCIHE5ds9Q-DESflZkYHp!pLC|; zCAK4R1snPJC67Q>Eh6Ppo4ws^Q?9!~9dkt9$)A%Y-X>wuz)SpOSh*`XucmMfBJ&o( zFyT(X>gS;N;ccf5YvM6~UWxkIb;*=(D6$bIr|AM*6UKF>5gf?AzoCpBtvUY+mYDY$ z2tKYaPHX$H_Qk{4EnB|2BXwr5?(u6=Rh|Jvt1IsdZnkoicB!RR)_oAagRs6-W$*q% zU+9+5C;LzPh0c3f505|Fxshqym}qxE&w5wi6ML1Dk=h-1HT6AgL*L^BN5z0SD+Y<@ z>6>5NeRW&4MLJ(??rFF;$idrc5ur*sA@WRKA*;4t_SgcZIF%uUv!GoDLG;eFsA%@} ziSzMD$`VrOuCR3H9A(?DR8+saunkXcUz(hu$GHNp`HQ(3C3cEj$<TybUh9d+fCGBwmItG#PI@ zjk)O!+XLBeI^-nE`&@U!VO5sfwg->Chj-fO!@w=&CXFn!@T6H#F;rgcadd#xFS_);h^lri>hjge0o8ah=<43s6K>hb-Ma?? zOQz@JsHrID{zd?*_tbkkzbJ&tBqY2DMDBhW^e#JCVSbY&d`4u)uZHJ_oaiE1^Soz4 z@7i@<>Pp*$5Q|#j5A3U$d~>atD$H~ph*4aqmUrpWLCOkDRY+)SHUc}nRtwjT)XLs| zqKKUstoW(`2B*=zap|Wi4S?}{nlIyyXNzrAu0+|9(QS$kgR~T7q|5{x3+u4X+(%ae zh45H{AwWus^#u!~5Vc9ynxvV)n@gF`=Zse)DNHx4OGFPN4_3>3{)v*(!Rt!z(2e3&T^<*GXl*3y2JU3Lm@r&|7!e1ld-|IgDSHRS>> z0)H!zBQ|?p8!6@YGT$R_?-APE4U&-g3&Rp#8UNZ3HAD83n+33UP+9T4r4#}0{a6jy z5JcNR>V(i&+r`jm34oqcho=fUft~s9P--N3i@!dDgoUC zV}sceuSzi%jD-j2G}nv343>2)V8}(nBT`%|>8>P^N5q;7i4x5s#M91NMqki9!8atE zDKYfRVI}l?cw0nDlzlk%+1*y?Dtti=FuFZ6!z$y2M7=}_wd(HU0Nb9FBXJ$5&&Fy$ zWHoo5cH%in`%CkuKw^|EGRRhr`$GwD0RRR(uH;G$D(Fi3)x#<_4T@!Se0G`bc4ymb zg-x0c;AvU2YpW$iNrY16>VgN_&f-)whe+AU<^PiYkvF9UB)FL~hHU*L>vpy~g_DX{!+IR#dR|K`YIU}I-s z`%jWA26jevw*LVtSalfM*=)B^V`oy1NaU=wB%*A#Sd&t=SpVDpt!>v^@7bQ+@(+uj zb4+Fp+rs6la2Wx71IxprBOB8xzCk|8Sx7l#d4+=0yi)MFMTY*kI0z2(OV0Go4Ty72 z3`ZcP=xBh55f=b35db1?ZeSQlzPXi|?f$K$DM&sWvHM$mzTtnqjIE6=z&ry><4Xe@ zGq61ud;5J?d#4g-yOZK?)A)jV`o8fgbUgzbV;F>_WJR=8M9{d%2{K@zXzM7Pr)mI) z88DTT5CC#VK|?H2Lny!SYT&*LUbw!wm65S8-85|TU)a8p;kEvAJUtS)mK6YVePTdF zbqr#^I{fSee7@Zg2z=s~SqCgq(HY;0XxQwmpJg28zVqIc!Z%rC`jD`Vn9 zTWc76w5H}3M;8Vc0I^L>49+!xpW*Jxz<1vGU-IQvmR5(~U+RDV?FTX+zi|Y5h9B_W zPrs1qoDpfA7+B-!oZo}3iGhvxz5^8_zwB7%hD460m%mi3KcR`fGRtEtBNN}hH*d0( zzdNxq@39?4c@)dO!gse#bvw)_ZsBHYG1<+F|lA;Bi{)F|ssAi#$FrS11YQk29`lbmuO9Fo-i}kfedl z8lQ?fwBb!zX8?M6_ZWFq$_wF{C=)at-2SCaF${9=&D8z$m{#B`CdoC^scR5D(Yh9m zea|0w{oR3_q+7L_?Xp$L8PI2ktgv%obp6Tq!b(9?n-P`V#OFD_ySJfs!(To)MHM6{ z$~r|+Dkr~;nZgi-9c!`XDsyIdD6(B&Ky*OlFY=cs-;eKoSh2H@+bf-~@Tyes$G<-u zAwXzgyxE0B>WC3PtKuR$6|k=UvrCl<^BwUQq#VhV22fiwibb-3@iQ~L4+FvVbao1g z=;Vv5x$yKLJ9{0K)8O8H+G!}Qf`sK{8S3gzW|mNC{L}T#So1i&df(8-a$)(NR0GcB z&ILc36Cw5^^+la#3_KjBM`l=E#tZP;!d2}QX~;9i*g$TT4nkH4=V8j+-}Z^s{~T>4(n{Vxp}SE{x;Cd)S+M-K++n z)iUJ()}&wQD?GAz6~f$UUh1&c#a?AFz+2+{7W9i8LdL8YWbVQ)^KC{dTMxT8nWr~D zIB@;!ULGiSY!m{wh)2X#<(%k%Y-q>SzdQs3Ku za!lPg0;Mo_1;60imA@z_x4%O3-^bR`^A?-CWwxQ#0x;oLU(il;%0s6 zr~|6js`FtmQfsOENWAnw3gI@__aAdf7g5rtDW@1$%CFF?Gx` z*jr@k9~5YNYxxJV&|TMZJq}m&2TB9T4{xG+nA2sXH&C>CKL642EEq&V%m?wQ7LFzX z*KmC?U$S-AL&Re8!XSg-UgS(x5mDk?zDJ_q2&Kbn_|J>hPCzY3wZ}{dpth`&Y|05` z!b7h7>q>Dh4?g63I&yW%Oo$wq2c+eK1A5c4>US|A&NQ)(QFtrS1lzS6qaAU`TV%LBKrhXt6K_H>XCR@@D#6>|bC+s}gfistVGiQ3t&_;G=uXfOix~G>}#?mJw@6O2B^6s!X)UH40Zy2*pKM3F> zLu|8i#Sv0j--$bztn$yF@J0aQX z2roUUXQ>#94-q1WPIoHVEPo;u_`5$~k%oel9JdCTb|$Iy;$feaHF^HYNitGq%>28o z?$%X?1;^+p9hJoAabWx(d}M1r<{H8|jF`ROY<@80%?tT}%N zxtak!St<@N>InE+>$x*jFQj}Sj_F~0jR<4TajOedP&LBI zbj;(x<~qT6&Tkyw(UGsN49aY)uthO#yq0FS#nlX9ZeK=f?l9_)!eR`c!`}`b#%`Fj z1;JkxEV?rB7!|sr^qWED0QN@^x7WH`Ic#3szfcTT)8yZN`0S34(}*XuwB0=8dS`|3 zJcRza4R{8Ctqys%pZW`@+v=&BnR*Me1B`edUA@S0iWIdkB>@RRZ8MBy!osrqp(egx zb#koLqj4<`tZvb$JQ|)P-tl7icS@dfCJypTG`=nom}f=|Mw$11t9{@d<^4298(Ic` z1Rc*t#RBK^X?T&UM2Y!~;BtB@DB~>u5de_?6b%S&f+N4o!^rpVa&rLiQlE2YY_$s4 zM@5Zibvc{#eQpm{Z!@`#B34LW`lfnSCuayv>L2>CyFN^cm=@qLln<1yT!}b_D-@m^ zAbob(06=>OM%580JTX~G;_2XMvHPytdEU}>>YUanBv1joVV#m7o(>zUGWdDefkVDr z?+&}8WXybt5bgWL9JS{p0v%Kz04Eo9!*c`JPWUggd~8r;u5bp{2G1`Y;@dt+HF4XF z>YKHAiDCB(YRCBrx**&@QtOTFqHnMZ_?y+fYB{D}WmpOZ+;XKlCpCtwAgoz5jd+gl z!OW8C&p+TWpreNrIjJcwl$E8-`3jC>6m9ew)(pEa!-{d$};)kN}j6?ys<{5;A{DuVRlY9w#dsJ2H5ti>91>{w~{N{snEUt zU$lPjXbgC0+r4WcSvWA56iwF@@*`p1$Z=itjQC4yC*?|Qz#P-5<75OI3869mDZYZ} zGauC3H9dJ8#fI)-3|Tcw5aZ>JHk-6=-!yO!x%sX+OjU&|s1#Js^p^nTnt?c4g98wF z9nT&?boPOw+b+4fI5l71fYxcYDuzJh84T<+ovI1pXa%CR1moKs4xzI99yh!e!}T}B zKPXiB&@J;yKe5GOG1*)Y8sNGgt> zf?HjoW8gtQZETi)e9bX1&%*A1=h3nc{*j*AOi2D{6A2ylp;kTfSqvpa`4co8E8Gu< z{Ys;Zn4!~&Bgy5A8Y_!oqc0S*8A|t7T{T&HVNRmj_V$z4c0fQg!9v&)VY*Q%HeqL? z3*;2AV&JunUy|)gpw6JYPhkHl*`~etyoh>ruZ;|lIUWd zl!9IEOB&{&)k7@m&}3f);nKK-3G9V@m**=Kk}Z-?Muvp(a5P(8eFf0eI)Abq5PYd8!( z4J0&WivY87Ji0W|KKcoXGFCIHFi-HjaJ0n(Q6Hww`wMf#N3@gSZ#(C4C!G0m$_Yo| z47EnrHJZhh&jX#nWR_SCclR^v`U*QUVeR+TWkal@xwi$z?$dLd;?vB<%V z?Yvc%x*?o_gBQ~JJQu1rn~o_Zu(rEEKB&ctcJ+8PvmTRcMSGvYF#V36!9}oIhU=+} z|D~(%%+|VhfYvGJM`x$I++3)NFd1Jmq+UJ-A{J^%!w@qy*5a4zN8af|4^`sHu?lc9 za@#`ldI>mVTuu>G)CPM!^#x%x@E~CzBQ`y({+Z=iRZhc|+LZO1dW}xs*`}VtjM>WsntY*R<{q4Yd^FtD zR%-HRmP;Kg#qonZW}>9s>u8ea#@LF|E`&@ji{!+cNPCQO;Oi`19l&I2wPV-}|84iQ zGVmR=q0d$+x5cruIjW)2^;x-)4 z$nze1K|l(K;g=FQx$r9tU00hG54y5IgDv**JbI^Nfc+pfmN=3e< z3(M&;+?K9>rqVOu`LV<|BW5f(m~-3AWbc4ktw)UuA)%Jf#It$-$YGwk;fsqHK)wzz z5{Lw5tW+Vf+T_lbT2IH&t;F7&DES|v>vz`ZJ3O)_-VfS?JBL41LuBMzJr_gVQTsOPkak^>euAg?lYu^U)PWL zJA%~a-2FxO>7#}U z~B58Vfl{+jYG|B}^0W6(T8MPm84{;^ISVPdLZ4T5w;- zsqhMY=fvCBdYe>L<2e$%PYkD@KDs(5!Zu`aoIDoMiAeEHEMQC8wBN>dmwIkTxwTt# zFhUI?QSH4~I?ONN`cHd;K(%FLg>3x%M~z*;2x`G7aJA#5%-zQR1FzUsHyI|B<04=% zGA(kV&4C;HK~;To(9?Hs0^? z85kQKKHt*Nrr^|`fDGcjAX9xR)3g@aP=jAX-RP{Jj}gvb(nTq9d4GoTIG&2%m|dgA z>aY6hg!kDpgeW(d79V4O5j^LJUlpOujr}x25mLM!PXYJ3hwD@A0_zb8eqYUwS!}(~ zC;HYeTd38=0_DMaEf81dzA50z+cVK`&Tf8hglM`B3UggA$cEtJ;WMizZpwsW7V=yZ z*m{iXgWT)4N_Gywa(dLF&PK!@{&ro<#59;{f_DyTGf)!#yMTEOzij9mSML^`1WA#4 z*~m&F30>+iF*7y-)8Nzsvh7{c8R4OK%_v9l^eg#kyZKbEk#NF39O$B3c&#@A#?2H_ zQD#PQ*8kVf+6e+r*6;BHZH3VSwo4bGn|&Z-3RozzzHnVrbV366&R_DL7XyTKr)OiV zmk$%R#7rL$dxxrCx##nLqB~hIlMrJZc~wmcPU^}e3VYA%YZ3l=H4z80yKa)&2R_C$ zAj#-;Eis4tw4nauZ_iVYYt9jyrI6UyE2J;v-+er0T{cVZok>j4Z|g%N&ZAs7K(cuw z1J0e!TCu#{1s0O&l-gM*ODG5IN1C$9_w2DnT1_dUsL8+5%tLE0u>bQb?L<6Wf|%V% zZ=1tn91-tCS`LXF-U*R%ZtUohzR5V_!?yrD!!%VR1B{@I)^RSVVL3tyi7Pi>2mf-% zHf9q;Wyw4eT_sgI7sT-G!y3#rLwzO~10&5N=TfdKQjNhs1AGE5IpFV8a2^uLX99b_^xWs z!(tBy-XquJ8x%tgexTRKXDq%{H^+`sG7hXZq^~;`Ew1Hh^jV!pFghjW;8?s6M9MS= z?wTXUG6E9g?PRA+h4vT0 z5|qnN$4F=a6hFuqbh&<&oNs(WQ+rPG^uXV>cE}c}nhr6Jted6MtNQ7HT8MHBFHy>_ z2%tvSQ}5i9Y1I|8!{j6^|6nclRsKBdR4CJczb_wUSX}fLHBAjn#P3E&uZU_;SDI9I zXU|CZ8b4Y*rUIjhJ$z!hP*&V-IOl!A+Ox;>3Xo5vo$+O9V6oUq8%MSj3!6;?QQd@f`Fl{7=DTsbnwV zHC<@E%Tewg-K2dbiqE#d>#^Etq~B-Lh>@wDuS>reB{^%NY9xQ;xe7x|RfTaEj$~T? zkkPI0>O;U%ebj9yG}tqXf{?pzP}m2-gXEs@a2gqW4Uni_z{-(UV0c0>fMIT(2G zW6m0O%@k^2B5a0vuI$64dTd#0k-WRQ+#N=_u>XjlL*f{jKDR6;-h(LbUyGqKphPl` z?biOIn=W*|LvT8qke#GzjV8#c?2c#EqQ+SmRzrJ7g|xTut(2zF8Nt(Yl@%)OW(AwS zo_DH2zVd<38ixD=U6GTrUE5u&+Y5tITjFNFGOg+CNGf1Fd{epMg*qvkptq&c`2oAr zcko3<^l?j0AfAC;=%)R4PQMhU@s&t_I~1yHtQB{eC@#pFR2{mg&5(KJNp{_^cHX#H z@!V^#T^eyEh8f6*g^q0q+y_&1NY`UA4X^tzmdEpqr6hV28*NZwzCT?2?K$aQK5-G~ z+`gh-Sy%~f%O@B;FBVX%#VO=V1p?d#;sY~#c4@dxW>;kG z<48x8yl@`6Hh&lYzAVScV5ARd%!g0Y>RaUQFP)s)z4ahi6$?<)b$1DGd@4u&rJv=R z-hx`A27w2%u=rqm7Uqz5uz<^(Ehs}PbW+NBAaOi{l+rQ%J8#7DMo#2T#a5GntQ1*q zV&ceTf5|Q}JKg zfN7PAM2i;`iWqL1hy!y;kK|`{-sA$Z<{=fZqNdgY5_L!iz(K}Uz5Lx}a>l8gKRPc_ zLbgF`sn=IVwyCZ3s##m{jKl!hBj`m*H*QQ4B3!5tn30RHSBn*&3#yUm@Ll>D>ioWh zy7di3np4ZK6$8KLmxo4UXM@XkVtYkj&oF};%cmL^TVF@1+6b?PR)C)x{{rE!m&tX0 zF1`c~GB9%sH?Gk~>FlE>EErKZ7M+cOSP4Juys1Ln^ph_8cAcMM?>W+OBJm7j+Xk`{ zia1Qf47F!=&7Z&cMUh8l$xAl)r-8fP0&g*P2J@7wyS zK2lWfq~MCFbs#N7y4`B03xJ(BZ!F-ebAt7bEU$|Ly7xpukFYzWO^$A+&y;wE6}yMYE$AQrYw4auW(b zbV>a4NGnTzvF6S#{+JJB82#I07;@)}GyydapS)?8cTZr92#ls{v5PY2iIqJXe(3Tp zK(|-Q3MVMc0}k}W`<(fQVyn8Hc+W7)g4^E8>|tH;Jxetx(9ae=RSwZvku!8R!qt_E z17|I8j2TmzMLTqn+ZNq%NFbYA`DHv3!QtiwMJln?q_aZ^3afg>BPr#NC(ttDu$*+1 zYCcqxnRM)Tp8yaejHl?lXoWQ!9?|p!BrUt(-J@FVu-6|pmuM~B8 zFkc;k8$>OqbesB7mQJvDuKH=D9^HnS>`*R}+9NDf-$ zQc;j*#cfTg68TiLm%Y!Si3HMcBk2vQ$9j((!KB5#=3`t5RDIy6HuE1x8xSIZuNA9l{seoKS<-sSok7>?5*SYm%7P*G-6a6|6f@sa-b207TVSN-?i2!a(0_a#Z2`t~528*%6 zbh^__9i1v3_$;C*F)QU<$kgGxpsP93bjAL1%OCN8U%PdPp%Bbznf>6nbQ|fyI==k# zxv4b;14i8;&~$d+t&@0PPGO0K9~`XS5b@lZ>|wb_9b_tcdH#W_>>~*t4tVK`pFdA- zeLoJvqkC1)Jm1+|`}Gyedh@k5STEY)JYxYs`)n!9XOEe%TP_WM&r7u^kPU1xW@()E zd-n~GGO!;4SDl?UJh0q__O?}#=*A+MVNRmrlWz@vf!h0Pq`IMs3$W&JKuOh`ICH|h zvI*+A7pgBv2m~|B0)a~0bsq4ubx^DNWXQ2|bMc^#lT);U6S9dlCY55m#jyo?kjr(i zY7YEaqqZNS9@~LvPe;9jGiPW1&?@KQXfzhw?V-zJ5OJtcZ(B~vW@qOb3@<1CfzFCgCRXS`|=Zf}$j6{yXi$&oSMb)Oc-^{9Q4uGam0zH(nN zWi$}zxP#T?p#qggsSpm6IH{Nse8I%A(alSM+ykOV0E{ARU}KuG4mp>ysNYtg$U_)t_9nf6=Ck{!x@%Kn#t~x4T_w5V?^XpiR4S z<>^qG#u4Ij{L2QyI0F!c^fz)-b;_|$O93c~!n)#u&1q)M<%!NeS|0I|cDTlNz5vxp zZ`ivutu8s_2o!DWHU$PH>z?KMIYxsAOA1$uukRhxB~H*4qHQR16N5t3AIRIyx(R>E z{DM5%z6pmbUoIQ)wpBaW;VoL9?EJK{>+&XrgkIs*y2LnJicf z^X%KMZQ{0$#`Pqas*SU^u4k#AeydyNI5swm4gzI`C-yb>57aqUZsG?XY$Li zvD*m~39re!Cm?j%2X^|)fan4`9v`QR-TbczLNk6h$f@8&C!uU$7&MD-0WLw!>-L8e zGVR@a5R4@2(pwb}sRBB|BX+^c$&dWNgKwiGALvn`lQtd@5^@y7dp9)gQ0EuVq!6$U zp~)wImhWYs<~?nJeGZ4osUQ0U$F(231EU_OFL}b_AL!2J{+;v$ixQooP(4+~hiMRO znh^bRPp&1=+7xCx!dm}A8(5jCh(kINZTRnpRstwtn`*?@ed>^?KY8Y6w+BB>gnp@c zoiHgeGc<~sPt?Q3)K7GpEHXL>FZTpml|GcU_{-Tb#$SY?#~(RW;>&g%+uM2woRs;T zC&0xf9*AjW!5KlV=NiwbP;jc=P~E}lEOZOVK73!Js1_7y@H-m+%x z_LcAuz8(2bG0$xsp+bXTMMH>%ZepS<9($9scUEW01`dHmR0bNm&ouGrR-zhBu!wqw z-{ki8kyxu-xF%IfUsij<;+j(pz%+16;GlQfN2yz<%VaY(tOwvK&pEv(sPBVr7%;Vp zz#Ebq6O70Mnh;L2hzLrP3xCW0N>UY}fojw?zo)NDn0XHj|$Q}Mnccc zEDS&yhFZvl?G6;FsXh(4nsKvnxp4%9@>Gu|Z(&$zO@JqG{-zD7SvqLrAHt575hft5 z8y|%jn}&9->9G6uE#@Kl*MPt&VT#Uo`j+Wb%mN_z}%PP?BDKi z>I;hGx?AEG4*}xJF<73+#YDJ{UhF|5ix~4e$?%iU|7rjrhI->8n4VnO%a*NIu4*Pm z&A)?`5t=&qGHYtx^$kj}DegBI3k1}km$r~ez3D_;25-8oHzlQX(~^^J^?OEY>Za&k zj6B}x4b3*QMr0H*;vmnIZzcA1sjJo_WIT%aO?%=M{iKS_kwTLni0UKNtm4i%gYXwl z0EGhFDm{;x&ms=(0!4BPq%Wf%#A@nd)dQ1t<|399O?~3lz2@R;`z<34E{P!_ppJaP9WD%LV{ZhYwDWeBfMs(@GgOQFS zgFpnBTY8N71yH~6GzLZ(A-kBtOh986D~3u-)fftznvR*X#AHpAip^g_8|e6lF$rBu zjWW1Q)Aq@hplbG@i_jjebrr4V&>oI+)~<_!;oelu7J}=~nGDSVt$!ui`AzaUlkNU( zScm6Ozkw@>0@vzJUlqPP1y_h9qqO6Cv(F`6#vnB!by>+nw;Qxh%eP?(p8rb%l}t0X z$t%k_e7v?lkvIlV;PK*C#GpU%Yu=>vyalIn6zGQ^Al{?MepVU#Jyg%!tHxPVOdDfS z>RJ1VrI*)NpZV$}v6(u(pQmc6YF+Ug>`hY&`48nD<&4vSX+&B!hTaTHy)K*bL84TY zwj8||LNj<4W;dvLZ65D@HAurVNTuWS)9u%XS|>l|=I8ggjFE|{oV4fw78`fP19M?nu($XqbP&uM z^8Tpi9sO5$#`Lq@54?w3LenO{!>B59NvwMrg4aJ^i0qzEM>yd-^4dF{6Ut+~^n#FT z%B%pH5nB&#L-Eb6FVS*#0sQ>tF7ZYQgh7k>)0f0PIb>Ex%2y!u)g`}!+_sT=bvU2` zdE4m+d4-l)IrEx*39SC(K4yY|{8!6yIq$cC3gFdwr-e|qMf=9gdXBmtDQW2>A_NtA z{X)b$)ze5cHOY};m-KKo8?_P8k27w%Alisu*m#&_0XDTAil3nr3lz|jIi}*A&grq? zw`RWsq`$gMf)|o6;U${7zsum=-E_bs^eN!gjdi7byyLS~g<(^SUy#@g5F9JD(;yo4 z;n2st$HtgVzHV%!`QJrSy?N=D>s%NvT$NM!$-zKnHph^oLSo`7anj)S=WR&k^PP;` z>5t+3JVanho&g0|vg&(#oV_RItafSIA&rO8`Bp%IYUUs{m=*i zca4H5B5M!fm90A}{vC04s_1MrA5g`OGx8|4f$Fh%zMo2;aQoKvZP1r z;9iF)qzBU%`e%WDi5RZssk00bSEj%Bz@}`lP2B?%Buw$(okZBwjoqDXDH2`a@m^c!R@>l;siBm#&vQw1w0<;i^+wk0Rye{3zP{o zzbf0Q$$|$*v+>x=a{uQ&3-2(%5b1YyjUL$;)b%Mye$pXvqy+lc5UvEupF90UY`!RA zXuWFr6CZ7UWANIAExpF?egA97TWG(XWClL9ae z55+tep~=QQOEb{geXzz6ziB76P=1yb{V=DK$+ki|c}^sZTF5GI?&V4?<|2ps+Mm^D z^-ez}6`$57QJ^5M5-*3N6D}hFr_1 zDai*Wi!`RxhdQ=to+IXRaYX94;_L#%1BHQ?N{-U~fS~A{B(5@?%Og zFck?=|4}jkeGYt4`>Z(U1<$t+kF=Aw+NqeO(%K@4Q?8BoFq03{WhxjN&>EMoH-ey# zaWu`@)yxiLW+8*DYqua5LFsv^r{ez%vL*mARF6!4AXyZQ-hT=hEP5;dmYU`05cS$P zb{VpQi(ud{E9Cj%MwLz835PKXd`ec}71Pajxj8Nt44OSfwtp>s5xbgmJCP_`)ApZ^ zpU==n%Wx$J;kDh}v`rGb)G*nkAXW$MesyM08DJ1vsmLZy?a{L(?4lIZksF2RstV^P zUXl4c17L+_Af9Nl`~Ni10#2C*vzjRujS`EjDMf?_VGI0w>Yf`DDjO@!8@4rD*2N5$ z4HP7g8~sX`4OAr%u||Ol1*q8{6LZ2yJR=4YCLeZ!Kaa5XeMn1DrMy%P=qjb~S_pRw z434BorVj^_s5hMC4g1$u--_hiem`q8@^}FUPQ9lyQ0AB;M=#?%(8Nf>_FN`t8#YM9 zZz_g8qxL@i*wilTF0c;k)z2CS-B@F%@l)10?fd;}h%->$zI_evRg}TIAmXy1WQa8Z z`OGE}UH*WmxSX#nnE`8U?}A>c{B0#B~$k z?NdpTOyMc4&WRTuQz;g^H^;AYG%l*BaxjHBQAF!pT;bo!Eznfd z)x?>iP&`JBR@=8BPW7pNAZ2}`1M1h|!Ic{&t{WmnJ-8~eek>iY)ahR%BM8Ec#F0P} z_RjLOR%@PZOG4)T=#UVjKs{Y}S<+U0;`vlBA@ry#m=C)V7l0)KgG^g5zZ=Lrh2M%3 zrhm)F^oEYTbgRp|ScIjJcl#&NZE=%heah0HT>Nr4rQ%eBDQHe$$z`<~MMjh*m{n-E zrFF)@zILq8Z@@BXZTNAK`nhxqi7YV9niyYF_)k5f-i4pIMY;DE8E8PM=X43%g`FXb zSEs8W5D@((9_qxX>9HKXXt61C&Y-&sa6`i)VDh^5T^hC%k^(nZ(DEp`=S&_cIEjFe z%UVPIbubf_(c}9uzn+jDf8iXGTdzV6iBEjXa~9tq{`2DD#R#_)vR*xsYo!F@BPHWeO z*M233n#phbdN_`AZ$wq^L(9~nb3$dIlkl}4Vc6i_r4=!);`X1RK%+S{RtNGE7zBG?$_#Sznm1iKIOE~ zr`?~j=i#IzF|AXcUhx}n?lK&`Ja$+5ZvJ ztPC1%T&#B;-9jYCbOk=VWkZ7de?QC8Ob+Z&nOfe2uilZ7 zOFm6Rk1KwM@qew=A_6~QBndzfGFfZ<-xE1g`aJ190YWd=CqW}RE2HQpP8MrRf={&h zquXPJmfN7wWaN5|M&=#6olE>Hi0pKm+6>tHlk!q(X$XGWU2fA2=QIJ8jRDp0EP>=d)*ngG8!!;Qx3W!?3g5!Z?qBL=Sy8i&jC>1tg zix0|50uc60RkT4xKvE%aG4Ww@YU`r_Pg}pFPXhZ=#}8)1C4(7^%HzW#EN?gpWN70F zYtxIOMMA{owV{im02jlGSFIlbhFfN_Qm3fMs)9jb+BY{~ObrVmy^dZsUydl+UCfiV-P)!M5eGXXi6HL>{dq|%as)nw4C}iTA z*@6RvOH%?)Sd^;HO-o|@o;_fXy-a<=^i)W{b9;nlf{fl3GcLru8vnN8jfpiU)@+<3 z$}joB)%GAeto0M_+z0)}!i06}2OTo)pqvMp@)!&G!0_f@kcw?4WVJHI6E;Sw zFPBhn_rjpKsr#{(Y5$9=CJ_e~xoTI_qnDH51$8#Nj0<`|cN?10likFQ>c%2I+? zd9M=>V*#HR18i^NA#jeeigxVpxpn+sT^$QOzWf}-)YQX0Q}lVBYDRIP8?dm-VlC7X z7x}Ef;9K@HK>Q(ZDA#b0P2^G!`%Ie$RE(o)qp9@VA;9)A^)c%0Ys7nX1RC`c5WQ+c zFzYd!QNCbE9wD&vd_g_rr2Kh%zn+bNLe*HPDS!)ozPM@CTSV3f-Vpw9&mkburEXCX z7ut3ibTH`!JWu1|C`*nIJdy^O3IpK-PzpwgObX!??N{?53D$>dSXoPYmP8$;6QX{@ zbJTHw4Du;dT^3FA9M|u6t2A5g#gKSN-ezY{ve@zqq}ZEv(E~HW8QI7Evbs_{Fb$)8 zNK!I+HD{TR=RI+y> zwSbOjAtQLn~tAe6!CZ^l1c{J>yF{S|291@U*6m| zITK*}+zJh)nR?_2tKkZ%Gn5#>Dtq4cTH$+s5868YkQz$Jqb|e;xjsSKFJ`E-M0eF) zmnlII2p~ur@72oAy^wror_pC58v4U1q|;ug(l(?Lc=U?VVN{;IiK{zf9_oW5CRXS5y!8CW?4wi%z=NoPpn zc&e0$oVF1vtn_dI6JflubWW?9f?zTE2>>cLgBUGiw@e1gCt6KG^Fn?tD@66+HCqv; zM9r%r+%_Q;3vH(;7);fwN1?wlEsBCQ!z@jE;JV znuV1XT9X#`Xq3~_2y!oH>%(Of93C`f{>dcGzAx8(`9!EcuT@NVM>?0Z6;k|o-^p}o zRe}Pt>enrSP`I-!eZT35fno*QKs=qS&KkEJvb4|9WJ$kUn-y2R-ZPyC=V4Su5*kb2 z`eU@SZ@h?<4X=E23Yz$;(bI zQs)QW)U;+s*_4>q|I3CwuSk2dXKksEDa+uRyzo4dWAv5ga04y}B`aK*CodoL%v$bc zAuH0@kAA~>TN{A5s<^T{-VyaCi_nuMAzC>QC z!4w>Kb|c7y%MQa(_}VGo1QKq|8Ss)m@E0u~D~W+nnCvWKl|>zy@r zQK?p38y!L5HC8avw@_57r3)z$FdQzB>p#`u$_pIpE@@LLc|ye}PPJ|ZsMi0&yjE(H zeTy>io6u&Bj@Zfk@2AtNg(jCm2ykd888alDwr9vAQfLFdVFI~~9PG;&i+-9_!c!Ql zPt%!hQyLv=Lc+fo<_Aqz*YpC{K~}3{@z_)^MOgqW$2zL{Od?2O*`FbF=&Ak^mE5z6 z69}~et(}aDdRuHG-?)r}l2>+3SqEjy8n`awD=;MNl8#sM)}5fST|q(x}3kGT7njE?Vpy0uzG?AC#Q!W zyD~MJ`;IcZo=TFXi?R6XSM#UZ!+D-06cVouZTKPbzk?I_#{oD$d+lzHq^fWJR^Np) z8~y&8gYI3&VWn|Pd-J1uhe-29QTejZQnXl)RMi>Mxf~f?U3+fb zNGj5|Ad0AhZbKn)-X|KshF*6};AL@?&cLHvjk6N7FDkaQ>qZ-(FOBYeJGbU6B z=dDGpU33|&B@PTR+UwuyUzVY+6LUAt-2qnwf{BUYs0w%3h>A+>c`;bd-_)q9Ke~l9 z6Ymsz;+ts^2;#J=SY{c73#}JzU>)(1b;n+@G@w~XKuH<88uDs|au+!W;SsqY;_O!3 z#Mq8BT#sgnK3|zcm9OLn?3K@DvxwS7VxW+Tt0TOEJw_)c&6Q*Da_O9Q-vu(yvA%A86BTfjL%n9wx`i zGKPwd0~BB`@w$66Xl5Af6Xo5p&=H*jrp-Hv!Jr93Hte3~xNATX><2~tE)4JGtfF_$ z+EnB@Fioh1xKOe2cj^}~8tlt*(Sr770foCM_vAD!hbi1okM|ntHeFOcE zKYf1cUtyefCM+{}k?YM8fnFCv)Nv$e<5qw$)L3Fyl+-a1esj5WTSW1dWKFC^GT|KM zM+KM`*NgGN-tZ)((!+QRw!9+X*loweaLRW&5gH*KF>L|O1RC?{$e%i^u!ziXqDf8| zZxr_Tl)r5IFBX``E**UY94}7c_PLv+IgiVd9CpTBfUT55ovv=l z`COVzchnfNKk`zS5<=amg%-wSrLM&lded!b0~to*Zo8%_tw6Z%j!Uqws@MsGNTWgR zaGXO)^VXpN{<;Q<9J1z;H3a7(|31JDdlU7uvaZ;qsN+)l<-1q2#<5%G8)6Yf+bQR>QpGtB_d7`(a<`rv~_~@RuQzm;l9^X#n{=?_}-g%}= z5bAoRa{PV)tB?0GtnKO(Rcth2b)d;KxeG97(VmUQ75PJ98&2V{O^o}5*+KsPEPue| zMh?=DIuIa?$o-~2q`Eg~^A2CzbkrY-(ER2^s!gPs&}w@L{pV!98?4fqU(Ga!CZ$Ox zqpyW7c+488p5YfL0a=&XMOBxmDY=YAsxEsFNdeN^5k;RG&L6(`C&Ia*u@Pkg%u z*wyXiARlBLzsk<^r0+ofG~PIBN{!XjwaiWHk7Jw2|LNMCwnR~&?b!Bi+qP}nwr$(C zZQHhO+qUgaGLna!`w{i9YRzIAzs&G;Ul4B%E%!lFPoeCFMRs$&&@mUbSm_~9`qzjS zaM793nOkrm%+Q|NXahTRGCvtdr#vkml=rWrl#>B0Wp1)5-)!o;>8Evav188ndBdu} z4k_Cco+B#G6w;=LI%`OE2D$vAJnUkM{JJVgIr+tpp*7EW|7hNHtan{%#W3h?N;2md zx8phRx^{%fzfSHmoWM9RmLzubV>)QhBR6c3u%Baafo!XiEu3Ttv>hn=7$FF#e6`AT zQc&q&KEL6uvSp^;E8hH}w}YIN*uW^e2E=wl1XlrlEYdvBq7SIGf1eAu3yMGtKPC|r zvp2_+gqLPf0j71w^5Zr)2q!t^yzSrQuK5_R;&cVCqY-7YCk(i7w zUV-3lCY`i|FVSkuYF&Cnr4J*OW@`#k_~Aei(Q_@>gVyO(6zc2zX6AF}^JyjBOT`Jc zGVKHr9>j2a3Q3?v_PcoX@k#`So;hnlI8g}!uW(b`W`Z;OgDWIU?S5<49-L}q?-*Wd zuE_RJ{E`LO`qA$RVe;i7%2GD;?C{v>CZG?4Ldzk%SSFZN^Rx8D&?MkIp zpn0PM^hvR6Vi5pH6St=K?{?fx1Xz~i)1dKn?&;e&uK+NH*KwK&r)XKb4cU&T@OoF- z%zdkJO*Z{a%=_-{%6&_iGo^!mP@j%}3?p0>M9fHm1KTw`u}Ws(+EphBB$uAs-V9L3 z{vwFSzD)lrNv8Bf+?45}%fwh#~IbY~dc!0G8Du}akZOtvR?4hp%gsaYLE3OTM9q?d!K)*z{ur3;oW zaE43i5ps>1SEu2m=DT4$ZD7!)RQ2soVUQv&UzbnFIaB>JwOBSI%C>Ve`v3E%YQi2mP8y0?M;etjG4~8k?r#(N3)vU~Jgi*x=Dq`JF>QRIm`nYCQ z!KMkBKx?f)L9Vu&(ynKqmZ*$Y$}<&{@m9?Uu2O$HxJSB=R3_A zl~t$5{tWe0E89f4{M9Z4;o5M!VG$rUUQQ+PvuF5=qAvux*hwDajgB)}-Q+SQUY4Ab zqw;jaZBN0woNzfqdB;auN34)ql%T4F12)TR^0!jb_d;a1Jw~~7YHT)1!ZuPVs=Edk zAe(!lG)#aC(U^LeshFnH*o6UumEPah(>60#v66?eb+u%cdeVz*Kdc*8$0=!7`8~U} zd)hio2{s#)X&LL4tPE3YWzgl@2Vsq1h*e}7zn2ZeS9|(WO_wWc6x13z@KGGWiko6tNOBd(=`%VJMxK+w>jO=LkUXLmVSc!TxLBz(5BUltG8Ec8 z{~uriQY(O6Dv-H#N}T^=B0zej$g!b0*}ij%6=2|wm-VLXoVfSag3=%H6}Pn&Y(Y<0&~jThdpuDJHL~zgURAJ z(mD`yp>!RGLlfJ0pUx_n-*a~2aTbFpcMo_a3^N6^hVMfcN?*^l%F2#{tRK)$wOE8e zS+_j&2hsy*F%6IDpw6fW3h6nt@T$_of}&{%G8a8n7KpIL|N5&hY^#)Ejdr{dAIt-k zR492GQ6jsZQ)m>F?_LpUcdFHy)rFN0YeD|n3r zL98b()crsfNocI_&$28Q6ZMXrDQ^Ia@)8k}69McL-PVXfbBSL<6=IyLzU&e~|yZzis1IKu@#D zvzmsworG|E9LZr-`aO|E@+gx6;Lxn}23W9;+CT)OuspWQdr>HNn#$b6{SmnrG&g*A z_x;^S_cQ|Z1Tv6;ydS5WXqVt6&?8{X8Ygh8fz8~FhJ#)G&;@{)ai@60X=ZP1Ac+Iy z-EEH8IqSI>u!C)Gw_FAqb2iyW`>K|{|M>7|ca``pO$}YLOv;h8V{xWEvu#3`z*9jd zsX%i)P8_%ITwMEP4+RE~?ozF;3|K`;@=^*A99F&Isk$kd?MUStJ}2@kd8hEs2FH5d zXL}&9zp}2d=}!t}HY8|3@7Nw*FJ#KQ8&Jiky-+kO6+g6#$a^p1b@dkr#Er5oja!4^!v-0hZ~67tVwXk{N42(=e?9{4m7o{rH8=ac&D_`<**Z<8Cjt zXn>tY`wdB+JC(*@H#B?SKMTkA#f?bd!%i;dd5|)*zS7cmpwQnRomCkwlG)YV(b|{OKn*id zflTaP1`=n{;zv`-eh_TtVldK5{EB#r6SXWMcK=6EiF&;_L^6QFgFI95Cl z&rd`oo^a&dRyGS8^X{5EBqobVFQ7ktmvsh$=ZfJtiSTdlS?)Huuw^}((*^dWMMU1J zB)uUnLU)|}@GUy4M%bbF8puBe?qp>F?WkGd`Rw`MiWcgG5#%;L;Ln|ZCJo}B02Q0*Cu}pJ!<|U<=Vf&+OI6rzUy&O!aNJbAkRg^gvGv9j z5-+z%4DW&_I?#{XAvS*|I+?k|R&0I$vii$(VP!T_787R_%wKIE>YnIws16eO}uOsl5jpKwOGA`g9*7(`L=_#hj z!tha5(NIh9^Rsv(oYus;+J+(~R_>w291G7?KNTix=GZ%fA=Qp7@d4>2DU7(fzR@h=Dv+gclqKhb2z20ol1S{PT!!#?YU?CzE}i zIAJCum?DPsXdJb5Xk|hzPDJ0_EECcjTutHuDD=xJFOpa;g zSk?nT@?2(9G{9i^?GiuO5MJX8kKkkISAFQ+u7c${6D@8yDdhob?TII6rSF4YCalW* zJQjp=r8~{M$2*;?vMC+tR(^nuQX_nhEy6Q!orgsL9yO-OPRD8vwa5lrd5{Z{zOtA* zw`b^90u|w8(3HeqzF{Q;+ZKKM>`sI}Nuoz8^~paA3c3u5JS-kgv@Gv*Q9awnI|d~| zgZE@C`PcH@80deNJ>xHBuvf_f+*iQD0iX{HFB6o00Oj`)*y`uyT%={z{>kf%vqit$ zo9-$e`I+Y%xXw%jQQ|bO+BRonxglBaS&TA=IYNSpfftB;-5F|vZXClNQ=P*|XRc2+ zQ@c60mj3T0uhtQglfN(;Q}ctWj#{^3kQ}M@j}=Ou;zGK8aaDbo;@T4+0Aajd6}z1! z4Oxca`jvUIv&&1EYkkur#%$pMPjG82At7*P1R4>DYK!qcf43=vE_`4f$hDWj&#%bT zPI*Ep3`wIVXA5=mpj)3kVG>sc-!!J$_eo-!V=0rO0EmVb`p6na_IO^p4N@;lO0CzwBEin|9zX2lVe3|+IKVTHw2D+ zaDY`#QD@M&A_Pt%Ln!S<_Ge`1A4et<^I?b!YQ)WE+~fd?OO^7fM~3`>+n)xSx0P{& z+l1GG=6EeN$QS57=ZJ>XGWkDhtR3Dw>*RRt>+B5#(sXFl zft#JH-AZg_a3l{xZ&2&v#+>H{rVQaNFr8}xpSF0uP?Qkpr|NFH^hATapJR(bTa3WQ zEBPHW;o!IaQBOR5SkCqBK=-R!(+qfbH>Ig=s_OkM%FUa?WuV8cRnVR&^b^eyp~{{f zzDbLj+Y#v0vCr!^P!>?|V*rC5A?1j~3Q7D!aJHie8Ap~IV4H2-sA7^G$!1_iz9dfM zQ!efy-L8(xY<`|sbC4%Sf#+gSJdr(manteb{Ge!CbyF&xNj!H>?$khbBN%_A2DE*S z&TFJZF#Q?HvFKaJj#<<&;f&B_DE8VRcB7@O0Bm**;Eev2LuE1QB2=RMR_9vcls7oy z7D<@DJb-*|-)8oMe;p>L@`KzhxF>4V#P#=;Ui_RGN!MDXE1&Bg2HbrA8NTRSj-UGt z>~vCY?6Va60funikuD@sB zSS?d#sSbwiQQhIS($mOX>3DttgdD5Os^=1uGZcoSLK58xab9xtv={oc9tq^g>c%)F z26>u7f#6hhizp6gFL&(^51G;xaflv0w~CLgN*CJu7SbCiAuX1~MMC-+Khg=*89c2V z9$=h&_4fz(D)`?PIMTTN@xyq;ZT;Ef|6bFq!AQz|53ORhC~6J8$=~$#zj~czuuDg5-=6Q;^iAsj?+bn~W~rL#;a|!Q}Z1 zIVcZ(+}9;4d3R{HeKc~lz6FQu0h7v*6|!{27Fjz&Xs+m>k(G4)@3xVA_aVL2*i596 zd_f{~D8rHz3sbRVD(Z(^zpj^Ye~Z;RCHmHat=%4m*1Iwr3U-WgM!Xg}-n>%1pa)Ai`#jj)!Beuoii2H>`mCY*X>L(6P(;$Jg~SySUZdG~*-C*igbL zq(!EWEn(xv4b*U89Q`Hxdo}3@2F4wR2UgJs3c2od1W{JE!%|tV8a#srG}4cYA9zu3 zQvH9Zj!r(`E0ht~jD%9yYw3j?ox~qkDm(r@cUCwl%5oa7EK{Z5UwSn|J7HQZ`p)+` zg5fe^342&x$w7*~T++BKq`c;c20hK4_8|kmRYT>E0*A#t-AcxR`_x}XzxbvU^wS+r za*y}5fa1*fnme{_2oNyozJ|%&ngQ<(>0{w=yo8?^A$zX#xWDk z0(7oR7rJJ&KftKq5j59+r^3x}HW8j3w8*i%_?{{fabaJRlY=&sqUld0J3b8eIv+*k za|cVwUV;DE_dkKc@cHF;pUp0xTRf;5aNEdsS4+x6(L}|{-fJUP8SY@hfC;ZBqHYH* z-jdOQZk;S@hwHRPD;ZWf=@$5Y!qh(^+1^KamC{&xVap+_QX+Rtj~~kdTeRAMr(M?X z15YvJ79?fp_98k2dfo!!_q;CemN|U*UpL-Y_rY_i}o> z57A=78LkBtw_&3(tHFMy*$MA(2*DaSMbTOBENZ1k=R8`IW1vA<9%KhRtGXP6&G1+6 z4Nor9Pkq=*VG-?WIE`-**Ct&rT1ksL091bjoqZ>9PaYy#x>ad{hxOdoW9528+Q!jI>xx zV!scJ2Cj*;5>96XUSbHbkij828L1++Y}#G7irN<4mbn>x07&LeB^^D`dFYy+`E^dj zAKOQ<0)w@#>o-*JR$A_wiq^QZv|fe0xFwNS9OF8qpN&i$f&Pye8P;ZvACKrpp9Ju( zcR#SVT?qeL&)4tZ95i>Wr26<&Dba@hXmBf;yuB7$&cnnye~rHx_2yg_6bR_Vk?&cA z6ZH+TKHpoQ?xJQv00C2Gq;;{*pvj|uC6=bVA8()Z_eeZ2E6+lyH3nvE`LmAQA98gfpp*GICu zM`9REPvWu|Xt&&N;qZ$_ss_%1q@EdII4OoRi$lWnQQ|bdeN@3|M=2`l%65K=j$ z9@xiNR?UrRWXK;h02)75(A>)V*jKunF3^Q0wu=UTg;R>zu&PhS8-fbPj-ybcrjfN} z2RDg=W82YgfX;#UgD{QA1Pp}xcyC9UwtnuK)_ zj!ToR>KWyza4RVxlbmf{uOpLT*n`NtP|q$~w#@{6i7yLW&nfhPi!AX)x6|mGBIhf*}vFWNR03+VN zD;JBrLQ7|6F(4JQA^N-$X|eYvCENhE)ok^46yvWU!k>l?-4k=jv{ify6R*g}>`(-F(J&)PwwMsZF{V zF0mq$X+p!<$*2RGq3{SWY(T*yl?}V!-w*69U}$OWhLnO682OcpFZvmN;^;?_aFMNT zzt{o(Hh&tsfb#?3x8&P7hMDHf zOUx80Y63$VF+3<$?&<@TjaHZOXi%iyb^dtYRNiCOeZKn?7oA33(0vHj(Cs-6VN}xd z!&f*L*4=GqN3`=Tz;L|hUU(n`lDLl;HATIinDA3dU{0R!sQ5nX^bPZsM0F=H*F=V9 zfdg)yZ&nTHx*&u@*z}sRrj1E`^zRAjaMDB++M?Z;_ZpnJS{QIjXAt8D9JX5OSM<{& zc)#j{{2B%%M>kT|5pv}e!cVX`FTtImjrjE6KGxj+sr4?q)`*tJvLN#;6k8}JMm^F{ zf$(ew$~#~cpeR;Kq$;cI9O!#62?XHz9D%V;3SK(nt*^}v3m&vc;8y55HUW;rWmA5) zQ>%;_Aw802wBHw`k0I06P)RQOdkMw_2W z-F2g`f7J?dR``eND6$q6&s~IU6ekY#>Yb7rh+=<8)+RmwOQI?oFSqsCVR^WrhNxyZguHYKNN|(J%hN+ACyq-MhqL~Y#@E~8Wjq9S{=GFB*M{J|c}l+= zHCOw3^>Iq`)ame(aV6E_A)S64*AUYfXfJu>(|#rxkaC7r*w{1fvcoV@IzEG0IuCGk z>9D`i=_cYN*FsQ`7FJP!emI%QNMxD75nXfF7Q&G!XQt!6C4Y;tDWtoI`7BAd+ zrH#xD`l^Gz_pb+bQNx%*aGyk?RCw=4;cS!~LkPpoj>Y-v5=xlPk-gbcW2m$TOu z@n(MO?0f_x(C0x=k#Ij6Pcw7drJ(9eD5{t`_e}R9gv!EqL?v$^9>qo(N9zO0%x`N# zs6eIyc_X}x-f2gXSK_;7&MdgwleO~h)g7V!4#b{dQ1m&~l+8i+#v%0>|hGwZD0uE=K|FOYO_Wx1{rT0aax5RC*^Q?((q&Qr}bk4Tc) zjKGd=u81E;rn9M??YbuFz|72>ZQoaD#G#LyOlUj(ma6-RBoTlfwIm#nCzOyDr)8Ps z^mQLfjf}$0brIN%=8vK8qv%Bsv3exN!Olk9tV}2fQRxE-?RX)MBSNlly6y-o`XJM7 zZfG_ahA9;&2U30~)OJp@Y?FU{5zlryUjf07=7K!eG;zUk-}v@Ez6`r(0P?bC-$c8C z`;IhFj$LgnHQSUj27yHdQLtot&{Iu$rl%y@&@V$|y})md+p1Pj*?n7bdDPSE6Nd^W zXt_eC#b?cnDG2CYS+0P{x^9nDl(!CcJ{Zd5tS-n63#W4--o?alK66pu>uRE%G_OzW zu?cx-1>avXczttlz@Q@b46?#24-Z9E?Zb5oagCjnfS9@+>XFQ<4@^IYYg3Xv1N$p} z#-@TB2*{-MTt(5G_#h^xiy|=oq_a14X<|SwQi&RML9K9Au;7l!XRu&YCW~hMdaEe79Jo z^M^TKG-zr?2Ek~U*XzWJr|;72j7h^q(ScO&!0n@oe&=1)X7bin{4YSD| z`;Ns~(@8mRmFYMf(+l>)5b@YKy@yjI$qt=1QRmfJokrDsMw5@&_#MFNX# zBY`_dIgcdr4V{G&&2vEW(mAV2nIQ$7SEV$~)ixxUu zf&-eQtlDLFz9Uq1ee%uJ5?T0grc&9a-^xkNJ?h;=^pvr^w$}=6QkGG16Wm-d! zoak@1I`o^fX`D`e*>-2Q9yO_UWg3o6o_nv#mY703ALFOl=EM+#`OO>Kb5F`&Vdv}k z?1O~i6?bgMfshE!yl{yuJx!Ala$kVPpx<3Wio(<0>?Ps7K+w z%pxKQrpE`*G+Z_<{j60t5I%Z!S9K`yZ||yOC+vOrym3J}_uQAYu^~S0qwZ(6^t}#H z*K6H$Ce~t&zkAX?cGc{hg-K={P7u_oL?24ut+0+$p**6HI}y&MGDEYJwp_pj7<{5! zahtLyMZKOy{9lj%9D+R`Scm$w=r24`g3Ol9?P#n*^fvv1F&Pyq|s{!nbr zTT4nX+ny;i!M2Y5IcJavtP?XP*&^aDN{^ns2a0xQvQsVihN8p2QQ_*Tnz$25Hr8~~ zL3!-ZI|+k!Mx=kk(XluHW2|hPwv*_tDFOyh87xhwzc#)1yXD&Mx8iH1fUe4Q4iX7I zf@0Yu(!MR3-$t@tgextDu_OXCh}_YOZ@X{YcS*I%yHVimT9lqGe*uW6>c!2Wq2%|o z#@`=Se{zKjDLC0z@8l&TbB!*&s>hGhI9~|V$0?q&@~WR!7b`WS zFg>}|d9Mv8T`?tTPBBdNDTCIUJ0m=z$2K3AYGbYV7%>ThVNz|I6HL+3`uBmMx*(tV5|07P9(J&u>nm+p151A zl(aUeluveTq$xqA=Xrqawst98c;@c4JU{!P298nnNT{bYDgQ4JF#2t!J|mYP1RAE8 z(Yaf3UMx4ubV;Pi(Fo$+no+0D*I}#bEQv7pQHTN?{1h2G_$G2{;t4HNxV$87!z z`6XhuWr#>^znH`{sd@RS;=BufQgIpRkhHQ{}S^2O`R>K!(;TePMc}I=T3|OfxWe!<=^t z_U~<03t;h!B(&EmZ{+e3Iyojf?Wo{E+?#m&hKZCl?d8K8x(e+)$Ngzo@wR)F=o4S+ zF#A={8w~7mvwEc$aKH%EZILyA$&31_uZ9mN+ZQJQZ4*W`^^D1-rrD=aC%FK%PK?u}+0Rdd zpjEzf{~pN|-Dp3`&bay8In2lElKMz9Fzcs%*1z?dtW7$=dhAgh78M~0PHH;ELWhwe zn~l#4b3v=KB;vYkvz90BS>Hv13p-ylA4(yyua!}FHHTP*r-H>40+Hd6cJcf>06$^p z(cjSKOksHElB8`rd`?|(T)zga=RDm`xk%`~`r|ebJ785m^z1UCLI)nO1o-lE4oTh~ z#))+cb#_X6gm>qk0O;B_PkUUN0@f_2d@MSI=%SGD)w=U_MDQhJjg+pj7zP;{>KYJ& zVt%P=IH2?kD(Xw|yo|*xwb{{|w3kfJAKcFCxD7J(mY+$J(WPHW#1RzLOWi9@&B3)1 z^4kGYI(33XZ)O85095h5>CFryaR>ZrbpyyC%v3*CO}g|g^f+jtvOX9jmfqQJ0=K9X zt}eoDCVjYOO~Qh(nZ?bzVTBqjGPytsut=wZaKj4*a_8ec5Nw7 z7XcV}3b+KiML6!ud`xQaPqCfe=CwcFQS?a}>z6uvJB<0;;w|F1wrZ{vD5aS{G;Sq# za>3VkUO2h4NT_?aX+|CIe1Ok%G6fS0K9OIpdDSrlVnHK+(s=b@USBVC#9}^@M}`OZ z=u>>cO46EHrKHu@{w9*BtKrPWMNIDqgRv_T^bJ7!y^i=+IY@DV0ko~_IDQB+F^oO7t2mXOsy*Ak)286EeGUoACd^+(M z8zx)}M$PVq(T_NiUl;2px1pqqN{CO)w^<;ttJ`U_%Lg}3`3RC4Au+ID?#@Z5o8yNK zOPeja*e&4Jdz@YcAZ15y=xVf`4!bS2)E*oCGzw9uS$LT5~TSQfG&!k;zG~P z{&EUnpF>8Lx3pcW80N(}H@!VD!XH<>23{m^G}fvPIB`g{$Pi8x0r8?d+>C1#-O6@Z z;9>O!UD+^CWq?kA!{sq>rQGLRYwJhEqF|(Ip*1JiH^=B0qaY)DJ!zU1S48(^HObu_ zq|X*h`sfkvK^^2@pnpN|A|+^^a--SAh#GJ1`q;4ABs&!tJkd7zg)bO@(KKz{jg7@- z)V?>+R;jF=W{r3SM+au_ZS4R}o}f!h{EooG6Abc%4MqskfIr^;4)l{Tq+_Xq8G^FT zU^@qBGNIW?W>+M%u(YSsmJV!gN=8Z69q zmHdy$8IK)c96cJJ{Vz&q;CWY^Kr}Qoa;6DMMfQD^@5>(;cy;nQvr#5+qrf8U8I$McVS@^z$&+1#kc$psrQfgl(A;J@n8>jjB8 zpfyQg+ChnAc+vBgAia(o($k%e*d<{IR|k`{pZ@5vKwr!XkCyUbZX{pJCq2+wau6Ul z$Tm2~=vXUnuF(bBJ?}qp$TZ&T*oW4nCUohdf4GVv;4%=An>U(@mA|kjXS`QN`F5lB zcx!KvIyhC>UVD~Rl?ga-(RqX*%Gr)$2+3v5TMD(c)7f0_tiG~b&1v$i)6&QQ^FXPP z(!5^SK$|BJ&!P+S+zjO&Ye6laxR!0Db=8gxWn#3kZ7s(dDhi=QIOeH7N35qKvI^;a zL84-JIi;jDA;z-e@Gkay zQ_EH+J7tSX4Y`61E4g^VXS~_8?fU8OG;_zGI(OF7viL?;A?kOlC(`gpN>NBvW$&rcwwNbwNE=8LLS`1 zm{RN}hZ6o0to>X+Pm=7kcn1RytGZFR+9u~cK)4vb6ws^zAu)2Kc)a6pF3=}}&>1Nf z)bMzq@JE|})IW4+vk1virsGkaWr2%blcwId11{BAO|Z@3BBh!L#P{lsOVdhzJcC9- z=mL5)8?T_$Csf+u~uURl^a))^OfIfkegO|-R8FDbZRRVhd zj-2DBtv`{G9J0oCVmIE}ItkPgb!-B^@z)D;pOy-=x;jYd^3!l=%;TrWv)8QDq5G<7 zW3$rJPDQeJITlbpTuHQfNy(d+g}9vKhVaDGw9c0DS|2myZ_uDB1LB3f`@#Nf1Y^|% zI2snX_?P_o0q^%JyL+6L9o_!AK-4M>>-yNKkoPQ;`~kH5wa^z`9I$w<7%6CD9^isC zF9-2hr;Z4|5vy3|{Pz!j!_Z6;e?#&ADq*)|n#2T|*hY zY~zvCH&`YaD;X|Y^dd0hP1&!0=BH47yON9go5AGYdv}dc!W6j{qMe;1Af6lruB|7@ zx_XxLyEy5yS7F8klD7G*>_fD^6jXhSgC{{y^UB#;|7RzJIlq0>{$H#Wey02tJBahP$<6OzemzL-}Iby zdYP{|6x%Nx>9dsrhm0S`)_n)wXAG32*@_TaUtDuvE?q33{GdwP%`p}^!=jK)wleMm zMfoIK9lt&6u3l~=!x4Z_Rx3?PL(q{KRQ1-}_!Xj}I`j9OC*ssbR+f>l?PO|F!@G=x z>Ju|9&Y_zx|`})MO$31l30?AGnK) znu2Cj(PdU7wKUg6R`!a;I-13N?VDp_UMmCTeZ1tFaNohfmd|c z{$=$}{$?S!qY~d9kxkj9uK4y8tA=6o&qIt4mWx*NQ%VW~uA`=!itz@nz9BBGC;1gu zq~_|=x{HSz`~`5Kx;mORuY<1ed_F`vZSM>o2hGJdij?Wq=Si8Y_Q!rR52ztVE3>sS zFO#R{x$}irdv1I<&7~`f z6o)R|J6;{IB8IPY$!}e>fE=piUj>gq8g(!SY;`&Vw3fjppHVplJ#$z+YdaC}aDO*C zI++AoRlEsQDGF*FJ6{4AzJ>F)@I`hiu_FJxz3HZl#yi^}BfQAM1Lm3nZnW!|8i*_; zZIE9Lppa3}?ff58*7S}N#9Fc}$ivUN8Lu~R`xDU$5y{!lpp@N}zLXh|$a{2Pyesii zKIC?}vH(O)S)WYERPLxdB34s$M|hZEI+QZn9qLLayrW2yC}EeH0LxTw=L>YmAQe2k1sa#*}fMllLMr^M?<$;^?cw1>6gfkVE2Hg4kc2Gw7O zd@5R)KZ;kIH>}x|4_t&;-eVBo#mdtu>AMca$E)4ua;M|7o_SA3j>L!gWw~$M<`l9i z`R?52#*V}-vMDpalU7U|i8Fd;IbPl+*k>0jKJymN9Z_d$UQK-5ZZhKAplZ*Z)vIW( zEp>%lwmKo=XSI}ZEMvYbWYmm#ZYc1-yXfA>L4E=Km?%^IS9k&Zn|R^i;`nF0C`#Mx zF(GvxskkC0u7M5`{x7^Fnm4M5NpYfx%RvQyzCGZgRw(l!NoQ?Oblku?Je@i5%qlfE zqdttRrt`TZcQwSHKV712ZvNGBGqdF3)zDZq=j9WqUGAc7cGi7brPywGxZW8_Z{pRa z;i>bYz<&GP?CZyW{<-;?nINqI%fG5~x-Zv^ig4@BhlhVM>~Q#f-`X(Ye)#dI%_2L+ z^&o>scT-tySvSIgty4m8{`W?%b@^vm@I@{IN}Q4tm*)1d8pkDnOveW=V$*6vFoQb7 z>ir~sR`6#$8^6eWa#x+_rdQ2d_1wmHDLHO@`fs>YDp%A&s?1J)by(1)e**R+?>^n? zq~_YM+s3Nuj^?WxKB_)yX5;SAH82Kg>Z~3vr0lcn1R^#T>|XE?3-=R2;r7^pMFBr@ z(n9!RfPREsQ&T|J0OdfB0Du;dS|QPV`Ud4)iJW{y+BA{OS%AcngMi9%XOw+=TS1tF z|43;)!}aOc7&yj?gWyABm70+0nmW#gGi3~EiU#aPfG{EUV;A6}205JK#RTudnH%!L zC5r={a(rt)ZvL3u-L#E+w|}0(slXQ4jl{&|(X$iW zM%$lY1Qw)$?}8s<3>*dNUs*+|zIFvjZgUPdifwcL!asS#2zcwV*2gU7r$8_tTZw5r zaLVwl?!ZQQO}t^=P)>bH+}qd(-)kHWm31Op%(nAZImT6BBxiQle8zGW!Xx%6UXY|R z6)R0voJl&&@^6w)8R(&Iw5;5oZm@a>SfG? z?2dr*O8t3Fi^|!3Jf6o0{bhNcyFN$7=y~-BO;4+is*2=fVe@vrkQwt+Ks{jpaBy7` z`2*7K!hhgjA%N@u8Uf?hdq0pm-e`>xW8*;e2v&lih$mN6_iDt^@)osw3F?w+A|tpC zu3X9>Sy9G-7Z_ArR9plF-v4aNI4)jXi10Z)>P>GJGG36qb@Kc~M08Wp##u+GUQ%wt z+0N2gao{3qD%-hnts{JuA)MTm^bjd2w37&%M*L*n+2mS;8Y13(S+zJ5* zDY2STa{ohU*Z5E+*y#Gzoo>3c`*GgiIB9{6@w*xC__1Qs*Cmyv>UG9P*W$^}b#0mA z{6K`E{KYV(#~q0_{EkbU@gjj}H&RcpY`XY+x{kWK^7EGlTB~l0Zp(%PkHMs$wPBVT zo1O6+#pj5moKBkj!^v|uW55tMX+WfSL~_yt1Y$r$KcCU!K~;ffzzp$#%tJOnVSeoD zQ}f1f{~*y~fMUl6Dv%TgT9h*_@7d7=P9f$iV(@^_V_soy8zJ-kp6@KMqA;C=vlRtS`TpI{MMC#;IU$Vn+d{Cj5Cn~r0XgALS$S$+S@Ku4zc-&%PuVv9zzK$< z=4!J)x#7~+G1Ehu_UI5jr=OYGiK8mwUVfoUZlWSvq=RB)Y9`|6M02-s7?^t^*L>~b zS|ah>#&asT(m=X=YYWxNcufJ6P#Xv@#Vy-Gbln<@9jgot+iDnXsf0O1V}>jfQ1UOv zDtZrnRLyV}X-nuowp=q`2LObCWJ{CfW@6+SOLOQa`5^xp0a=YuUS`s{{jK*O#dbVIN6x~`5N$Fai;Sonnm;Km=tR=u{<)o z*Qc`^(tqYmmXu|dkH#q9oXPz4a0A^q?sKEDcT;N#?I^x9SZ64q2+!n=@^OxRa0%|5 zf739%4TpfvO#yS*!Ax}NiOg!Nd`+PSvE7^1n_{pO)uT>8WjMEt5^tl~9H2hsGC#4j zA|2UE-YvbXBKLFszzfASMDJ?dLeq2-LpY>8;*8&gpcw##C_Jvd4L zfng)?+MB`gkPDq8$>G@;|1skU2t@3?Z>5!1S-d*`6iv-s&02pn&GEa@ZPYL1S_xWu ztKJ-~IdOrgPH2u^VmcY}Q$&|Gdv#Vxu~z9$*ByNcqrdY&PicF%VaP>)>lc%zOPszz zFEd4_Gcv7>_`QR%4niR;9oR}A&^m~WPKS_GTLbuArE6Cs#gtlLIspEM>RvAVj6r$1 zgeWySEUdfzGA}b2mTupajy&3zd@zc)nS35I}qg02wfw_AVrx5YKoY!F7{MJX(U_CG-yGi86O`DQ`Cy zdZ*ZTBr2Ybi~h};Ql}%szFE_`Lq1A2ZlQ-Yn$b_e=r&iE_iyIoIR!bCT?g!)wtXlb z{!N!|EwUVs?+Q%5xzerMTO1@xS0NTv`E{mb0c+}sni4mo%FH`kjXQ^ffg8oa@~VUUtd>64IDs+ z7zsBazC1s@g+`A&zFT5+zf>ygi)S&yf0ana`@G?`IGs}c1UAt{*4$$dOOU`>5ZQNah*YC z{e~Swl3GQ-b+sZ4@aSD>Hms4QQ`PD;mMHx?`{#FMSm7MCa*)p&2o!nbD)`i2A~#pAV`_1+8@VzHODi`lJuxM zz^>Wk_bD5!h-u_Gf;DRMK7Vqj(jXR~_$_ecNGs}#fFE#D*{zsV@@>+J`n$5sE31k( zKAJNbf@ainqZG+hff!4vt*H-5RnU?HY^V<7ZF=O@w_vf!B=c<9;-b`>==JwBE18Qn zY9tPz59LUH-NdRc{I2jbLt+F#wpom7`}C>XMR2P-M0BQcB^c(URSLbpMp17&NWIWs zfpc~qy85O}OZcYecFpNl>t&60QX3bJk_9!!0W+tzz!=Vk8I%dDV++6V=7Vwevr3Ga z-2Iube*P508mYNJ-)b5 z=|=BH;N8?F*{es{F<4JBYYoE>?Y(@jGTYv)P4C+eIio42_&q35gUF5Q@nLEyCoHMOo}yUShIh&?o+pQ;%>Tq=Zq7& zLhnR#B@JTU%4+EU^wGNiWwZNmGq@ZreIrn%dP?xG(9H60q8Y&P&+{KoC25=d@A=Q& zf2a6tMTZ~ z+0qwGt!~!rEg6|WHjawg6Ac&s3mne#HcU*qM(t7~t!j|v#;?iL5QVHKRF{j^w#RC@ zUoSMOIArNDAVnfXqC~qt4nT8(MSoaCdlKCNs<>iaet?KdBszhJB8X;@>dG@oRLI-G zNBbLriw;twV_BFBDd@tBM&_{>FHotI7BX=8B(w?nShz+!7W3Z?F1XyebB6rX#)GjC zZczowx5H0@wiRjdgZ?9k7jEP@aRUR;v-DM%lJ;edLKQgLz*(`!lR5KSCYKq#0cp{% z{yi^}5#FK)55AiKNaII(!|6rse;_qw(i9NC*46ZiIFVE#G$^b83>UCF^)}cEyk^i0 zXuwnr2IicNn3bhz2PQB&XuMhaV~N#&MrR0Tr8a=|P!GP(&g5n_5>BTV@v1hK0>FMk zgim!9Jd49L2G*(bV`eiUH*if*iYVoOu+cvctuThm=%(8fmz=4?dt?Iyb>HIap_o};OQksgur|Jap-fsKLr z+bcQy?^2is|2W8hH~v3ue0M20>6;rFIR1yfie`?M->3cEs%WogYindk#3-s~>0tB^ zrfy_yXrpfq!_3b1pCYIDed0uns`5t0M2u2K)+XPvU;{9H?`3A`_kjdec>rvDnY1ZCy zzpt>l!2uDlFQ&n45tMZ?5~>!I&^c$nL@{EC7y&3zYcPN1AV1jhNCpw<5D^imjfQw3 z66ydC!rn7F0_~}eZLTAp?OHg`q?3ya+$6n}bbTSVSD+8%kEaadzNUs*#vyW)FQTFD zpR!@g3Ipc?`cjc8+~7w)-C-|7_(45m?}!`6W=PAFSV%9N!m3}^y?!~iiMYrS6yr|& z9or!14>5k>$PnJx44loN>jQOr3LQvwI7<{Hbh~cQvaJt}A#rW*mKIyfHKe2cammI>KlVLTJOXCIu_)~R(J}v9kblGEXT^qF?oG54n^!?&k&gH-lZjJE!(=& zY=beWk${`bB@6+78W@m`6%T63|03+HM7@&B+DUP|VMczHeI-%8BO$1l5I|#HXia?y zA4Lqe;&{7`kPi9F&`}|Y{uG?@{S<{;cHtf8ag8`#?pMjrTFW$b#*_E<_dxMEPQu;d zw(ml+P|dpS@8$Lg;I0<36Q@M$b*gWo-ndy}+il|(MfJh?=Z_3&@0=^-4$+}618f|V z!~E$T-%GBCT{&Y4MlvrnT7Ut=mce~d8Faxj7)ECw%SU(nB-}4x_#w^o{}cU0|Nr&> cVgz6u9QEuSUBCZ_VOW?sV93dT%8J7LH-D7{(EtDd literal 0 HcmV?d00001 From 8979fa5be02844a69ff109cf8c58fc621ea744ef Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:05:59 +0800 Subject: [PATCH 23/36] feat(launcher): add best-effort post-exit hook (#1459) Run a configurable post-exit shell command after local, Ray, and Slurm launchers stop jobs on timeout, interrupt, or failure paths. The hook receives LOG_DIR, has a 600-second timeout, and logs failures without interrupting shutdown or recovery. Ensure the hook still runs if launcher shutdown raises. Add the config field, unit tests, and regenerated CLI docs. --- areal/api/cli_args.py | 8 +++++ areal/infra/launcher/local.py | 11 ++++-- areal/infra/launcher/ray.py | 8 ++++- areal/infra/launcher/slurm.py | 7 +++- areal/infra/utils/launcher.py | 56 +++++++++++++++++++++++++++++ docs/en/cli_reference.md | 6 ++++ docs/zh/cli_reference.md | 6 ++++ tests/infra/test_launcher_utils.py | 57 ++++++++++++++++++++++++++++++ 8 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 tests/infra/test_launcher_utils.py diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 151bd2e0d6..cb7051901a 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -2908,6 +2908,14 @@ class BaseExperimentConfig: scheduler: SchedulerConfig = field(default_factory=SchedulerConfig) + post_exit_hook: str = field( + default="", + metadata={ + "help": "Shell command run after launcher shutdown. " + "LOG_DIR is injected; failures are logged and ignored." + }, + ) + def __post_init__(self): """Validate training configuration.""" if self.total_train_epochs <= 0: diff --git a/areal/infra/launcher/local.py b/areal/infra/launcher/local.py index 2e1dd82b6d..452c089535 100644 --- a/areal/infra/launcher/local.py +++ b/areal/infra/launcher/local.py @@ -31,6 +31,7 @@ JobState, get_scheduling_spec, get_thread_env_vars, + run_post_exit_hook, validate_config_for_launcher, wait_llm_server_addrs, ) @@ -371,7 +372,10 @@ def local_main(config, run_id: int = 0): f"LLM inference server launched at: AREAL_LLM_SERVER_ADDRS={','.join(server_addrs)}" ) except (TimeoutError, KeyboardInterrupt) as e: - launcher.stop_all(signal="SIGINT") + try: + launcher.stop_all(signal="SIGINT") + finally: + run_post_exit_hook(config) raise e if config.get("enable_offload", False): @@ -421,7 +425,10 @@ def local_main(config, run_id: int = 0): remove_status=(), ) except (KeyboardInterrupt, JobException, TimeoutError) as e: - launcher.stop_all("SIGTERM") + try: + launcher.stop_all("SIGTERM") + finally: + run_post_exit_hook(config) # NOTE: For local launcher, We cannot distinguish between a completed job and a failed job. # So we will always try to recover the job if it is finished or failed. recover_states = [JobState.FAILED, JobState.NOT_FOUND, JobState.COMPLETED] diff --git a/areal/infra/launcher/ray.py b/areal/infra/launcher/ray.py index 4b5504221b..6bc7d53bd7 100644 --- a/areal/infra/launcher/ray.py +++ b/areal/infra/launcher/ray.py @@ -34,6 +34,7 @@ JobState, get_scheduling_spec, get_thread_env_vars, + run_post_exit_hook, validate_config_for_distributed_launcher, wait_llm_server_addrs, ) @@ -490,6 +491,7 @@ def sglang_env_hook( launcher.stop_all( force=False ) # force=False will send KeyboardInterrupt to sglang_server.main() to further clean all sglang-related processes + run_post_exit_hook(config) raise e elif allocation_mode.gen_backend == "vllm": config.vllm = to_structured_cfg(config.vllm, vLLMConfig) @@ -531,7 +533,10 @@ def sglang_env_hook( n_vllm_servers, ) except (TimeoutError, KeyboardInterrupt) as e: - launcher.stop_all(force=True) + try: + launcher.stop_all(force=True) + finally: + run_post_exit_hook(config) raise e if config.get("enable_offload", False): @@ -626,6 +631,7 @@ def torch_env_hook(n_tasks: int, placement_group: PlacementGroup) -> list[dict]: # Note: For trainer processes, we use force=True because the trainer doesn't # handle KeyboardInterrupt properly when force=False. launcher.stop_all(force=True, pattern="trainer") + run_post_exit_hook(config) recover_states = [JobState.FAILED] if isinstance(e, JobException): recover_this = ( diff --git a/areal/infra/launcher/slurm.py b/areal/infra/launcher/slurm.py index cb4f1f9428..7f5df3d3b4 100644 --- a/areal/infra/launcher/slurm.py +++ b/areal/infra/launcher/slurm.py @@ -29,6 +29,7 @@ JobState, get_scheduling_spec, get_thread_env_vars, + run_post_exit_hook, validate_config_for_distributed_launcher, wait_llm_server_addrs, ) @@ -571,7 +572,10 @@ def _build_llm_server_plan(backend: str, spec: dict): n_backend_servers, ) except (TimeoutError, KeyboardInterrupt) as e: - launcher.stop_all(force=True) + try: + launcher.stop_all(force=True) + finally: + run_post_exit_hook(config) raise e trainer_n_nodes = n_nodes - n_backend_nodes @@ -675,6 +679,7 @@ def _build_trainer_cmds( ) except (KeyboardInterrupt, JobException, TimeoutError) as e: launcher.stop_all(force=True) + run_post_exit_hook(config) recover_states = [JobState.FAILED, JobState.NOT_FOUND] if isinstance(e, JobException): recover_this = ( diff --git a/areal/infra/utils/launcher.py b/areal/infra/utils/launcher.py index 973edff381..4b1e73e3b4 100644 --- a/areal/infra/utils/launcher.py +++ b/areal/infra/utils/launcher.py @@ -5,6 +5,7 @@ import getpass import os import pathlib +import subprocess import sys import time @@ -274,3 +275,58 @@ def validate_config_for_distributed_launcher(config): ) if allocation_mode.gen.tp_size > config.cluster.n_gpus_per_node: raise ValueError("Currently only support vLLM TP size <= #GPUs per node.") + + +def run_post_exit_hook(config) -> None: + """Run the post-exit hook command if configured. + + Executes ``config.post_exit_hook`` as a shell command with a 600-second + timeout. The ``LOG_DIR`` environment variable is injected so the hook + can locate experiment logs. Failures (timeout, non-zero exit, exceptions) + are logged as warnings but never propagate — they must not block recovery. + + Args: + config: Experiment configuration with ``post_exit_hook``, + ``experiment_name``, ``trial_name``, and ``cluster.fileroot``. + """ + hook_cmd = getattr(config, "post_exit_hook", "") + if not hook_cmd or not hook_cmd.strip(): + return + + hook_cmd = hook_cmd.strip() + logger.info(f"Running post-exit hook: {hook_cmd}") + + env = os.environ.copy() + try: + log_dir = os.path.join( + config.cluster.fileroot, + "logs", + getpass.getuser(), + config.experiment_name, + config.trial_name, + ) + env["LOG_DIR"] = log_dir + except Exception: + pass + + try: + result = subprocess.run( + hook_cmd, + shell=True, + timeout=600, + capture_output=True, + text=True, + env=env, + ) + if result.stdout: + logger.info(f"Post-exit hook stdout: {result.stdout.strip()}") + if result.stderr: + logger.warning(f"Post-exit hook stderr: {result.stderr.strip()}") + if result.returncode != 0: + logger.warning(f"Post-exit hook exited with code {result.returncode}") + else: + logger.info("Post-exit hook completed successfully") + except subprocess.TimeoutExpired: + logger.warning(f"Post-exit hook timed out after 600s: {hook_cmd}") + except Exception as e: + logger.warning(f"Post-exit hook failed: {e}") diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index 5efb5ae945..734b5e7456 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -119,6 +119,7 @@ Base configuration class for all experiment types with common settings. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | (section-grpo)= @@ -149,6 +150,7 @@ A dummy place holder of GRPO config for backward compatibility. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) | **Required** | - | | `eval_gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) \| None | `None` | Generation hyperparameters for evaluation. If None, use gconfig. | | `rollout` | [`InferenceEngineConfig`](section-inference-engine) | **Required** | - | @@ -187,6 +189,7 @@ Configuration for Proximal Policy Optimization (PPO) reinforcement learning expe | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) | **Required** | - | | `eval_gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) \| None | `None` | Generation hyperparameters for evaluation. If None, use gconfig. | | `rollout` | [`InferenceEngineConfig`](section-inference-engine) | **Required** | - | @@ -225,6 +228,7 @@ Configuration for Reward Model (RW) training experiments. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `actor` | [`TrainEngineConfig`](section-train-engine) | **Required** | - | (section-sft)= @@ -256,6 +260,7 @@ Configuration for Supervised Fine-Tuning (SFT) experiments. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `actor` | [`TrainEngineConfig`](section-train-engine) | **Required** | - | (section-fsdp-engine)= @@ -965,6 +970,7 @@ Configuration for Direct Preference Optimization (DPO) experiments. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `actor` | [`DPOEngineConfig`](section-dpo-engine) | **Required** | - | | `ref` | [`DPOEngineConfig`](section-dpo-engine) | **Required** | - | diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index c65794eecc..9134943427 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -117,6 +117,7 @@ Base configuration class for all experiment types with common settings. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | (section-grpo)= @@ -147,6 +148,7 @@ A dummy place holder of GRPO config for backward compatibility. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) | **Required** | - | | `eval_gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) \| None | `None` | Generation hyperparameters for evaluation. If None, use gconfig. | | `rollout` | [`InferenceEngineConfig`](section-inference-engine) | **Required** | - | @@ -185,6 +187,7 @@ Configuration for Proximal Policy Optimization (PPO) reinforcement learning expe | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) | **Required** | - | | `eval_gconfig` | [`GenerationHyperparameters`](section-generation-hyperparameters) \| None | `None` | Generation hyperparameters for evaluation. If None, use gconfig. | | `rollout` | [`InferenceEngineConfig`](section-inference-engine) | **Required** | - | @@ -223,6 +226,7 @@ Configuration for Reward Model (RW) training experiments. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `actor` | [`TrainEngineConfig`](section-train-engine) | **Required** | - | (section-sft)= @@ -254,6 +258,7 @@ Configuration for Supervised Fine-Tuning (SFT) experiments. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `actor` | [`TrainEngineConfig`](section-train-engine) | **Required** | - | (section-fsdp-engine)= @@ -963,6 +968,7 @@ Configuration for Direct Preference Optimization (DPO) experiments. | `sglang` | [`SGLangConfig`](section-sg-lang) | **Required** | - | | `vllm` | [`vLLMConfig`](section-v-llm) | **Required** | - | | `scheduler` | [`SchedulerConfig`](section-scheduler) | **Required** | - | +| `post_exit_hook` | string | `""` | Shell command run after launcher shutdown. LOG_DIR is injected; failures are logged and ignored. | | `actor` | [`DPOEngineConfig`](section-dpo-engine) | **Required** | - | | `ref` | [`DPOEngineConfig`](section-dpo-engine) | **Required** | - | diff --git a/tests/infra/test_launcher_utils.py b/tests/infra/test_launcher_utils.py new file mode 100644 index 0000000000..1f2a4601f6 --- /dev/null +++ b/tests/infra/test_launcher_utils.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import subprocess +from types import SimpleNamespace +from unittest.mock import patch + +from areal.infra.utils.launcher import run_post_exit_hook + + +def _config(post_exit_hook: str): + return SimpleNamespace( + post_exit_hook=post_exit_hook, + cluster=SimpleNamespace(fileroot="/tmp/areal-test"), + experiment_name="exp", + trial_name="trial", + ) + + +def test_run_post_exit_hook_empty_hook_skips_subprocess(): + with patch("areal.infra.utils.launcher.subprocess.run") as mock_run: + run_post_exit_hook(_config("")) + + mock_run.assert_not_called() + + +def test_run_post_exit_hook_command_injects_log_dir(): + with patch("areal.infra.utils.launcher.getpass.getuser", return_value="tester"): + with patch("areal.infra.utils.launcher.subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args="cleanup", + returncode=0, + stdout="ok", + stderr="", + ) + + run_post_exit_hook(_config(" cleanup ")) + + mock_run.assert_called_once() + _, kwargs = mock_run.call_args + assert mock_run.call_args.args[0] == "cleanup" + assert kwargs["shell"] is True + assert kwargs["timeout"] == 600 + assert kwargs["capture_output"] is True + assert kwargs["text"] is True + assert kwargs["env"]["LOG_DIR"] == "/tmp/areal-test/logs/tester/exp/trial" + + +def test_run_post_exit_hook_timeout_does_not_raise(): + with patch("areal.infra.utils.launcher.subprocess.run") as mock_run: + mock_run.side_effect = subprocess.TimeoutExpired( + cmd="cleanup", + timeout=600, + ) + + run_post_exit_hook(_config("cleanup")) + + mock_run.assert_called_once() From a315ea97d2f6575645f67411c47baf3e6c735f91 Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:28:38 +0800 Subject: [PATCH 24/36] feat(megatron): add CP-safe vocab stats and MoE config support (#1460) Add Megatron context-parallel output gathering for forward-only paths and plumb vocabulary logits statistics through PPO/DPO/SFT losses. Support per-key reduce groups in StatsTracker so CP-local loss and vocab statistics can reduce across the appropriate DP/CP group without changing the default reduction group for unrelated stats. Expose Megatron/MoE configuration knobs for router fusion, auxiliary-loss-free balancing, router z-loss, FP32 lm_head output, and fused cross entropy. Update Bailing MoE defaults and regenerate CLI docs. --- areal/api/cli_args.py | 39 +++++++++++ areal/engine/megatron_engine.py | 84 +++++++++++++++++++++++- areal/models/mcore/bailing_moe.py | 12 ++++ areal/models/mcore/bailing_moe_bridge.py | 13 ++++ areal/trainer/dpo/dpo_engine.py | 2 + areal/trainer/ppo/actor.py | 2 + areal/trainer/sft/lm_engine.py | 32 +++++++-- areal/utils/stats_tracker.py | 68 ++++++++++++++----- docs/en/cli_reference.md | 5 ++ docs/zh/cli_reference.md | 5 ++ tests/test_stats_tracker_reduce_group.py | 50 ++++++++++++++ 11 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 tests/test_stats_tracker_reduce_group.py diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index cb7051901a..34783b2db7 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -921,6 +921,45 @@ class MegatronEngineConfig: default=False, metadata={"help": "Fuse token rearrangement ops during token dispatching."}, ) + moe_router_fusion: bool = field( + default=False, + metadata={ + "help": "Enable fusion for MoE TopK routing and aux-loss computation. " + "Requires TransformerEngine >= 2.7.0.", + }, + ) + moe_router_bias_update_rate: float = field( + default=0.0, + metadata={ + "help": "Update rate for auxiliary-loss-free MoE load balancing " + "(DeepSeek V3 style). Controls how fast expert_bias adjusts. " + "Default 0.0 disables bias updates; set a positive value such as " + "1e-3 to enable.", + }, + ) + moe_z_loss_coeff: float | None = field( + default=None, + metadata={ + "help": "Scaling coefficient for router z-loss. Complements " + "auxiliary-loss-free load balancing for router stability. A starting " + "value of 1e-3 is recommended. None disables z-loss.", + }, + ) + + # Precision & Loss + enable_fp32_lm_head: bool = field( + default=False, + metadata={ + "help": "Cast lm_head output to FP32 before loss computation for " + "numerical stability." + }, + ) + cross_entropy_loss_fusion: bool = field( + default=False, + metadata={ + "help": "Enable fused cross-entropy loss kernel for better performance." + }, + ) # FP8 Training Configuration fp8_config: FP8EngineConfig | None = None diff --git a/areal/engine/megatron_engine.py b/areal/engine/megatron_engine.py index 9b6619e392..6e268451a8 100644 --- a/areal/engine/megatron_engine.py +++ b/areal/engine/megatron_engine.py @@ -560,6 +560,51 @@ def _build_hf_mcore_bridge(self): distribute_saved_activations=self.mcore_config.distribute_saved_activations, recompute_modules=self.mcore_config.recompute_modules, ) + + # Set MoE configuration overrides (aux-loss-free balancing, z-loss). + moe_extra_args: dict = { + "moe_token_dispatcher_type": self.mcore_config.moe_token_dispatcher_type, + "moe_permute_fusion": self.mcore_config.moe_permute_fusion, + "moe_router_fusion": self.mcore_config.moe_router_fusion, + "moe_shared_expert_overlap": self.mcore_config.moe_shared_expert_overlap, + "moe_router_bias_update_rate": self.mcore_config.moe_router_bias_update_rate, + } + if self.mcore_config.moe_router_dtype is not None: + moe_extra_args["moe_router_dtype"] = self.mcore_config.moe_router_dtype + if self.mcore_config.moe_z_loss_coeff is not None: + moe_extra_args["moe_z_loss_coeff"] = self.mcore_config.moe_z_loss_coeff + if self.mcore_config.moe_enable_deepep: + moe_extra_args["moe_enable_deepep"] = True + # Filter out args not accepted by the target TransformerConfig class. + accepted = { + f.name for f in dataclasses.fields(self.bridge.TransformerConfigClass) + } + moe_extra_args = {k: v for k, v in moe_extra_args.items() if k in accepted} + self.bridge.set_extra_args(**moe_extra_args) + + # Set precision and loss configuration (may not be supported by all + # model configs, e.g. MLATransformerConfig rejects enable_fp32_lm_head). + precision_args = {} + if self.mcore_config.enable_fp32_lm_head: + precision_args["enable_fp32_lm_head"] = True + if self.mcore_config.cross_entropy_loss_fusion: + precision_args["cross_entropy_loss_fusion"] = True + if precision_args: + skipped_precision_args = [ + k for k in precision_args if k not in accepted + ] + precision_args = { + k: v for k, v in precision_args.items() if k in accepted + } + if skipped_precision_args: + self.logger.warning( + "Some precision/loss args are not supported by this model " + f"config ({self.bridge.TransformerConfigClass.__name__}); " + f"skipping: {skipped_precision_args}" + ) + if precision_args: + self.bridge.set_extra_args(**precision_args) + self.logger.info( "Using mbridge to create models and hf model save/load in MegatronEngine." ) @@ -841,6 +886,7 @@ def forward_backward_batch( [torch.Tensor, dict[str, Any]], torch.Tensor | None ], forward_only: bool = False, + gather_cp_output: bool = False, ) -> None: self._ensure_ready() @@ -872,7 +918,13 @@ def forward_step(batch_iter, model): tree_attn_keys = list(tree_kwargs.keys()) cp_size = mpu.get_context_parallel_world_size() - cp_local = cp_size > 1 + # forward_batch (compute_logp / compute_values) passes + # gather_cp_output=True so CP-local outputs are gathered back to the + # full sequence length inside forward. This matches downstream + # labels / output_seqlens and fixes split_with_sizes mismatches when + # compute_logp runs with CP > 1. Train/eval keeps the default False + # value, so the CP-local loss path (_cp_local_labels) is unchanged. + cp_local = cp_size > 1 and not gather_cp_output output = packed_context_parallel_forward( model, @@ -1077,7 +1129,9 @@ def process_output(output: torch.Tensor, inputs: dict[str, Any]) -> None: outputs.append(result) return None - self.forward_backward_batch(mb_list, process_output, forward_only=True) + self.forward_backward_batch( + mb_list, process_output, forward_only=True, gather_cp_output=True + ) # Step 4: Aggregate, reorder, and broadcast outputs res = None @@ -2272,6 +2326,10 @@ def _compute_logprobs_and_loss( vocab_min_logits, vocab_max_logits = gather_packed_tree_vocab_stats( output, trie_node ) + # Tree training only supports packed min/max vocab stats; mean/norm + # would need per-sequence unpacking, so leave them unset. + vocab_mean_logits = None + vocab_norm_logits = None logprobs, entropy = gather_packed_tree_logprobs_entropy( output, trie_node, @@ -2298,6 +2356,8 @@ def _compute_logprobs_and_loss( ) vocab_min_logits = output.detach().min(-1).values.float() vocab_max_logits = output.detach().max(-1).values.float() + vocab_mean_logits = output.detach().float().mean(-1) + vocab_norm_logits = output.detach().float().norm(dim=-1) if cp_padded_cu_seqlens is not None: logprobs = reassemble_cp_packed_logprobs( logprobs, cp_padded_cu_seqlens @@ -2311,6 +2371,12 @@ def _compute_logprobs_and_loss( vocab_max_logits = reassemble_cp_packed_logprobs( vocab_max_logits, cp_padded_cu_seqlens ) + vocab_mean_logits = reassemble_cp_packed_logprobs( + vocab_mean_logits, cp_padded_cu_seqlens + ) + vocab_norm_logits = reassemble_cp_packed_logprobs( + vocab_norm_logits, cp_padded_cu_seqlens + ) cp_padding_length = inputs.get("_cp_padding_length", 0) cp_old_cu_seqlens = inputs.get("_cp_old_cu_seqlens") logprobs = unpad_logits( @@ -2337,6 +2403,18 @@ def _compute_logprobs_and_loss( cp_padded_cu_seqlens, cp_old_cu_seqlens, ) + vocab_mean_logits = unpad_logits( + vocab_mean_logits, + cp_padding_length, + cp_padded_cu_seqlens, + cp_old_cu_seqlens, + ) + vocab_norm_logits = unpad_logits( + vocab_norm_logits, + cp_padding_length, + cp_padded_cu_seqlens, + cp_old_cu_seqlens, + ) inputs = { k: v for k, v in inputs.items() if not k.startswith("_cp_") } @@ -2346,6 +2424,8 @@ def _compute_logprobs_and_loss( inputs, vocab_min_logits=vocab_min_logits, vocab_max_logits=vocab_max_logits, + vocab_mean_logits=vocab_mean_logits, + vocab_norm_logits=vocab_norm_logits, ) else: values = output.squeeze(-1) diff --git a/areal/models/mcore/bailing_moe.py b/areal/models/mcore/bailing_moe.py index 6157edb129..35d4a28b74 100644 --- a/areal/models/mcore/bailing_moe.py +++ b/areal/models/mcore/bailing_moe.py @@ -200,6 +200,14 @@ def hf_to_mcore_config_bailing_moe( # YaRN RoPE scaling parameters (explicitly set to prevent HybridEngine default drift) "mscale": 0.707, "mscale_all_dim": 0.707, + # original_max_position_embeddings: YaRN nests it inside rope_scaling; non-YaRN + # configs may have it at the top level. Fall back to max_position_embeddings. + # mcore 0.16 default (4096) is wrong for long-context models. + "original_max_position_embeddings": ( + rope_scaling.get("original_max_position_embeddings") + or getattr(hf_config, "original_max_position_embeddings", None) + or getattr(hf_config, "max_position_embeddings", 4096) + ), } # MoE-specific parameters @@ -222,6 +230,10 @@ def hf_to_mcore_config_bailing_moe( "moe_router_load_balancing_type": "none", "moe_grouped_gemm": True, "moe_router_dtype": "fp32", + # Auxiliary-loss-free load balancing (DeepSeek V3 style) + router z-loss. + # 0.0 disables bias updates by default; engine config can override. + "moe_router_bias_update_rate": 0.0, + "moe_z_loss_coeff": 3.5e-6, } # Merge all args diff --git a/areal/models/mcore/bailing_moe_bridge.py b/areal/models/mcore/bailing_moe_bridge.py index 3eead5670e..e23e23a093 100644 --- a/areal/models/mcore/bailing_moe_bridge.py +++ b/areal/models/mcore/bailing_moe_bridge.py @@ -174,6 +174,16 @@ def _build_config(self): "factor", 1.0 ), apply_rope_fusion=False, + # original_max_position_embeddings: YaRN nests it in rope_scaling; fall back + # to the top-level field / max_position_embeddings. mcore 0.16 default (4096) + # is wrong for long-context models. + original_max_position_embeddings=( + (getattr(hf_config, "rope_scaling", None) or {}).get( + "original_max_position_embeddings" + ) + or getattr(hf_config, "original_max_position_embeddings", None) + or getattr(hf_config, "max_position_embeddings", 4096) + ), # MoE parameters moe_ffn_hidden_size=getattr(hf_config, "moe_intermediate_size", None), moe_token_dispatcher_type="alltoall", @@ -192,6 +202,9 @@ def _build_config(self): moe_layer_freq=moe_layer_freq, # Other moe_router_dtype="fp32", + # Auxiliary-loss-free load balancing (DeepSeek V3 style) + router z-loss. + moe_router_bias_update_rate=0.0, + moe_z_loss_coeff=3.5e-6, persist_layer_norm=True, bias_activation_fusion=True, bias_dropout_fusion=True, diff --git a/areal/trainer/dpo/dpo_engine.py b/areal/trainer/dpo/dpo_engine.py index cfd23b432c..8cd6736b0b 100644 --- a/areal/trainer/dpo/dpo_engine.py +++ b/areal/trainer/dpo/dpo_engine.py @@ -153,6 +153,8 @@ def compute_dpo_loss( loss_type: str = "sigmoid", vocab_min_logits: torch.Tensor | None = None, vocab_max_logits: torch.Tensor | None = None, + vocab_mean_logits: torch.Tensor | None = None, + vocab_norm_logits: torch.Tensor | None = None, ) -> torch.Tensor: device = logprobs.device cu_seqlens = input_["cu_seqlens"].to(device=device, dtype=torch.long) diff --git a/areal/trainer/ppo/actor.py b/areal/trainer/ppo/actor.py index f35ac219a6..c0da8f7dee 100644 --- a/areal/trainer/ppo/actor.py +++ b/areal/trainer/ppo/actor.py @@ -426,6 +426,8 @@ def grpo_loss_fn( use_decoupled_loss: bool = False, vocab_min_logits: torch.Tensor | None = None, vocab_max_logits: torch.Tensor | None = None, + vocab_mean_logits: torch.Tensor | None = None, + vocab_norm_logits: torch.Tensor | None = None, ): """Loss function for actor step, all inputs should be splitted into pipeline micro batches, returns loss and logging stats.""" diff --git a/areal/trainer/sft/lm_engine.py b/areal/trainer/sft/lm_engine.py index 047671eaab..5e5343bb2d 100644 --- a/areal/trainer/sft/lm_engine.py +++ b/areal/trainer/sft/lm_engine.py @@ -84,9 +84,21 @@ def compute_packed_sft_loss( input_: dict[str, Any], vocab_min_logits: torch.Tensor | None = None, vocab_max_logits: torch.Tensor | None = None, + vocab_mean_logits: torch.Tensor | None = None, + vocab_norm_logits: torch.Tensor | None = None, ) -> torch.Tensor: - """Compute SFT loss from logprobs.""" - del entropy # SFT does not use entropy + """Compute SFT loss from logprobs. + + CP NOTE: MegatronEngine reassembles the CP-local per-token scalars + (logprobs / entropy / vocab_*) into full sequences via + ``reassemble_cp_packed_logprobs`` (a 1D all-gather over the CP group) *before* + calling this loss. The vocab reduction along the V dim happens earlier, so the + all-gather only moves per-token scalars, not the full logits — this is the + OOM-avoiding CP path. Consequently the tensors here are already full-sequence + and CP-replicated; the default DP-only reduce at export time yields the correct + global values. We must NOT additionally reduce across CP (that would multiply + sums by cp_size). See ``reassemble_cp_packed_logprobs`` and #1242. + """ cu_seqlens: torch.Tensor = input_["cu_seqlens"] loss_mask = input_["loss_mask"].bool() @@ -103,11 +115,8 @@ def compute_packed_sft_loss( logp = logprobs[cu_seqlens[i] : cu_seqlens[i + 1]] valid_tokens = int(m.count_nonzero().item()) if valid_tokens == 0: - # This is a padded dummy sequence created in `padded_mb_input`. - # When Ulysses SP is enabled, padded inputs are passed into the loss function. - # So we skip it. + # Padded dummy sequence created in `padded_mb_input`; skip it. continue - n_seqs[i] = True seqlogp[i] = torch.where(m, logp.detach(), 0.0).sum() / valid_tokens @@ -120,6 +129,10 @@ def compute_packed_sft_loss( ) stats_tracker.stat(ppl=(-seqlogp).exp().float(), denominator="n_seqs") stats_tracker.stat(loss=-logprobs.detach(), denominator="n_valid_tokens") + stats_tracker.stat( + entropy=torch.where(loss_mask, entropy.detach().float(), 0.0), + denominator="n_valid_tokens", + ) if vocab_min_logits is not None and vocab_max_logits is not None: stats_tracker.stat( @@ -128,4 +141,11 @@ def compute_packed_sft_loss( denominator="n_tokens", ) + if vocab_mean_logits is not None and vocab_norm_logits is not None: + stats_tracker.stat( + vocab_mean_logits=vocab_mean_logits, + vocab_norm_logits=vocab_norm_logits, + denominator="n_tokens", + ) + return loss diff --git a/areal/utils/stats_tracker.py b/areal/utils/stats_tracker.py index ec8c9617af..64bb9652b0 100644 --- a/areal/utils/stats_tracker.py +++ b/areal/utils/stats_tracker.py @@ -36,6 +36,11 @@ def __init__(self, name: str = ""): self.scope_stack.append(name.strip("/")) self.denominators = {} # key -> denominator key self.reduce_types = {} # key -> ReduceType + # Per-key override of the reduce_group. If set, its value takes + # precedence over the `reduce_group` passed to `export`. This is used, + # e.g., to make CP-local SFT stats (loss/entropy/vocab_*) reduce across + # DP + CP so the reported numbers are CP-invariant (#1242 follow-up). + self.reduce_groups = {} # key -> dist.ProcessGroup self.stats = defaultdict(list) @@ -93,7 +98,7 @@ def record_timing(self, key): self._set_reduce_type(full_key, ReduceType.SCALAR) self.stats[full_key].append(time.perf_counter() - start_time) - def denominator(self, **kwargs): + def denominator(self, *, reduce_group=None, **kwargs): with self.lock: for key, value in kwargs.items(): if not isinstance(value, torch.Tensor) or value.dtype != torch.bool: @@ -105,21 +110,34 @@ def denominator(self, **kwargs): full_key = self._get_full_key(key) self._set_reduce_type(full_key, ReduceType.SUM) self.stats[full_key].append(value.detach().clone()) + if reduce_group is not None: + self.reduce_groups[full_key] = reduce_group - def scalar(self, **kwargs): + def scalar(self, *, reduce_group=None, **kwargs): with self.lock: for key, value in kwargs.items(): full_key = self._get_full_key(key) self._set_reduce_type(full_key, ReduceType.SCALAR) self.stats[full_key].append(float(value)) + if reduce_group is not None: + self.reduce_groups[full_key] = reduce_group def stat( self, denominator: str, reduce_type: ReduceType | None = None, + *, + reduce_group=None, **kwargs, ): - """Record multiple values from a dictionary""" + """Record multiple values from a dictionary. + + If `reduce_group` is provided, it overrides the `reduce_group` passed + to `export` for these specific keys. This enables recording stats that + must be reduced across a different topology than the default (e.g., + loss/vocab_* under CP-local loss need DP+CP reduce while n_seqs stays + DP-only). + """ with self.lock: for key, value in kwargs.items(): if not isinstance(value, torch.Tensor) or value.dtype != torch.float: @@ -143,12 +161,18 @@ def stat( reduce_type = ReduceType.AVG_MIN_MAX self._set_reduce_type(full_key, reduce_type) self.stats[full_key].append(value.detach().clone()) + if reduce_group is not None: + self.reduce_groups[full_key] = reduce_group def _set_reduce_type(self, key, reduce_type): if not isinstance(reduce_type, ReduceType): raise ValueError("reduce_type must be a ReduceType enum") self.reduce_types[key] = reduce_type + def _effective_reduce_group(self, key, default_reduce_group): + """Return the per-key reduce_group override if any, else the default.""" + return self.reduce_groups.get(key, default_reduce_group) + def export(self, key=None, reduce_group=None, reset=True) -> dict[str, float]: """Get aggregated statistics""" with self.lock: @@ -159,7 +183,9 @@ def export(self, key=None, reduce_group=None, reset=True) -> dict[str, float]: if full_key in self.denominators: self.denominators.pop(full_key) if full_key in self.reduce_types: - self.denominators.pop(full_key) + self.reduce_types.pop(full_key) + if full_key in self.reduce_groups: + self.reduce_groups.pop(full_key) self.stats.pop(full_key) return result @@ -176,6 +202,7 @@ def export(self, key=None, reduce_group=None, reset=True) -> dict[str, float]: if reset: self.denominators = {} self.reduce_types = {} + self.reduce_groups = {} self.stats = defaultdict(list) results = { k: v.cpu().item() if torch.is_tensor(v) else v @@ -208,9 +235,10 @@ def _aggregate(self, key, reduce_group): sum(self.stats[key]), dtype=torch.float32, device=device ) cnt = torch.tensor(len(self.stats[key]), dtype=torch.float32, device=device) - if reduce_group is not None: - dist.all_reduce(value, group=reduce_group) - dist.all_reduce(cnt, group=reduce_group) + effective_group = self._effective_reduce_group(key, reduce_group) + if effective_group is not None: + dist.all_reduce(value, group=effective_group) + dist.all_reduce(cnt, group=effective_group) result[key] = float(value / cnt) result[key + "__count"] = int(cnt) else: @@ -223,10 +251,11 @@ def _aggregate(self, key, reduce_group): def _sum_of(self, key, reduce_group): values = self.stats[key] + effective_group = self._effective_reduce_group(key, reduce_group) if key not in self.denominators: x = sum([x.sum() for x in values]) - if reduce_group is not None: - dist.all_reduce(x, group=reduce_group) + if effective_group is not None: + dist.all_reduce(x, group=effective_group) else: denominator = self.denominators[key] if denominator not in self.stats: @@ -237,8 +266,8 @@ def _sum_of(self, key, reduce_group): for v, d in zip(values, self.stats[denominator]): xs.append(torch.where(d, v, 0.0).sum()) x = sum(xs) - if reduce_group is not None: - dist.all_reduce(x, group=reduce_group) + if effective_group is not None: + dist.all_reduce(x, group=effective_group) return float(x) def _avg_of(self, key, reduce_group): @@ -253,9 +282,10 @@ def _avg_of(self, key, reduce_group): ds.append(d.sum()) x = sum(xs) d = sum(ds) - if reduce_group is not None: - dist.all_reduce(x, group=reduce_group) - dist.all_reduce(d, group=reduce_group) + effective_group = self._effective_reduce_group(key, reduce_group) + if effective_group is not None: + dist.all_reduce(x, group=effective_group) + dist.all_reduce(d, group=effective_group) if d == 0: return None return x / d @@ -269,8 +299,9 @@ def _min_of(self, key, reduce_group): for v, d in zip(values, self.stats[denominator]): xs.append(torch.where(d, v, float("inf")).min()) x = min(xs) - if reduce_group is not None: - dist.all_reduce(x, group=reduce_group, op=dist.ReduceOp.MIN) + effective_group = self._effective_reduce_group(key, reduce_group) + if effective_group is not None: + dist.all_reduce(x, group=effective_group, op=dist.ReduceOp.MIN) if torch.isinf(x): return None return float(x) @@ -284,8 +315,9 @@ def _max_of(self, key, reduce_group): for v, d in zip(values, self.stats[denominator]): xs.append(torch.where(d, v, -float("inf")).max()) x = max(xs) - if reduce_group is not None: - dist.all_reduce(x, group=reduce_group, op=dist.ReduceOp.MAX) + effective_group = self._effective_reduce_group(key, reduce_group) + if effective_group is not None: + dist.all_reduce(x, group=effective_group, op=dist.ReduceOp.MAX) if torch.isinf(x): return None return float(x) diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index 734b5e7456..14008390dd 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -1103,6 +1103,11 @@ Refer to Megatron-LM documentation for implementation details. | `moe_enable_deepep` | boolean | `False` | - | | `moe_token_dispatcher_type` | string | `"alltoall"` | Type of token dispatcher. Options: 'allgather','alltoall' and 'flex'. | | `moe_permute_fusion` | boolean | `False` | Fuse token rearrangement ops during token dispatching. | +| `moe_router_fusion` | boolean | `False` | Enable fusion for MoE TopK routing and aux-loss computation. Requires TransformerEngine >= 2.7.0. | +| `moe_router_bias_update_rate` | float | `0.0` | Update rate for auxiliary-loss-free MoE load balancing (DeepSeek V3 style). Controls how fast expert_bias adjusts. Default 0.0 disables bias updates; set a positive value such as 1e-3 to enable. | +| `moe_z_loss_coeff` | float \| None | `None` | Scaling coefficient for router z-loss. Complements auxiliary-loss-free load balancing for router stability. A starting value of 1e-3 is recommended. None disables z-loss. | +| `enable_fp32_lm_head` | boolean | `False` | Cast lm_head output to FP32 before loss computation for numerical stability. | +| `cross_entropy_loss_fusion` | boolean | `False` | Enable fused cross-entropy loss kernel for better performance. | | `fp8_config` | [`FP8EngineConfig`](section-fp8-engine) \| None | `None` | - | | `bridge_type` | string | `"mbridge"` | Bridge backend for MegatronEngine. Choices: 'mbridge' or 'megatron-bridge'. **Choices:** `mbridge`, `megatron-bridge` | | `use_mbridge_save` | boolean | `False` | Use mbridge's save method to save gpu memory when saving weights. | diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index 9134943427..f2951ecbe5 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -1101,6 +1101,11 @@ Refer to Megatron-LM documentation for implementation details. | `moe_enable_deepep` | boolean | `False` | - | | `moe_token_dispatcher_type` | string | `"alltoall"` | Type of token dispatcher. Options: 'allgather','alltoall' and 'flex'. | | `moe_permute_fusion` | boolean | `False` | Fuse token rearrangement ops during token dispatching. | +| `moe_router_fusion` | boolean | `False` | Enable fusion for MoE TopK routing and aux-loss computation. Requires TransformerEngine >= 2.7.0. | +| `moe_router_bias_update_rate` | float | `0.0` | Update rate for auxiliary-loss-free MoE load balancing (DeepSeek V3 style). Controls how fast expert_bias adjusts. Default 0.0 disables bias updates; set a positive value such as 1e-3 to enable. | +| `moe_z_loss_coeff` | float \| None | `None` | Scaling coefficient for router z-loss. Complements auxiliary-loss-free load balancing for router stability. A starting value of 1e-3 is recommended. None disables z-loss. | +| `enable_fp32_lm_head` | boolean | `False` | Cast lm_head output to FP32 before loss computation for numerical stability. | +| `cross_entropy_loss_fusion` | boolean | `False` | Enable fused cross-entropy loss kernel for better performance. | | `fp8_config` | [`FP8EngineConfig`](section-fp8-engine) \| None | `None` | - | | `bridge_type` | string | `"mbridge"` | Bridge backend for MegatronEngine. Choices: 'mbridge' or 'megatron-bridge'. **Choices:** `mbridge`, `megatron-bridge` | | `use_mbridge_save` | boolean | `False` | Use mbridge's save method to save gpu memory when saving weights. | diff --git a/tests/test_stats_tracker_reduce_group.py b/tests/test_stats_tracker_reduce_group.py new file mode 100644 index 0000000000..f7b04b8c73 --- /dev/null +++ b/tests/test_stats_tracker_reduce_group.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import patch + +import torch + +from areal.utils.stats_tracker import DistributedStatsTracker + + +def test_stats_tracker_scalar_reduce_group_override_used_on_export(): + # Arrange + tracker = DistributedStatsTracker() + default_group = object() + override_group = object() + + tracker.scalar(score=1.0, reduce_group=override_group) + + # Act + with patch("areal.utils.stats_tracker.dist.all_reduce") as mock_all_reduce: + tracker.export(key="score", reduce_group=default_group) + + # Assert + assert mock_all_reduce.call_count == 2 + assert all( + call.kwargs["group"] is override_group for call in mock_all_reduce.mock_calls + ) + + +def test_stats_tracker_stat_reduce_group_override_used_on_export(): + # Arrange + tracker = DistributedStatsTracker() + default_group = object() + override_group = object() + + tracker.denominator(mask=torch.tensor([True, False, True])) + tracker.stat( + "mask", + loss=torch.tensor([1.0, 2.0, 3.0]), + reduce_group=override_group, + ) + + # Act + with patch("areal.utils.stats_tracker.dist.all_reduce") as mock_all_reduce: + tracker.export(key="loss", reduce_group=default_group) + + # Assert + assert mock_all_reduce.call_count == 4 + assert all( + call.kwargs["group"] is override_group for call in mock_all_reduce.mock_calls + ) From bbc10f0e95bf350d71169efcb2facb9756f1238b Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:47:28 +0800 Subject: [PATCH 25/36] fix(ppo): handle variable-size trajectory groups in reward normalization (#1454) Use actual trajectory group sizes when applying group-level reward and advantage normalization so failed or filtered rollout samples do not cause fixed-size slices to cross prompt groups. Pass trajectory metadata through batched_call instead of injecting a sentinel batch key, rename the Normalization argument to group_sizes, and zero singleton leave-one-out groups because they have no peer baseline. Add tests for variable-size groups, singleton leave-one-out behavior, validation, 2D advantage normalization, and batched_call metadata forwarding. --- areal/trainer/ppo/actor.py | 18 ++- areal/utils/data.py | 68 +++++++-- tests/test_reward_norm_variable_group.py | 185 +++++++++++++++++++++++ 3 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 tests/test_reward_norm_variable_group.py diff --git a/areal/trainer/ppo/actor.py b/areal/trainer/ppo/actor.py index c0da8f7dee..0da045557f 100644 --- a/areal/trainer/ppo/actor.py +++ b/areal/trainer/ppo/actor.py @@ -24,6 +24,7 @@ from areal.utils.data import ( KLEstimator, Normalization, + TrajBatchMeta, batched_call, split_padded_tensor_dict_into_mb_list, ) @@ -141,9 +142,11 @@ def _compute_logp(self, data: dict[str, Any]) -> torch.Tensor | None: @trace_perf("ppo_actor.compute_advantages", category="compute") def compute_advantages(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: - return batched_call(self._compute_advantages, data) + return batched_call(self._compute_advantages, data, pass_meta=True) - def _compute_advantages(self, data: dict[str, Any]) -> dict[str, Any]: + def _compute_advantages( + self, data: dict[str, Any], meta: TrajBatchMeta | None = None + ) -> dict[str, Any]: bs = data["input_ids"].shape[0] max_seqlen = data["input_ids"].shape[1] batch_indices = torch.arange( @@ -170,8 +173,13 @@ def _compute_advantages(self, data: dict[str, Any]) -> dict[str, Any]: reward_score = torch.clip( reward_score, max=self.reward_clip, min=-self.reward_clip ) + # Use actual trajectory group sizes when available so group-level + # normalization handles failed/filtered rollout samples without slicing + # across prompts. Direct calls without batched metadata keep the legacy + # fixed-group-size behavior. + group_sizes = meta.traj_group_sizes if meta is not None else None if self.reward_norm: - reward_score = self.reward_norm(reward_score) + reward_score = self.reward_norm(reward_score, group_sizes=group_sizes) loss_mask = data["loss_mask"].float() loss_mask = torch.roll(loss_mask, shifts=-1, dims=-1) @@ -237,7 +245,9 @@ def _compute_advantages(self, data: dict[str, Any]) -> dict[str, Any]: # Optionally perform advantage normalization. if self.adv_norm is not None: - advantages = self.adv_norm(advantages, loss_mask) + # Use the same actual trajectory group sizes as reward normalization; + # ignored when adv_norm is batch-level. + advantages = self.adv_norm(advantages, loss_mask, group_sizes=group_sizes) # Store data in the dict. data["advantages"] = advantages diff --git a/areal/utils/data.py b/areal/utils/data.py index 09368e4ef2..1560742a1a 100644 --- a/areal/utils/data.py +++ b/areal/utils/data.py @@ -401,10 +401,11 @@ def split_batch( def batched_call( - fn: Callable[[dict[str, Any]], Any], + fn: Callable[..., Any], data: list[dict[str, Any]], *, unpack: bool = True, + pass_meta: bool = False, ) -> Any: """Concatenate per-trajectory dicts into one batch, call *fn*, optionally unpack. @@ -421,9 +422,12 @@ def batched_call( unpack : bool If True (default), split the result back into a per-trajectory list via :func:`split_batch`. + pass_meta : bool + If True, call ``fn(batched, meta)`` so functions that need trajectory + metadata can consume it without injecting sentinel keys into the batch. """ batched, meta = concat_batch(data) - result = fn(batched) + result = fn(batched, meta) if pass_meta else fn(batched) if unpack: return split_batch(result, meta) return result @@ -1393,6 +1397,37 @@ def __init__(self, config: NormConfig): self.group_size = config.group_size self.eps = config.eps + def _build_group_slices( + self, bs: int, group_sizes: list[int] | None + ) -> list[slice]: + """Build slices for group-level normalization. + + When ``group_sizes`` (e.g. ``[8, 7, 8, ...]``) is provided it gives + the actual sample count of each trajectory group, handling variable-size + groups that arise when some rollout samples fail / are filtered. A fixed + ``group_size`` slice would otherwise straddle two groups, or leave a tail + of sequences whose std stays 0 → advantage blows up to (reward-mean)/eps. + When *None*, fall back to fixed-``group_size`` slicing. + """ + if group_sizes is not None: + if any(sz <= 0 for sz in group_sizes): + raise ValueError(f"group_sizes must be all positive, got {group_sizes}") + if sum(group_sizes) != bs: + raise ValueError( + f"group_sizes sum ({sum(group_sizes)}) must equal " + f"batch size ({bs}), got {group_sizes}" + ) + slices: list[slice] = [] + offset = 0 + for sz in group_sizes: + slices.append(slice(offset, offset + sz)) + offset += sz + return slices + return [ + slice(i * self.group_size, (i + 1) * self.group_size) + for i in range(bs // self.group_size) + ] + @torch.no_grad() def __call__( self, @@ -1400,6 +1435,7 @@ def __call__( loss_mask: torch.Tensor | None = None, high_precision: bool = True, reduce_group=None, + group_sizes: list[int] | None = None, ) -> torch.Tensor: bs = x.size(0) eps = self.eps @@ -1408,6 +1444,11 @@ def __call__( if loss_mask is not None and loss_mask.sum().item() == 0: return x.float() + # Pre-compute group slices once (variable-size groups via group_sizes). + group_slices = None + if self.mean_level == "group" or self.std_level == "group": + group_slices = self._build_group_slices(bs, group_sizes) + # Step 1: Compute mean if self.mean_level == "batch": mean = self._compute_mean( @@ -1421,16 +1462,17 @@ def __call__( mean = mean.expand_as(x) elif self.mean_level == "group": mean = torch.zeros_like(x) - for i in range(0, bs // self.group_size): - s = slice(i * self.group_size, (i + 1) * self.group_size) + for s in group_slices: xx = x[s] m = loss_mask[s] if loss_mask is not None else None - - # Special case: with group_size=1 and leave_one_out=True, mean should be 0 - if self.group_size == 1 and self.mean_leave1out: - dtype = torch.float64 if high_precision else torch.float32 - group_mean = torch.zeros( - (1, *xx.shape[1:]), dtype=dtype, device=xx.device + group_sz = s.stop - s.start + + # A singleton group has no peer to leave out. Use itself as the + # baseline so leave-one-out normalization outputs zero instead + # of passing the raw reward/advantage through. + if group_sz == 1 and self.mean_leave1out: + group_mean = xx.to( + torch.float64 if high_precision else torch.float32 ) else: group_mean = self._compute_mean( @@ -1465,14 +1507,14 @@ def __call__( std = std.expand_as(x) elif self.std_level == "group": std = torch.zeros_like(x) - for i in range(0, bs // self.group_size): - s = slice(i * self.group_size, (i + 1) * self.group_size) + for s in group_slices: xx = x[s] m = loss_mask[s] if loss_mask is not None else None group_mean_slice = mean[s] # already computed and expanded + group_sz = s.stop - s.start # Special case: with group_size=1 and std_unbiased=True, std should be 1 for numerical stability - if self.group_size == 1 and self.std_unbiased: + if group_sz == 1 and self.std_unbiased: dtype = torch.float64 if high_precision else torch.float32 group_std = torch.ones( (1, *xx.shape[1:]), dtype=dtype, device=xx.device diff --git a/tests/test_reward_norm_variable_group.py b/tests/test_reward_norm_variable_group.py new file mode 100644 index 0000000000..c6ce65232f --- /dev/null +++ b/tests/test_reward_norm_variable_group.py @@ -0,0 +1,185 @@ +"""Tests for variable-size group support in reward/advantage normalization. + +These cover the ``group_sizes`` argument added to ``Normalization`` so that +group-level normalization slices each trajectory group by its actual sample +count instead of a fixed ``group_size``. Variable-size groups arise when some +rollout samples fail or are filtered, leaving groups smaller than ``n_samples``. +A fixed-size slice would otherwise straddle two groups or leave a tail. +""" + +import pytest +import torch + +from areal.api.cli_args import NormConfig +from areal.utils.data import Normalization, batched_call + + +def _group_norm_config() -> NormConfig: + return NormConfig( + mean_level="group", + std_level="group", + group_size=2, + mean_leave1out=False, + std_unbiased=False, + eps=0.0, + ) + + +def _ref_group_norm(x: torch.Tensor, boundaries: list[int]) -> torch.Tensor: + """Reference: independently mean/std normalize each variable-size group.""" + out = torch.empty_like(x, dtype=torch.float64) + offset = 0 + for size in boundaries: + chunk = x[offset : offset + size].to(torch.float64) + mean = chunk.mean() + std = chunk.std(unbiased=False) + out[offset : offset + size] = (chunk - mean) / std if std > 0 else 0.0 + offset += size + return out + + +def test_variable_group_sizes_normalize_each_group_independently(): + """Unequal group sizes are sliced exactly, not by a fixed group_size.""" + norm = Normalization(_group_norm_config()) + # Three groups of sizes 3, 2, 3 (total 8); fixed group_size=2 would be wrong. + x = torch.tensor( + [0.0, 3.0, 6.0, 10.0, 20.0, 1.0, 2.0, 3.0], + dtype=torch.float32, + ) + boundaries = [3, 2, 3] + + out = norm(x, group_sizes=boundaries) + expected = _ref_group_norm(x, boundaries) + + torch.testing.assert_close(out.double(), expected, rtol=1e-5, atol=1e-5) + # Each group is zero-mean after normalization. + offset = 0 + for size in boundaries: + torch.testing.assert_close( + out[offset : offset + size].double().mean(), + torch.tensor(0.0, dtype=torch.float64), + rtol=1e-5, + atol=1e-5, + ) + offset += size + + +def test_group_sizes_none_matches_fixed_group_size(): + """Without group_sizes, behavior is unchanged (backward compatible).""" + norm = Normalization(_group_norm_config()) # group_size=2 + x = torch.tensor([0.0, 2.0, 10.0, 14.0], dtype=torch.float32) + + out_default = norm(x) + out_explicit = norm(x, group_sizes=[2, 2]) + + torch.testing.assert_close(out_default, out_explicit, rtol=1e-6, atol=1e-6) + + +def test_variable_group_with_zero_variance_group_is_finite(): + """A zero-variance group stays finite thanks to eps in the denominator. + + When a group's rewards are all equal its std is 0; dividing by ``std + eps`` + (eps > 0) keeps the result finite (and zero, since the numerator is also 0) + instead of producing NaN/inf. This is the guard that prevents a degenerate + group from blowing up advantages. + """ + cfg = NormConfig( + mean_level="group", + std_level="group", + group_size=2, + mean_leave1out=False, + std_unbiased=False, + eps=1e-5, + ) + norm = Normalization(cfg) + # Group 0 (size 3) has variance; group 1 (size 2) is all-equal -> std 0. + x = torch.tensor([0.0, 3.0, 6.0, 5.0, 5.0], dtype=torch.float32) + boundaries = [3, 2] + + out = norm(x, group_sizes=boundaries) + + assert torch.isfinite(out).all(), "normalized output must be finite" + # The degenerate group collapses to zero (x_centered=0 / (0+eps)). + torch.testing.assert_close( + out[3:].double(), + torch.zeros(2, dtype=torch.float64), + rtol=0, + atol=0, + ) + + +def test_leave_one_out_singleton_group_outputs_zero(): + """A size-1 leave-one-out group has no peer baseline, so it is zeroed.""" + cfg = NormConfig( + mean_level="group", + std_level="group", + group_size=1, + mean_leave1out=True, + std_unbiased=True, + eps=1e-5, + ) + norm = Normalization(cfg) + x = torch.tensor([7.0, 1.0, 5.0], dtype=torch.float32) + # One singleton group then a size-2 group. + out = norm(x, group_sizes=[1, 2]) + + assert torch.isfinite(out).all() + assert out[0].item() == 0.0 + + +def test_group_sizes_sum_mismatch_raises(): + """Boundaries whose sizes do not sum to the batch size are rejected.""" + norm = Normalization(_group_norm_config()) + x = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32) # bs=4 + with pytest.raises(ValueError, match="must equal"): + norm(x, group_sizes=[3, 2]) # sums to 5 != 4 + + +def test_group_sizes_non_positive_raises(): + """Boundaries containing a non-positive size are rejected.""" + norm = Normalization(_group_norm_config()) + x = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32) # bs=4 + with pytest.raises(ValueError, match="positive"): + norm(x, group_sizes=[4, 0]) + + +def test_adv_norm_style_2d_variable_groups_normalize_per_group(): + """Token-level (2D) advantages normalize per variable-size group on dim 0. + + Mirrors how PPOActor.adv_norm is applied to the GAE advantage tensor. + """ + norm = _group_norm_config() # group mean+std, eps=0 + adv_norm = Normalization(norm) + # bs=5 (groups [3, 2]), seqlen=2. + x = torch.tensor( + [[0.0, 6.0], [3.0, 6.0], [6.0, 6.0], [10.0, 0.0], [20.0, 0.0]], + dtype=torch.float32, + ) + loss_mask = torch.ones_like(x) + out = adv_norm(x, loss_mask, group_sizes=[3, 2]) + + assert torch.isfinite(out).all() + assert out.shape == x.shape + + +def test_batched_call_pass_meta_provides_traj_group_sizes(): + data = [ + { + "attention_mask": torch.ones(2, 3, dtype=torch.bool), + "values": torch.tensor([[1.0], [2.0]]), + }, + { + "attention_mask": torch.ones(1, 3, dtype=torch.bool), + "values": torch.tensor([[3.0]]), + }, + ] + + def fn(batch, meta): + assert meta.traj_group_sizes == [2, 1] + return {"values": batch["values"] + 1} + + out = batched_call(fn, data, pass_meta=True) + + assert len(out) == 2 + torch.testing.assert_close(out[0]["values"], torch.tensor([[2.0], [3.0]])) + torch.testing.assert_close(out[1]["values"], torch.tensor([[4.0]])) From 87a034e64f9d2ad9777c82099833b59ce0915193 Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:49:58 +0800 Subject: [PATCH 26/36] fix(openai): parse tool_call arguments from JSON string to dict before chat template (#1463) Some chat templates (e.g. GLM-5.1) iterate over tool_call arguments with .items(), which fails when arguments is a JSON string as per the OpenAI API convention. Normalize tool_call arguments to dicts before applying the chat template in concat_prompt_token_ids_with_parent and the completions/responses tokenizer paths. --- areal/experimental/openai/client.py | 40 ++++++++++++++ .../openai/test_parse_tool_call_arguments.py | 55 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/experimental/openai/test_parse_tool_call_arguments.py diff --git a/areal/experimental/openai/client.py b/areal/experimental/openai/client.py index 016bd738c5..2655b0eba1 100644 --- a/areal/experimental/openai/client.py +++ b/areal/experimental/openai/client.py @@ -343,6 +343,43 @@ def _build_messages_list(item: dict) -> list[dict]: return messages_list +def _parse_tool_call_arguments(messages: list[dict]) -> list[dict]: + """Return a new message list with tool_call arguments parsed from JSON strings + to dicts. Some chat templates (e.g. GLM-5.1) iterate over arguments with + .items(), which fails when arguments is a JSON string per OpenAI API convention. + + Only creates new dicts where modifications are needed; unaffected messages + are shared with the input list to avoid expensive deep copies. + """ + result = [] + for msg in messages: + tool_calls = msg.get("tool_calls") if isinstance(msg, dict) else None + if not tool_calls: + result.append(msg) + continue + new_tool_calls = [] + modified = False + for tc in tool_calls: + fn = tc.get("function") if isinstance(tc, dict) else None + if fn is None: + new_tool_calls.append(tc) + continue + args = fn.get("arguments") + if isinstance(args, str): + try: + parsed = json.loads(args) + tc = {**tc, "function": {**fn, "arguments": parsed}} + modified = True + except (json.JSONDecodeError, TypeError): + pass + new_tool_calls.append(tc) + if modified: + result.append({**msg, "tool_calls": new_tool_calls}) + else: + result.append(msg) + return result + + def concat_prompt_token_ids_with_parent( message_list: list[dict], parent: InteractionWithTokenLogpReward | None, @@ -385,6 +422,7 @@ def concat_prompt_token_ids_with_parent( parent_tokens += [eos_token_id] all_message_list += message_list + all_message_list = _parse_tool_call_arguments(all_message_list) all_tokens = apply_chat_template( tokenizer, @@ -610,6 +648,7 @@ async def create( has_images = len(image_data) > 0 tokenizer_messages = messages_for_tokenizer if has_images else messages_list + tokenizer_messages = _parse_tool_call_arguments(tokenizer_messages) if self.chat_template_type == "hf": prompt_token_ids = apply_chat_template( self.tokenizer, @@ -1018,6 +1057,7 @@ async def create( has_images = len(image_data) > 0 tokenizer_messages = messages_for_tokenizer if has_images else messages_list + tokenizer_messages = _parse_tool_call_arguments(tokenizer_messages) if self.chat_template_type == "hf": prompt_token_ids = apply_chat_template( self.tokenizer, diff --git a/tests/experimental/openai/test_parse_tool_call_arguments.py b/tests/experimental/openai/test_parse_tool_call_arguments.py new file mode 100644 index 0000000000..afb702ebcc --- /dev/null +++ b/tests/experimental/openai/test_parse_tool_call_arguments.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: Apache-2.0 + +from areal.experimental.openai.client import _parse_tool_call_arguments + + +def test_string_arguments_are_parsed_to_dict(): + messages = [ + { + "role": "assistant", + "tool_calls": [ + {"function": {"name": "Bash", "arguments": '{"command": "ls"}'}} + ], + } + ] + + out = _parse_tool_call_arguments(messages) + + assert out[0]["tool_calls"][0]["function"]["arguments"] == {"command": "ls"} + + +def test_non_string_arguments_are_left_unchanged(): + messages = [ + { + "role": "assistant", + "tool_calls": [ + {"function": {"name": "Bash", "arguments": {"command": "ls"}}} + ], + } + ] + + out = _parse_tool_call_arguments(messages) + + assert out[0] is messages[0] + assert out[0]["tool_calls"][0]["function"]["arguments"] == {"command": "ls"} + + +def test_invalid_json_arguments_are_left_unchanged(): + messages = [ + { + "role": "assistant", + "tool_calls": [{"function": {"name": "Bash", "arguments": "not json"}}], + } + ] + + out = _parse_tool_call_arguments(messages) + + assert out[0]["tool_calls"][0]["function"]["arguments"] == "not json" + + +def test_messages_without_tool_calls_pass_through(): + messages = [{"role": "user", "content": "hi"}] + + out = _parse_tool_call_arguments(messages) + + assert out[0] is messages[0] From d8ff8ce7225fb126561bf1f0e87e1b123c810615 Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:01:44 +0800 Subject: [PATCH 27/36] feat(swe): add SWE-bench RL training workflow (#1462) Add a SWE-bench RL training workflow under examples/swe: - train_swe_rl.py: the RL training entrypoint. - agent.py: the AReaL-SWEAgent rollout workflow that runs SWE-bench agents through AReaL's OpenAI-compatible proxy during training. - preprocessors.py / prefix_matchers.py: message preprocessing and interaction-cache prefix matching for the agent proxy. - filter_function.py: rollout group accept/reject filter. - qwen3_30b_a3b_grpo.yaml: a runnable Qwen3-Coder GRPO example config. - README.md: setup guide covering the AReaL-SWEAgent checkout, the AEnvironment backend, and Claude Code (cc) agent training. Also add the SWE dataset loader. --- areal/dataset/__init__.py | 11 + areal/dataset/swe_sft.py | 2559 ++++++++++++++++++++++++++ areal/utils/logging.py | 2 + examples/swe/README.md | 190 ++ examples/swe/agent.py | 260 +++ examples/swe/filter_function.py | 26 + examples/swe/prefix_matchers.py | 58 + examples/swe/preprocessors.py | 124 ++ examples/swe/qwen3_30b_a3b_grpo.yaml | 291 +++ examples/swe/train_swe_rl.py | 218 +++ examples/swe/utils.py | 123 ++ 11 files changed, 3862 insertions(+) create mode 100644 areal/dataset/swe_sft.py create mode 100644 examples/swe/README.md create mode 100644 examples/swe/agent.py create mode 100644 examples/swe/filter_function.py create mode 100644 examples/swe/prefix_matchers.py create mode 100644 examples/swe/preprocessors.py create mode 100644 examples/swe/qwen3_30b_a3b_grpo.yaml create mode 100644 examples/swe/train_swe_rl.py create mode 100644 examples/swe/utils.py diff --git a/areal/dataset/__init__.py b/areal/dataset/__init__.py index 1ab23a4cb9..5e06d98681 100644 --- a/areal/dataset/__init__.py +++ b/areal/dataset/__init__.py @@ -19,6 +19,7 @@ "virl39k", "hh-rlhf", "torl_data", + "swe_sft", ] logger = logging.getLogger("Dataset") @@ -133,6 +134,16 @@ def _get_custom_dataset( max_length=max_length, **kwargs, ) + elif "swe" in path.lower() and type == "sft": + from .swe_sft import get_swe_sft_dataset + + return get_swe_sft_dataset( + path=path, + split=split, + tokenizer=tokenizer, + max_length=max_length, + **kwargs, + ) else: # Fallback: try loading as a generic HuggingFace dataset from disk. # This supports arbitrary datasets saved via dataset.save_to_disk(). diff --git a/areal/dataset/swe_sft.py b/areal/dataset/swe_sft.py new file mode 100644 index 0000000000..20dd14ad61 --- /dev/null +++ b/areal/dataset/swe_sft.py @@ -0,0 +1,2559 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""SWE SFT dataset loader. + +Loads SWE-bench trajectory data and converts it into progressive SFT +training pairs. Each trajectory is split at assistant-turn boundaries +so that every pair ends with an assistant segment (assistant message + +its subsequent tool responses). + +Example trajectory:: + + [system, user, asst1, tool1a, tool1b, asst2, tool2, asst3] + +Produces three pairs:: + + Pair 1: [system, user, asst1, tool1a, tool1b] + Pair 2: [system, user, asst1, tool1a, tool1b, asst2, tool2] + Pair 3: [system, user, asst1, tool1a, tool1b, asst2, tool2, asst3] + +In each pair, only the **last** assistant segment is trained (loss=1); +earlier assistant turns are treated as context (loss=0). + +By default, pairs whose current segment contains a tool result with +``is_error=True`` are discarded. Set ``filter_errors=False`` to keep them. + +The file is organized into the following sections: + +1. **Constants & Infrastructure** — shared constants, distributed sync +2. **Cleaning** — message content transforms (thinking tags, field cleanup) +3. **Filters** — keep/discard predicates (error, empty, bare-text, truncation) +4. **Splitting** — trajectory → progressive pairs (segment detection + split) +5. **Tokenization** — template detection, render→tokenize→loss_mask, dump +6. **Pipeline** — loading, processing, distributed cache, public API +7. **CLI** — ``python -m areal.dataset.swe_sft`` entry point +""" + +import json +import os +import random +import re +import time + +from datasets import Dataset + +from areal.utils import logging + +logger = logging.getLogger("SWESFTDataset") + + +# ============================================================ +# 1. Constants & Infrastructure +# ============================================================ + +DATASET_NUM_PROC = 1 + +# Timeout (seconds) for non-rank-0 workers waiting for rank 0 to finish +# dataset processing. Large datasets with tokenization can take minutes; +# 30 min is a generous upper bound. +_RANK0_CACHE_TIMEOUT = 36000 +_RANK0_CACHE_POLL_INTERVAL = 5 + + +def _wait_for_marker(marker_path: str): + """Block until *marker_path* exists on disk, with timeout.""" + start = time.monotonic() + while not os.path.exists(marker_path): + elapsed = time.monotonic() - start + if elapsed > _RANK0_CACHE_TIMEOUT: + raise TimeoutError( + f"Waited {_RANK0_CACHE_TIMEOUT}s for rank 0 to finish dataset " + f"processing (marker: {marker_path}). Check rank 0 logs." + ) + time.sleep(_RANK0_CACHE_POLL_INTERVAL) + + +def _extract_messages(record, record_idx): + """Extract messages and tools from a parsed JSONL record. + + Handles nested (``conversations`` wrapper) and flat formats. + Warns if multiple conversations are present. + + Returns: + Tuple of ``(messages, record_tools)``. *messages* may be empty. + """ + convs = record.get("conversations", []) + if convs: + if len(convs) > 1: + logger.warning( + "Record %d has %d conversations, using only the last one.", + record_idx, + len(convs), + ) + conv = convs[-1] + return conv.get("messages", []), conv.get("tools") + return record.get("messages", []), record.get("tools") + + +def _set_messages(record, messages): + """Write *messages* back into *record* (inverse of ``_extract_messages``). + + Used by the ``--save-trajectories`` CLI path to update truncated + messages in the original record structure before serialization. + """ + convs = record.get("conversations", []) + if convs: + convs[-1]["messages"] = messages + else: + record["messages"] = messages + + +def _iter_jsonl_records(path): + """Iterate trajectory JSONL records. + + Yields ``(record_idx, messages, record_tools)`` tuples. Handles + nested (``conversations`` wrapper) vs flat format auto-detection + via ``_extract_messages``. Records with empty messages are skipped. + + Warns about multi-user trajectories which break think-tag rendering + in templates with ``ns.last_query_index`` logic (e.g. Bailing). + """ + record_idx = 0 + n_multi_user = 0 + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + record = json.loads(line) + record_idx += 1 + messages, record_tools = _extract_messages(record, record_idx) + if not messages: + continue + n_user = sum(1 for m in messages if m.get("role") == "user") + if n_user > 1: + n_multi_user += 1 + if n_multi_user <= 3: + logger.warning( + "Record %d has %d user messages. Templates with " + "ns.last_query_index logic (e.g. Bailing) will NOT " + "render for assistant turns before the last " + "user message. Consider filtering!", + record_idx, + n_user, + ) + yield record_idx, messages, record_tools + if n_multi_user > 0: + logger.warning( + "Total %d/%d records have multiple user messages. " + "These may produce no-think training signal.", + n_multi_user, + record_idx, + ) + + +# ============================================================ +# 2. Cleaning — message content transforms +# ============================================================ + +# Match reasoning blocks with any common tag variant: +# ... (Qwen standard) +# ... (Claude) +# The opening and closing tag names need not match exactly — mixed pairs +# like ``...`` (seen in distillation data) are handled. +_THINK_OPEN_RE = re.compile(r"") +_THINK_CLOSE_RE = re.compile(r"") +_THINK_RE = re.compile(r"(.*?)", re.DOTALL) + + +def _normalize_thinking_tags(content): + """Normalise all thinking tag variants to ````/````. + + Distillation data from different models may use ```` (Claude) + vs ```` (Qwen). Non-standard variants are multi-token for the + Qwen tokenizer which breaks think/tool_call boundaries. + """ + if not content: + return content + content = _THINK_OPEN_RE.sub("", content) + content = _THINK_CLOSE_RE.sub("", content) + return content + + +def _extract_thinking(content): + """Strip thinking blocks from *content*. + + Callers must run ``_normalize_thinking_tags`` first so that all + tag variants have been converted to ````/````. + + Returns: + Cleaned content with thinking blocks removed, or the original + content unchanged if no thinking tags are found. + """ + if not content: + return content + cleaned = _THINK_RE.sub("", content).strip() + return cleaned if cleaned != content.strip() else content + + +def _clean_message(msg, strip_thinking=True, ensure_thinking=False): + """Remove non-standard fields before tokenization. + + Keeps only the fields expected by tokenizer chat templates: + role, content, reasoning_content (for assistant), tool_calls + (for assistant), tool_call_id (for tool). + + Handles thinking content in two representations: + + - Inline ``...`` tags in ``content`` + - Separate ``reasoning_content`` field (DeepSeek, Qwen3 API style) + + If both are present, inline tags take priority and + ``reasoning_content`` is dropped with a warning to avoid double + thinking blocks in the rendered template. + + Args: + msg: Raw message dict. + strip_thinking: If True, remove thinking from assistant messages + (both inline ```` tags and ``reasoning_content``). + Used for context turns. If False, preserve thinking as-is + (used for the training-target assistant turn). + ensure_thinking: If True, inject inline ``\n`` + on assistant turns that lack a thinking block (either + inline or in ``reasoning_content``). Requires the + patched Bailing template (via + ``_patch_chat_template_for_training``) which detects + ``had_think_tags`` and preserves empty think blocks. + """ + cleaned = {"role": msg["role"]} + + # Handle content — some assistant messages have content=None when + # they only contain tool_calls. Preserve None so chat templates + # that distinguish None vs "" render correctly. + content = msg.get("content") + # Some APIs (DeepSeek, Qwen3 with enable_thinking) return thinking + # in a separate ``reasoning_content`` field instead of inline + # ```` tags. Handle both representations. + raw_reasoning = msg.get("reasoning_content") if msg["role"] == "assistant" else None + has_thinking = False + if content is not None: + if msg["role"] == "assistant": + content = _normalize_thinking_tags(content) + has_inline_thinking = bool(_THINK_RE.search(content)) + if has_inline_thinking and raw_reasoning and raw_reasoning.strip(): + # Conflict: both reasoning_content and inline tags. + # Keep inline tags (they are already in the content the + # tokenizer will see) and drop reasoning_content to avoid + # double thinking blocks in the rendered template. + if not strip_thinking: + logger.warning( + "Message has both reasoning_content and inline " + " tags. Keeping inline tags, dropping " + "reasoning_content." + ) + raw_reasoning = None + elif not has_inline_thinking and raw_reasoning and raw_reasoning.strip(): + # Convert reasoning_content → inline in content. + # This ensures a single representation that templates + # render identically to the reasoning_content path, while + # being more transparent and debuggable. + if not strip_thinking: + content = ( + f"\n{raw_reasoning.strip(chr(10))}\n" + f"\n\n{content.lstrip(chr(10))}" + ) + has_inline_thinking = True + raw_reasoning = None + has_thinking = has_inline_thinking or bool( + raw_reasoning and raw_reasoning.strip() + ) + if strip_thinking: + content = _extract_thinking(content) + cleaned["content"] = content + elif msg["role"] == "assistant" and msg.get("tool_calls"): + # Assistant with tool_calls but content=None. + if raw_reasoning and raw_reasoning.strip(): + has_thinking = True + if not strip_thinking: + # Convert reasoning_content → inline in content. + cleaned["content"] = ( + f"\n{raw_reasoning.strip(chr(10))}\n" + ) + else: + cleaned["content"] = None + raw_reasoning = None + else: + cleaned["content"] = None + else: + # Non-assistant messages without content: default to empty string. + cleaned["content"] = "" + + # Preserve reasoning_content for target turns only when it was NOT + # already inlined above (i.e. only when raw_reasoning is still set). + if not strip_thinking and raw_reasoning is not None: + cleaned["reasoning_content"] = raw_reasoning + + # For the target assistant turn without a thinking block, inject + # inline ``\n`` so that the (patched) template detects + # think intent via ``had_think_tags`` and renders + # ``\n\n\n\n`` — identical token output to the old + # ``reasoning_content='\n'`` approach. + # + # Requires ``_patch_chat_template_for_training`` to have been called + # on the tokenizer, otherwise the stock Bailing template will extract + # and discard the empty ```` block. + if ensure_thinking and msg["role"] == "assistant" and not has_thinking: + cur_content = cleaned.get("content") + if cur_content is None or cur_content == "": + cleaned["content"] = "\n" + else: + cleaned["content"] = f"\n\n\n{cur_content.lstrip(chr(10))}" + + # Copy tool_calls for assistant messages + if msg["role"] == "assistant" and msg.get("tool_calls"): + cleaned_tool_calls = [] + for tc in msg["tool_calls"]: + cleaned_tc = { + "type": tc.get("type", "function"), + "function": { + "name": tc["function"]["name"], + "arguments": json.dumps(tc["function"]["arguments"]) + if isinstance(tc["function"]["arguments"], dict) + else tc["function"]["arguments"], + }, + } + if "id" in tc: + cleaned_tc["id"] = tc["id"] + cleaned_tool_calls.append(cleaned_tc) + cleaned["tool_calls"] = cleaned_tool_calls + + # Copy tool_call_id for tool messages + if msg["role"] == "tool" and msg.get("tool_call_id"): + cleaned["tool_call_id"] = msg["tool_call_id"] + + return cleaned + + +# ============================================================ +# 3. Filters — keep/discard predicates +# ============================================================ + + +def _segment_has_error(messages, start, end): + """Check if any tool message in ``messages[start:end]`` has ``is_error=True``.""" + for m in messages[start:end]: + if m.get("role") == "tool" and m.get("is_error") is True: + return True + return False + + +def _is_empty_tool_call(msg): + """True if assistant *msg* has no text content and no reasoning but has tool_calls.""" + content = msg.get("content") or "" + if content.strip() or not msg.get("tool_calls"): + return False + # If reasoning_content exists, the model did think — not a silent invocation. + reasoning = msg.get("reasoning_content") + if reasoning and reasoning.strip(): + return False + return True + + +def _is_bare_text_tool_call(msg): + """True if assistant *msg* has text without ```` tags and has tool_calls.""" + content = msg.get("content") or "" + if not content.strip() or not msg.get("tool_calls"): + return False + # If reasoning_content exists, thinking is in a separate field — not bare text. + reasoning = msg.get("reasoning_content") + if reasoning and reasoning.strip(): + return False + normalized = _THINK_OPEN_RE.sub("", content) + normalized = _THINK_CLOSE_RE.sub("", normalized) + match = _THINK_RE.search(normalized) + return not (match and match.group(1).strip()) + + +def _msg_has_thinking(msg): + """True if assistant *msg* has thinking content (inline or reasoning_content).""" + if msg.get("role") != "assistant": + return False + content = msg.get("content") or "" + normalized = _THINK_OPEN_RE.sub("", content) + normalized = _THINK_CLOSE_RE.sub("", normalized) + if _THINK_RE.search(normalized): + return True + rc = msg.get("reasoning_content") or "" + return bool(rc.strip()) + + +def _truncate_at_task_notification(messages): + """Truncate messages when a ```` follows a pure-text assistant. + + Claude Code emits ```` as a user message when a + background task (e.g. ``pip install``) completes. If the model has + already produced a text-only summary (no tool_calls), the notification + and all subsequent messages are noise — the model just replies + "nothing to do". Truncating here removes that noise. + + Only triggers when the pattern is: + assistant (text, no tool_calls) → user () + + Returns: + Truncated message list (or the original list if no truncation needed). + """ + for i, m in enumerate(messages): + if m.get("role") != "user": + continue + if "" not in (m.get("content") or ""): + continue + # Find preceding assistant + prev_asst = None + for j in range(i - 1, -1, -1): + if messages[j].get("role") == "assistant": + prev_asst = messages[j] + break + if prev_asst is None: + continue + content = prev_asst.get("content") or "" + if content.strip() and not prev_asst.get("tool_calls"): + # Truncate: keep everything up to (but not including) this user msg + return messages[:i] + return messages + + +# ============================================================ +# 3b. Balancing — downsample non-thinking pairs +# ============================================================ + + +def _classify_pair(pair): + """Classify a pair by its target assistant turn's content type. + + Returns one of: + ``"thinking"`` — target has actual ```` content or + non-empty ``reasoning_content``. + ``"no_thinking_tool_call"`` — target has no thinking but has + ``tool_calls`` (the dominant category that causes distribution + skew). + ``"pure_text"`` — target has no thinking and no tool_calls + (typically the final summary turn in a trajectory). + """ + target = pair[-1] + if target.get("role") != "assistant": + return "pure_text" + + content = target.get("content") or "" + rc = target.get("reasoning_content") or "" + has_thinking = bool(_THINK_RE.search(content)) or bool(rc.strip()) + + if has_thinking: + return "thinking" + if target.get("tool_calls"): + return "no_thinking_tool_call" + return "pure_text" + + +def _balance_thinking_pairs(pairs, max_no_thinking_ratio, seed=42, tools_list=None): + """Downsample non-thinking **tool-call** pairs to control balance. + + Only ``no_thinking_tool_call`` pairs (no thinking but has tool_calls) + are subject to downsampling. ``thinking`` pairs and ``pure_text`` + pairs (the final summary turn, no thinking and no tool_calls) are + always kept — the latter are critical for the model to learn when + to stop calling tools and give a final answer. + + Args: + pairs: List of progressive SFT pairs. + max_no_thinking_ratio: Maximum ratio of non-thinking tool-call + pairs to thinking pairs. For example, ``1.0`` means at most + 1:1, ``2.0`` means at most 2 non-thinking per 1 thinking pair. + ``None`` disables downsampling. + seed: Random seed for reproducible downsampling. + + Returns: + Balanced list of pairs (order preserved, randomly sampled for + the downsampled category). + """ + if max_no_thinking_ratio is None: + return pairs, tools_list + + thinking = [] + no_think_tc = [] + pure_text = [] + for i, pair in enumerate(pairs): + cat = _classify_pair(pair) + if cat == "thinking": + thinking.append(i) + elif cat == "no_thinking_tool_call": + no_think_tc.append(i) + else: + pure_text.append(i) + + n_think = len(thinking) + n_no_think_tc = len(no_think_tc) + n_pure_text = len(pure_text) + + if n_think == 0: + logger.warning( + "No thinking pairs found; skipping balance " + "(all %d pairs have empty thinking).", + n_no_think_tc + n_pure_text, + ) + return pairs, tools_list + + max_no_think_tc = int(n_think * max_no_thinking_ratio) + if n_no_think_tc <= max_no_think_tc: + logger.info( + "Thinking balance OK: %d thinking + %d no-think-tc + %d pure-text " + "(ratio %.1f <= %.1f), no downsampling needed.", + n_think, + n_no_think_tc, + n_pure_text, + n_no_think_tc / n_think, + max_no_thinking_ratio, + ) + return pairs, tools_list + + rng = random.Random(seed) + sampled_tc = set(rng.sample(no_think_tc, max_no_think_tc)) + keep_indices = sorted(set(thinking) | sampled_tc | set(pure_text)) + balanced = [pairs[i] for i in keep_indices] + balanced_tools = ( + [tools_list[i] for i in keep_indices] if tools_list is not None else None + ) + + logger.info( + "Balanced thinking pairs: %d thinking + %d no-think-tc " + "(downsampled from %d, ratio %.1f → %.1f) + %d pure-text (kept all).", + n_think, + max_no_think_tc, + n_no_think_tc, + n_no_think_tc / n_think, + max_no_thinking_ratio, + n_pure_text, + ) + return balanced, balanced_tools + + +# ============================================================ +# 3c. Thinking augmentation stats +# ============================================================ + + +def _log_thinking_augmentation_stats( + n_variants, + prob, + n_total_trajs, + thinking_turns_per_traj, + total_asst_turns_per_traj, + patterns_per_traj, +): + """Log adaptive-thinking augmentation quality metrics. + + Called after the augmentation loop in loaders to report how well + the ``n_thinking_variants`` / ``random_strip_thinking_prob`` settings + produce diverse thinking-pattern variants. + + Args: + n_variants: ``n_thinking_variants`` setting (K). + prob: ``random_strip_thinking_prob`` setting. + n_total_trajs: Total number of source trajectories processed. + thinking_turns_per_traj: List of N_thinking per source trajectory. + total_asst_turns_per_traj: List of N_total_asst per source trajectory. + patterns_per_traj: List of ``set[frozenset]`` — the unique strip + patterns generated for each source trajectory (including the + empty frozenset for the original unstripped variant). + """ + n_eligible = sum(1 for n in thinking_turns_per_traj if n > 0) + total_thinking = sum(thinking_turns_per_traj) + total_asst = sum(total_asst_turns_per_traj) + + # 1. Thinking Turn Coverage + avg_thinking = total_thinking / max(n_total_trajs, 1) + thinking_ratio = total_thinking / max(total_asst, 1) + + # 2. Pattern Diversity + diversity_ratios = [] + for n_think, patterns in zip(thinking_turns_per_traj, patterns_per_traj): + if n_think == 0: + continue + theoretical_max = min(n_variants, 2**n_think) + actual_unique = len(patterns) + diversity_ratios.append(actual_unique / theoretical_max) + avg_diversity = sum(diversity_ratios) / max(len(diversity_ratios), 1) + + # 3. Augmentation Efficiency + n_non_trivial = 0 + for patterns in patterns_per_traj: + # Count variants that differ from the original (non-empty strip set) + n_non_trivial += sum(1 for p in patterns if p) + expected_aug = (n_variants - 1) * max(n_eligible, 1) + efficiency = n_non_trivial / max(expected_aug, 1) + + # 4. Total sample count + n_total_samples = sum(len(p) for p in patterns_per_traj) + + logger.info( + f"Thinking augmentation stats (K={n_variants}, p={prob:.2f}):\n" + f" Source trajectories: {n_total_trajs} " + f"({n_eligible} with thinking turns)\n" + f" Thinking coverage: {avg_thinking:.1f} thinking turns/traj, " + f"{thinking_ratio:.1%} of all assistant turns\n" + f" Pattern diversity: {avg_diversity:.2f} " + f"(1.0 = all variants unique)\n" + f" Augmentation efficiency: {efficiency:.2f} " + f"({n_non_trivial}/{expected_aug} non-trivial variants)\n" + f" Total samples after augmentation: {n_total_samples}" + ) + + +# ============================================================ +# 4. Splitting — trajectory → progressive pairs +# ============================================================ + + +def _find_segments(messages): + """Find assistant+tools segment boundaries. + + Returns: + List of ``(assistant_start_idx, segment_end_idx)`` tuples. + """ + segments = [] + i = 0 + while i < len(messages): + if messages[i].get("role") == "assistant": + j = i + 1 + while j < len(messages) and messages[j].get("role") == "tool": + j += 1 + segments.append((i, j)) + i = j + else: + i += 1 + return segments + + +def _split_and_filter( + messages, + filter_errors=True, + strip_all_thinking=False, + filter_empty_tool_calls=False, + filter_bare_text_tool_calls=False, + random_strip_thinking_prob=0.0, + rng=None, +): + """Split trajectory into progressive pairs and optionally filter. + + By default, thinking (``...``) is stripped from context + assistant turns only; the last assistant turn (training target) keeps + its content unchanged. Set *strip_all_thinking* to strip from every + assistant turn including the target. + + When *random_strip_thinking_prob* > 0, each target assistant turn that + has thinking content is independently stripped with that probability. + Stripped turns use the context-cleaned version (thinking fully removed, + no empty ```` injected). + + Args: + messages: Raw trajectory messages. + filter_errors: If True (default), discard pairs whose current segment + contains a tool result with ``is_error=True``. Set to False to + keep all pairs regardless of tool errors. + strip_all_thinking: If True, strip ```` blocks from every + assistant turn including the training target. + filter_empty_tool_calls: If True, discard pairs whose training-target + assistant turn has no text content but has tool_calls. + filter_bare_text_tool_calls: If True, discard pairs whose + training-target assistant turn has text content without + ```` tags and has tool_calls. + random_strip_thinking_prob: Probability of stripping thinking + from each target assistant turn. 0.0 = no stripping. + rng: ``random.Random`` instance for reproducible sampling. + + Returns: + Tuple of ``(pairs, n_filtered_errors, n_filtered_empty_tc, + n_filtered_bare_tc, n_stripped)``. + """ + segments = _find_segments(messages) + if not segments: + return [], 0, 0, 0, 0 + + pairs = [] + n_filtered_errors = 0 + n_filtered_empty_tc = 0 + n_filtered_bare_tc = 0 + n_stripped = 0 + + # Pre-clean all messages in context mode (thinking stripped). + # This avoids re-cleaning the same message for every progressive pair + # (O(N+K) instead of O(N*K) where K = number of segments). + context_cleaned = [_clean_message(m, strip_thinking=True) for m in messages] + + # For target assistant turns, clean with thinking preserved (unless + # strip_all_thinking is set, in which case context_cleaned is reusable). + # When stripping is active (augmented variant), use ensure_thinking=False + # so empty-thinking turns don't get \n injected. + stripping_active = random_strip_thinking_prob > 0.0 and rng is not None + target_ensure = not stripping_active + target_cleaned = {} + if not strip_all_thinking: + for asst_start, _ in segments: + target_cleaned[asst_start] = _clean_message( + messages[asst_start], + strip_thinking=False, + ensure_thinking=target_ensure, + ) + + for asst_start, seg_end in segments: + # Check if current segment has any tool errors + if filter_errors and _segment_has_error(messages, asst_start, seg_end): + n_filtered_errors += 1 + continue + + # Content-type filters operate on the raw assistant message. + asst_msg = messages[asst_start] + if filter_empty_tool_calls and _is_empty_tool_call(asst_msg): + n_filtered_empty_tc += 1 + continue + if filter_bare_text_tool_calls and _is_bare_text_tool_call(asst_msg): + n_filtered_bare_tc += 1 + continue + + # Build pair: include context up to the target assistant turn, + # truncating tool responses that follow it. This ensures the + # target assistant is always the *last* message so that chat + # templates with ``loop.last``-dependent rendering (e.g. Qwen3 + # ```` injection) behave consistently. The tool responses + # would have loss_mask=0 anyway and only add noise. + pair = list(context_cleaned[: asst_start + 1]) + if not strip_all_thinking: + # Randomly strip: leave context_cleaned version (thinking + # already removed) instead of replacing with target_cleaned. + should_strip = ( + rng is not None + and _msg_has_thinking(messages[asst_start]) + and rng.random() < random_strip_thinking_prob + ) + if not should_strip: + pair[asst_start] = target_cleaned[asst_start] + else: + n_stripped += 1 + pairs.append(pair) + + return pairs, n_filtered_errors, n_filtered_empty_tc, n_filtered_bare_tc, n_stripped + + +def _prepare_trajectory( + messages, + filter_errors=True, + filter_empty_tool_calls=False, + filter_bare_text_tool_calls=False, + random_strip_thinking_prob=0.0, + rng=None, +): + """Prepare a full trajectory for trajectory-level training. + + Cleans all messages preserving thinking for every assistant turn + (``strip_thinking=False``, ``ensure_thinking=True``). Identifies + which assistant segments should be masked (``loss_mask=0``) based + on error tool responses, empty tool calls, or bare-text tool calls. + + When *random_strip_thinking_prob* > 0, each assistant turn that has + thinking content is independently stripped with that probability. + Stripped turns have their ```` blocks and ``reasoning_content`` + completely removed (no empty ```` injected). + + Args: + messages: Raw trajectory messages. + filter_errors: If True (default), mask segments with error tool + responses. + filter_empty_tool_calls: If True, mask segments whose assistant + turn has no text content but has tool_calls. + filter_bare_text_tool_calls: If True, mask segments whose + assistant turn has text without ```` tags and has + tool_calls. + random_strip_thinking_prob: Probability of stripping thinking + from each assistant turn that has thinking content. + 0.0 (default) = no stripping, 1.0 = strip all. + rng: ``random.Random`` instance for reproducible sampling. + + Returns: + Tuple of ``(cleaned_messages, masked_segment_indices, + n_error, n_empty_tc, n_bare_tc, stripped_pattern)`` or ``None`` + if the trajectory has no assistant turns. *stripped_pattern* is + a ``frozenset`` of message indices whose thinking was stripped + (empty if no stripping occurred). + """ + segments = _find_segments(messages) + if not segments: + return None + + masked_indices = set() + n_error = 0 + n_empty_tc = 0 + n_bare_tc = 0 + for idx, (asst_start, seg_end) in enumerate(segments): + if filter_errors and _segment_has_error(messages, asst_start, seg_end): + masked_indices.add(idx) + n_error += 1 + continue + asst_msg = messages[asst_start] + if filter_empty_tool_calls and _is_empty_tool_call(asst_msg): + masked_indices.add(idx) + n_empty_tc += 1 + continue + if filter_bare_text_tool_calls and _is_bare_text_tool_call(asst_msg): + masked_indices.add(idx) + n_bare_tc += 1 + + # Determine which assistant turns to randomly strip thinking from. + strip_thinking_indices = set() + stripping_active = random_strip_thinking_prob > 0.0 and rng is not None + if stripping_active: + for asst_start, _seg_end in segments: + if _msg_has_thinking(messages[asst_start]): + if rng.random() < random_strip_thinking_prob: + strip_thinking_indices.add(asst_start) + + # When stripping is active (augmented variant), use ensure_thinking=False + # for ALL turns so that empty-thinking turns don't get \n + # injected. Only real thinking content is preserved. + # When stripping is inactive (variant 0 or no augmentation), keep + # ensure_thinking=True to match the standard training format. + default_ensure = not stripping_active + + cleaned = [] + for i, m in enumerate(messages): + if i in strip_thinking_indices: + cleaned.append( + _clean_message(m, strip_thinking=True, ensure_thinking=False) + ) + else: + cleaned.append( + _clean_message(m, strip_thinking=False, ensure_thinking=default_ensure) + ) + + return ( + cleaned, + sorted(masked_indices), + n_error, + n_empty_tc, + n_bare_tc, + frozenset(strip_thinking_indices), + ) + + +# ============================================================ +# 5. Tokenization — template detection, render, loss mask, dump +# ============================================================ + + +# -- Chat template patch (runtime, no file modification) -------- + +# Both Bailing and Qwen3 templates have ``ns.last_query_index`` logic +# that prevents ```` rendering for assistant turns BEFORE the +# last user message, AND discards inline empty ``\n`` +# extracted from content. +# +# This breaks trajectory-mode training: +# - Multi-user trajectories: turns before the last user msg lack +# - Empty ensure_thinking via inline gets stripped +# +# The patch below handles both Bailing (`ASSISTANT` style) +# and Qwen3 (`<|im_start|>assistant` style) templates: +# 1. Adds ``had_think_tags`` detection so empty ```` survives. +# 2. Removes the ``ns.last_query_index`` gate so all assistant turns +# render ```` uniformly when think intent is detected. +# +# Applied at runtime via ``tokenizer.chat_template = patched`` — the +# original template file on disk is never modified. + +_BAILING_OLD_BLOCK = ( + "{%- if loop.index0 > ns.last_query_index %}\n" + " {%- if reasoning_content != '' %}\n" + " {{- 'ASSISTANT\\n' + '\\n'" + " + reasoning_content.strip('\\n') + '\\n\\n\\n'" + " + content.lstrip('\\n') }}\n" + " {%- else %}\n" + " {{- 'ASSISTANT\\n' + content }}\n" + " {%- endif %}\n" + " {%- else %}\n" + " {{- 'ASSISTANT\\n' + content }}\n" + " {%- endif %}" +) +_BAILING_NEW_BLOCK = ( + "{%- if reasoning_content != '' or had_think_tags %}\n" + " {{- 'ASSISTANT\\n' + '\\n'" + " + reasoning_content.strip('\\n') + '\\n\\n\\n'" + " + content.lstrip('\\n') }}\n" + " {%- else %}\n" + " {{- 'ASSISTANT\\n' + content }}\n" + " {%- endif %}" +) + +# Qwen3 uses `loop.last or (not loop.last and reasoning_content)` so the +# last turn always renders even with empty reasoning. We +# preserve `loop.last` and add `had_think_tags` for inline-empty support. +_QWEN3_OLD_BLOCK = ( + "{%- if loop.index0 > ns.last_query_index %}\n" + " {%- if loop.last or (not loop.last and reasoning_content) %}\n" + " {{- '<|im_start|>' + message.role + '\\n\\n'" + " + reasoning_content.strip('\\n') + '\\n\\n\\n'" + " + content.lstrip('\\n') }}\n" + " {%- else %}\n" + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n" + " {%- endif %}\n" + " {%- else %}\n" + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n" + " {%- endif %}" +) +_QWEN3_NEW_BLOCK = ( + "{%- if loop.last or reasoning_content != '' or had_think_tags %}\n" + " {{- '<|im_start|>' + message.role + '\\n\\n'" + " + reasoning_content.strip('\\n') + '\\n\\n\\n'" + " + content.lstrip('\\n') }}\n" + " {%- else %}\n" + " {{- '<|im_start|>' + message.role + '\\n' + content }}\n" + " {%- endif %}" +) + +_OLD_DETECT = "{%- set reasoning_content = '' %}" +_NEW_DETECT = ( + "{%- set reasoning_content = '' %}\n" + " {%- set had_think_tags = ('' in content) %}" +) + + +def _patch_chat_template_for_training(tokenizer): + """Patch Bailing/Qwen3 chat templates to render ```` uniformly. + + Detects template family by matching known render blocks: + - Bailing: ``ASSISTANT`` markers + - Qwen3: ``<|im_start|>assistant`` markers + + Other templates (e.g. plain ChatML without ``last_query_index``) + are left unchanged. If the template has ``last_query_index`` but + neither known block matches, logs a warning. + """ + template = getattr(tokenizer, "chat_template", None) + if not template or "last_query_index" not in template: + return + + if _BAILING_OLD_BLOCK in template: + family = "Bailing" + patched = template.replace(_BAILING_OLD_BLOCK, _BAILING_NEW_BLOCK) + elif _QWEN3_OLD_BLOCK in template: + family = "Qwen3" + patched = template.replace(_QWEN3_OLD_BLOCK, _QWEN3_NEW_BLOCK) + else: + logger.warning( + "Chat template has last_query_index but matches neither " + "Bailing nor Qwen3 render block. Patch skipped — empty " + " may be discarded, multi-user turns may lack ." + ) + return + + patched = patched.replace(_OLD_DETECT, _NEW_DETECT) + + tokenizer.chat_template = patched + logger.info( + f"Patched {family} chat template for training: removed " + "last_query_index gate, added had_think_tags detection." + ) + + +_TEMPLATE_PATTERNS = [ + # ChatML (Qwen, etc.): <|im_start|>assistant\n ... <|im_end|> + (r"<\|im_start\|>assistant\n", r"<\|im_end\|>"), + # Llama 3: <|start_header_id|>assistant<|end_header_id|>\n\n ... <|eot_id|> + (r"<\|start_header_id\|>assistant<\|end_header_id\|>\n\n", r"<\|eot_id\|>"), + # GLM: <|assistant|> ... (ends at next <|user|>, <|observation|>, or end of string) + (r"<\|assistant\|>", r"(?=<\|user\|>|<\|observation\|>|\Z)"), +] + + +def _parse_tool_call_arguments(messages): + """Parse JSON-string arguments in tool_calls to dicts. + + OpenAI returns tool_call arguments as JSON strings, but some chat + templates (e.g. GLM-4.x / GLM-5.x) expect parsed dicts. Most other + templates (Qwen / ChatML, Llama 3, Bailing, ...) accept the standard + OpenAI string form, so this conversion must be opt-in. + """ + patched = [] + for m in messages: + tool_calls = m.get("tool_calls") + if not tool_calls: + patched.append(m) + continue + new_tcs = [] + for tc in tool_calls: + fn = tc.get("function", tc) + args = fn.get("arguments") + if isinstance(args, str): + try: + parsed = json.loads(args) + except (json.JSONDecodeError, TypeError): + parsed = args + fn = {**fn, "arguments": parsed} + tc = {**tc, "function": fn} if "function" in tc else fn + new_tcs.append(tc) + patched.append({**m, "tool_calls": new_tcs}) + return patched + + +def _render_tokenize_mask( + messages, + tokenizer, + assistant_pattern, + tools=None, + *, + split_mode="pair", + error_indices=None, + parse_tool_call_args=False, +): + """Render, tokenize, and build loss_mask for a message list. + + In **pair mode** (default), only the **last** assistant turn gets + ``loss_mask=1``. In **trajectory mode**, **all** assistant turns + get ``loss_mask=1`` except those at indices in *error_indices*. + + When *parse_tool_call_args* is True, JSON-string ``tool_calls`` arguments + are converted to dicts before rendering (required by GLM chat templates; + other templates such as Qwen / Llama / Bailing must keep the OpenAI + string form). + + Returns: + Tuple of ``(full_text, input_ids, loss_mask, offset_mapping)``, or + ``None`` if ``apply_chat_template`` fails. + """ + # 1) Render the full template text. + try: + kwargs = {"tokenize": False} + if tools is not None: + kwargs["tools"] = tools + if parse_tool_call_args: + messages = _parse_tool_call_arguments(messages) + full_text = tokenizer.apply_chat_template(messages, **kwargs) + except Exception as e: + logger.warning( + "apply_chat_template failed: %s. Skipping sample.", + e, + ) + return None + + # 2) Tokenize with offset mapping so we can map char→token. + encoding = tokenizer( + full_text, add_special_tokens=False, return_offsets_mapping=True + ) + input_ids = encoding["input_ids"] + offset_mapping = encoding["offset_mapping"] + + # 3) Build loss_mask. + loss_mask = [0] * len(input_ids) + + if split_mode == "trajectory": + # Trajectory mode: mask ALL assistant segments, skip error_indices. + skip = set(error_indices) if error_indices else set() + matches = list(assistant_pattern.finditer(full_text)) + + # Verify regex matches correspond 1:1 to assistant messages. + n_asst = sum(1 for m in messages if m.get("role") == "assistant") + if len(matches) != n_asst: + logger.warning( + "Segment count mismatch: %d assistant messages but %d regex " + "matches in rendered text. Training all matched segments " + "(error masking disabled for this sample).", + n_asst, + len(matches), + ) + skip = set() + + for seg_idx, m in enumerate(matches): + if seg_idx in skip: + continue + rs, re_ = m.start(1), m.end(0) + for tok_idx, (cs, ce) in enumerate(offset_mapping): + if ce > rs and cs < re_: + loss_mask[tok_idx] = 1 + else: + # Pair mode: mask only the LAST assistant segment. + last_match = None + for m in assistant_pattern.finditer(full_text): + last_match = m + if last_match is not None: + rs, re_ = last_match.start(1), last_match.end(0) + for tok_idx, (cs, ce) in enumerate(offset_mapping): + if ce > rs and cs < re_: + loss_mask[tok_idx] = 1 + + return full_text, input_ids, loss_mask, offset_mapping + + +class _TokenizeAndMask: + """Picklable callable for ``Dataset.map(num_proc=N)``.""" + + def __init__( + self, + tokenizer, + assistant_pattern, + max_length=None, + *, + split_mode="pair", + parse_tool_call_args=False, + ): + self.tokenizer = tokenizer + self.assistant_pattern = assistant_pattern + self.max_length = max_length + self.split_mode = split_mode + self.parse_tool_call_args = parse_tool_call_args + + def __call__(self, sample): + error_indices = ( + sample.get("error_indices", []) if self.split_mode == "trajectory" else None + ) + tools_json = sample.get("tools_json") + tools = json.loads(tools_json) if tools_json else None + result = _render_tokenize_mask( + sample["messages"], + self.tokenizer, + self.assistant_pattern, + tools, + split_mode=self.split_mode, + error_indices=error_indices, + parse_tool_call_args=self.parse_tool_call_args, + ) + if result is None: + return {"input_ids": [], "loss_mask": []} + + _full_text, input_ids, loss_mask, _offset_mapping = result + + # Early exit: overlength or empty → return empty so a single + # filter pass removes it together with template-failure empties. + if self.max_length is not None and len(input_ids) > self.max_length: + return {"input_ids": [], "loss_mask": []} + + return {"input_ids": input_ids, "loss_mask": loss_mask} + + +def _detect_template_pattern(tokenizer, tools=None): + """Detect the assistant role delimiter used by this tokenizer's template. + + When *tools* is provided the probe is rendered with ``tools=`` so that + the detected delimiters match the actual training text (some templates + alter the system block when tools are present). + + Strategy: + 1. Try known ``_TEMPLATE_PATTERNS`` (fast, battle-tested). + 2. Fall back to double-probe diff: render the template with a known + marker and with empty content, then diff the two strings to extract + the exact header and end-of-turn delimiters. + + Raises: + ValueError: If both strategies fail to detect a usable pattern. + """ + _PROBE_CONTENT = "PROBE_MARKER" + + extra_kwargs = {} + if tools is not None: + extra_kwargs["tools"] = tools + + probe_msgs = [ + {"role": "user", "content": "x"}, + {"role": "assistant", "content": _PROBE_CONTENT}, + ] + probe_text = tokenizer.apply_chat_template( + probe_msgs, tokenize=False, **extra_kwargs + ) + + # --- Strategy 1: known patterns --- + for hdr_re, eot_re in _TEMPLATE_PATTERNS: + if re.search(hdr_re, probe_text): + pattern = re.compile(hdr_re + r"(.*?)" + eot_re, re.DOTALL) + logger.info( + f"Detected template style (known pattern): " + f"header_re={hdr_re!r}, eot_re={eot_re!r}" + ) + return pattern + + # --- Strategy 2: double-probe diff --- + try: + probe_empty = [ + {"role": "user", "content": "x"}, + {"role": "assistant", "content": ""}, + ] + text_empty = tokenizer.apply_chat_template( + probe_empty, tokenize=False, **extra_kwargs + ) + + marker_idx = probe_text.index(_PROBE_CONTENT) + header = probe_text[:marker_idx] + tail = probe_text[marker_idx + len(_PROBE_CONTENT) :] + + if text_empty == header + tail: + # Extract the assistant-specific header by removing the shared + # user-only prefix. + user_only = tokenizer.apply_chat_template( + [{"role": "user", "content": "x"}], + tokenize=False, + **extra_kwargs, + ) + asst_header = header[len(user_only) :] + # end-of-turn delimiter: strip leading newlines, then take + # up to the first newline (or the full string if none). + eot_stripped = tail.lstrip("\n") + eot = eot_stripped.split("\n")[0] if "\n" in eot_stripped else eot_stripped + + if asst_header and eot: + hdr_re = re.escape(asst_header) + eot_re = re.escape(eot) + pattern = re.compile(hdr_re + r"(.*?)" + eot_re, re.DOTALL) + logger.info( + f"Detected template style (probe diff): " + f"header={asst_header!r}, eot={eot!r}" + ) + return pattern + except (ValueError, IndexError): + pass # PROBE_CONTENT not found in rendered text, skip + + raise ValueError( + "Could not detect chat template assistant delimiters. " + "Unable to build a reliable loss mask. " + f"Probe text: {probe_text[:200]!r}" + ) + + +def _dump_samples( + samples, + tokenizer, + assistant_pattern, + tools_list, + dump_dir, + n_samples, + *, + split_mode="pair", + error_indices_list=None, + parse_tool_call_args=False, +): + """Dump sampled message lists as ``.txt`` + ``.json`` for inspection. + + Args: + samples: List of message-list samples (pairs or full trajectories). + tokenizer: Tokenizer with ``apply_chat_template`` support. + assistant_pattern: Compiled regex from ``_detect_template_pattern``. + tools_list: Per-sample tool definitions (parallel to *samples*), + or ``None`` when no tools are available. + dump_dir: Directory to write files into (created if needed). + n_samples: Number of random samples to dump. ``-1`` dumps all. + split_mode: ``"trajectory"`` for trajectory-mode loss masking. + error_indices_list: Per-sample error segment indices (trajectory mode). + """ + import random as _random + + os.makedirs(dump_dir, exist_ok=True) + + if n_samples == -1 or n_samples >= len(samples): + indices = list(range(len(samples))) + else: + indices = sorted(_random.sample(range(len(samples)), n_samples)) + + n_written = 0 + for i in indices: + sample = samples[i] + sample_tools = tools_list[i] if tools_list else None + err_idxs = ( + error_indices_list[i] + if split_mode == "trajectory" and error_indices_list + else None + ) + + result = _render_tokenize_mask( + sample, + tokenizer, + assistant_pattern, + sample_tools, + split_mode=split_mode, + error_indices=err_idxs, + parse_tool_call_args=parse_tool_call_args, + ) + if result is None: + continue + + full_text, input_ids, loss_mask, offset_mapping = result + n_loss = sum(loss_mask) + base = os.path.join(dump_dir, f"sample_{i}") + + # --- .txt --- + with open(base + ".txt", "w", encoding="utf-8") as fout: + fout.write( + f"Sample {i}: {len(sample)} messages, " + f"{len(input_ids)} tokens, loss=1: {n_loss}\n" + ) + fout.write(f"Last msg role: {sample[-1]['role']}\n") + fout.write(f"{'=' * 72}\n\n") + + fout.write("--- Rendered Text ---\n") + fout.write(full_text) + fout.write("\n\n") + + fout.write("--- Token / Loss Mask ---\n") + fout.write(f"{'Idx':>6} | {'TokenID':>8} | Loss | Token Text\n") + fout.write(f"{'-' * 6}-+-{'-' * 8}-+------+{'-' * 40}\n") + for t in range(len(input_ids)): + cs, ce = offset_mapping[t] + tok_text = repr(full_text[cs:ce]) + fout.write( + f"{t:>6} | {input_ids[t]:>8} | {loss_mask[t]:>4} | {tok_text}\n" + ) + + # --- .json --- + tokens_list = [] + for t in range(len(input_ids)): + cs, ce = offset_mapping[t] + tokens_list.append( + { + "idx": t, + "token_id": input_ids[t], + "text": full_text[cs:ce], + "loss": loss_mask[t], + } + ) + record = { + "sample_index": i, + "n_messages": len(sample), + "n_tokens": len(input_ids), + "n_loss_tokens": n_loss, + "rendered_text": full_text, + "tokens": tokens_list, + } + with open(base + ".json", "w", encoding="utf-8") as fout: + json.dump(record, fout, ensure_ascii=False) + + n_written += 1 + + logger.info(f"Dumped {n_written} samples to {dump_dir}/") + + +# ============================================================ +# 6. Pipeline — loading, processing, distributed cache, public API +# ============================================================ + + +def _load_trajectory_pairs( + path: str, + filter_errors: bool = True, + strip_all_thinking: bool = False, + filter_empty_tool_calls: bool = False, + filter_bare_text_tool_calls: bool = False, + truncate_task_notifications: bool = False, + max_no_thinking_ratio: float | None = None, + random_strip_thinking_prob: float = 0.0, + random_strip_thinking_seed: int = 42, + n_thinking_variants: int = 1, +): + """Load trajectory JSONL and split into progressive pairs. + + When *n_thinking_variants* > 1, each trajectory is split K times: + variant 0 preserves all thinking, variants 1~K-1 randomly strip. + + Supports nested (``conversations`` wrapper) and flat JSONL formats + (auto-detected per record via ``_iter_jsonl_records``). + + Returns: + Tuple of ``(all_pairs, tools)`` where *tools* is ``None`` when no + tool definitions are found. + """ + all_pairs = [] + all_tools = [] + records_in = 0 + total_filtered_errors = 0 + total_filtered_empty_tc = 0 + total_filtered_bare_tc = 0 + total_truncated = 0 + total_stripped_thinking = 0 + + augment = n_thinking_variants > 1 + rng = ( + random.Random(random_strip_thinking_seed) + if random_strip_thinking_prob > 0.0 + else None + ) + + if augment and random_strip_thinking_prob <= 0.0: + logger.warning( + "n_thinking_variants=%d but random_strip_thinking_prob=0; " + "all variants will be identical.", + n_thinking_variants, + ) + + # Stats collectors for augmentation logging. + thinking_turns_per_traj = [] + total_asst_turns_per_traj = [] + patterns_per_traj = [] + + for record_idx, messages, record_tools in _iter_jsonl_records(path): + records_in = record_idx + + if truncate_task_notifications: + truncated = _truncate_at_task_notification(messages) + if len(truncated) < len(messages): + total_truncated += 1 + messages = truncated + + shared_kwargs = dict( + filter_errors=filter_errors, + strip_all_thinking=strip_all_thinking, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + ) + + if augment: + # Variant 0: preserve all thinking. + pairs_orig, n_err, n_empty_tc, n_bare_tc, _ = _split_and_filter( + messages, **shared_kwargs, random_strip_thinking_prob=0.0, rng=None + ) + total_filtered_errors += n_err + total_filtered_empty_tc += n_empty_tc + total_filtered_bare_tc += n_bare_tc + all_pairs.extend(pairs_orig) + all_tools.extend([record_tools] * len(pairs_orig)) + # Collect stats. + segments = _find_segments(messages) + n_think = sum(1 for s, _ in segments if _msg_has_thinking(messages[s])) + n_asst = len(segments) + thinking_turns_per_traj.append(n_think) + total_asst_turns_per_traj.append(n_asst) + + # Variants 1 ~ K-1: random strip. + variant_patterns = {frozenset()} # original = no strip + for _k in range(n_thinking_variants - 1): + pairs_aug, _, _, _, n_stripped = _split_and_filter( + messages, + **shared_kwargs, + random_strip_thinking_prob=random_strip_thinking_prob, + rng=rng, + ) + total_stripped_thinking += n_stripped + all_pairs.extend(pairs_aug) + all_tools.extend([record_tools] * len(pairs_aug)) + # Approximate pattern: record which pairs had their target stripped. + # For stats, use the count as a proxy since _split_and_filter + # doesn't return per-pair strip info. + variant_patterns.add(frozenset([n_stripped])) + patterns_per_traj.append(variant_patterns) + else: + # Single variant (original behavior). + pairs, n_err, n_empty_tc, n_bare_tc, n_stripped = _split_and_filter( + messages, + **shared_kwargs, + random_strip_thinking_prob=random_strip_thinking_prob, + rng=rng, + ) + total_filtered_errors += n_err + total_filtered_empty_tc += n_empty_tc + total_filtered_bare_tc += n_bare_tc + total_stripped_thinking += n_stripped + all_pairs.extend(pairs) + all_tools.extend([record_tools] * len(pairs)) + + # Log extracted tools summary. + n_with_tools = sum(1 for t in all_tools if t is not None) + if n_with_tools > 0: + all_tool_names = set() + for t_list in all_tools: + if t_list is not None: + for t in t_list: + all_tool_names.add(t.get("function", {}).get("name", "?")) + logger.info( + f"Extracted tools from {n_with_tools}/{len(all_tools)} pairs: " + f"{sorted(all_tool_names)}" + ) + + filter_parts = [] + if total_truncated: + filter_parts.append( + f"{total_truncated} trajectories truncated at task-notification" + ) + if total_filtered_errors: + filter_parts.append(f"{total_filtered_errors} with tool errors") + if total_filtered_empty_tc: + filter_parts.append(f"{total_filtered_empty_tc} empty-content tool calls") + if total_filtered_bare_tc: + filter_parts.append(f"{total_filtered_bare_tc} bare-text tool calls") + if total_stripped_thinking: + filter_parts.append(f"{total_stripped_thinking} thinking blocks stripped") + filter_msg = ", ".join(filter_parts) if filter_parts else "none" + + logger.info( + f"Loaded {records_in} trajectories, " + f"generated {len(all_pairs)} pairs " + f"(filtered: {filter_msg})" + ) + + if augment and patterns_per_traj: + _log_thinking_augmentation_stats( + n_thinking_variants, + random_strip_thinking_prob, + records_in, + thinking_turns_per_traj, + total_asst_turns_per_traj, + patterns_per_traj, + ) + + # Balance thinking / no-thinking pair ratio. + all_pairs, all_tools = _balance_thinking_pairs( + all_pairs, max_no_thinking_ratio, tools_list=all_tools + ) + + return all_pairs, all_tools + + +def _load_presplit_pairs( + path: str, + strip_all_thinking: bool = False, + random_strip_thinking_prob: float = 0.0, + random_strip_thinking_seed: int = 42, + n_thinking_variants: int = 1, +): + """Load pre-split pair JSONL where each line is ``{"messages": [...]}``. + + Messages are cleaned but no splitting or error-filtering is performed. + By default, thinking is stripped from context assistant turns but + preserved for the last assistant turn (the training target). Set + *strip_all_thinking* to strip from every assistant turn. + + When *n_thinking_variants* > 1, each pair is augmented: variant 0 + preserves thinking, variants 1~K-1 randomly strip the target turn. + + Also extracts per-record ``tools`` definitions so that each pair + carries its own tools, same as ``_load_trajectory_pairs``. + + Returns: + Tuple of ``(all_pairs, all_tools)`` where *all_tools* is a + parallel list of per-sample tool definitions (may be ``None``). + """ + all_pairs = [] + all_tools = [] + n_stripped = 0 + augment = n_thinking_variants > 1 + + rng = ( + random.Random(random_strip_thinking_seed) + if random_strip_thinking_prob > 0.0 + else None + ) + + def _build_pair(messages, last_asst, strip_target): + pair = [] + for idx, m in enumerate(messages): + is_target = m.get("role") == "assistant" and idx == last_asst + strip = strip_all_thinking or not is_target or strip_target + pair.append(_clean_message(m, strip_thinking=strip)) + return pair + + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + record = json.loads(line) + messages = record.get("messages", []) + if not messages: + continue + + record_tools = record.get("tools") + + # Find the last assistant index so we can preserve its thinking. + last_asst = None + for i, m in enumerate(messages): + if m.get("role") == "assistant": + last_asst = i + + has_thinking = ( + last_asst is not None + and not strip_all_thinking + and _msg_has_thinking(messages[last_asst]) + ) + + if augment: + # Variant 0: preserve all thinking. + all_pairs.append(_build_pair(messages, last_asst, strip_target=False)) + all_tools.append(record_tools) + + # Variants 1 ~ K-1: random strip. + for _k in range(n_thinking_variants - 1): + do_strip = ( + has_thinking + and rng is not None + and rng.random() < random_strip_thinking_prob + ) + if do_strip: + n_stripped += 1 + all_pairs.append( + _build_pair(messages, last_asst, strip_target=do_strip) + ) + all_tools.append(record_tools) + else: + # Single variant (original behavior). + strip_target = ( + has_thinking + and rng is not None + and rng.random() < random_strip_thinking_prob + ) + if strip_target: + n_stripped += 1 + all_pairs.append( + _build_pair(messages, last_asst, strip_target=strip_target) + ) + all_tools.append(record_tools) + + # Log extracted tools summary. + n_with_tools = sum(1 for t in all_tools if t is not None) + if n_with_tools > 0: + all_tool_names = set() + for t_list in all_tools: + if t_list is not None: + for t in t_list: + all_tool_names.add(t.get("function", {}).get("name", "?")) + logger.info( + f"Extracted tools from {n_with_tools}/{len(all_tools)} pairs: " + f"{sorted(all_tool_names)}" + ) + + strip_msg = f", {n_stripped} thinking blocks stripped" if n_stripped else "" + logger.info(f"Loaded {len(all_pairs)} pre-split pairs from {path}{strip_msg}") + return all_pairs, all_tools + + +def _load_full_trajectories( + path: str, + filter_errors: bool = True, + filter_empty_tool_calls: bool = False, + filter_bare_text_tool_calls: bool = False, + truncate_task_notifications: bool = False, + random_strip_thinking_prob: float = 0.0, + random_strip_thinking_seed: int = 42, + n_thinking_variants: int = 1, +): + """Load trajectory JSONL for trajectory-level training. + + Each trajectory becomes a single training sample with all assistant + turns as targets (``loss_mask=1``). When *filter_errors* is True, + assistant segments with error tool responses are identified so + tokenization can mask them (``loss_mask=0``) instead of discarding + the entire trajectory. + + When *n_thinking_variants* > 1, each trajectory is augmented into + K variants: the first preserves all thinking, the remaining K-1 + randomly strip thinking turns with *random_strip_thinking_prob*. + + Supports nested (``conversations`` wrapper) and flat JSONL formats + (auto-detected per record via ``_iter_jsonl_records``). + + Returns: + Tuple of ``(trajectories, error_indices_list, all_tools)`` where + *trajectories* is a list of cleaned message lists, + *error_indices_list* is a list of error segment index lists, + and *all_tools* is a parallel list of per-sample tool definitions. + """ + trajectories = [] + error_indices_list = [] + all_tools = [] + records_in = 0 + total_truncated = 0 + total_masked_errors = 0 + total_masked_empty_tc = 0 + total_masked_bare_tc = 0 + total_stripped_thinking = 0 + + augment = n_thinking_variants > 1 + rng = ( + random.Random(random_strip_thinking_seed) + if random_strip_thinking_prob > 0.0 + else None + ) + + if augment and random_strip_thinking_prob <= 0.0: + logger.warning( + "n_thinking_variants=%d but random_strip_thinking_prob=0; " + "all variants will be identical.", + n_thinking_variants, + ) + + # Stats collectors for augmentation logging. + thinking_turns_per_traj = [] + total_asst_turns_per_traj = [] + patterns_per_traj = [] + + for record_idx, messages, record_tools in _iter_jsonl_records(path): + records_in = record_idx + + if truncate_task_notifications: + truncated = _truncate_at_task_notification(messages) + if len(truncated) < len(messages): + total_truncated += 1 + messages = truncated + + shared_kwargs = dict( + filter_errors=filter_errors, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + ) + + if augment: + # Variant 0: preserve all thinking (no stripping). + result_orig = _prepare_trajectory( + messages, **shared_kwargs, random_strip_thinking_prob=0.0, rng=None + ) + if result_orig is None: + continue + cleaned_orig, masked_idxs, n_err, n_empty_tc, n_bare_tc, _ = result_orig + trajectories.append(cleaned_orig) + error_indices_list.append(masked_idxs) + all_tools.append(record_tools) + total_masked_errors += n_err + total_masked_empty_tc += n_empty_tc + total_masked_bare_tc += n_bare_tc + + # Collect stats: count thinking turns in this trajectory. + segments = _find_segments(messages) + n_think = sum(1 for s, _ in segments if _msg_has_thinking(messages[s])) + n_asst = len(segments) + thinking_turns_per_traj.append(n_think) + total_asst_turns_per_traj.append(n_asst) + + # Variants 1 ~ K-1: random strip thinking. + variant_patterns = {frozenset()} # original = empty pattern + for _k in range(n_thinking_variants - 1): + result_aug = _prepare_trajectory( + messages, + **shared_kwargs, + random_strip_thinking_prob=random_strip_thinking_prob, + rng=rng, + ) + if result_aug is None: + continue + cleaned_aug, _, _, _, _, strip_pattern = result_aug + trajectories.append(cleaned_aug) + error_indices_list.append(masked_idxs) # reuse + all_tools.append(record_tools) + total_stripped_thinking += len(strip_pattern) + variant_patterns.add(strip_pattern) + patterns_per_traj.append(variant_patterns) + else: + # Single variant (original behavior). + result = _prepare_trajectory( + messages, + **shared_kwargs, + random_strip_thinking_prob=random_strip_thinking_prob, + rng=rng, + ) + if result is None: + continue + cleaned, masked_idxs, n_err, n_empty_tc, n_bare_tc, strip_pattern = result + trajectories.append(cleaned) + error_indices_list.append(masked_idxs) + all_tools.append(record_tools) + total_masked_errors += n_err + total_masked_empty_tc += n_empty_tc + total_masked_bare_tc += n_bare_tc + total_stripped_thinking += len(strip_pattern) + + # Log extracted tools summary. + n_with_tools = sum(1 for t in all_tools if t is not None) + if n_with_tools > 0: + all_tool_names = set() + for t_list in all_tools: + if t_list is not None: + for t in t_list: + all_tool_names.add(t.get("function", {}).get("name", "?")) + logger.info( + f"Extracted tools from {n_with_tools}/{len(all_tools)} " + f"trajectories: {sorted(all_tool_names)}" + ) + + parts = [] + if total_truncated: + parts.append(f"{total_truncated} trajectories truncated at task-notification") + if total_masked_errors: + parts.append(f"{total_masked_errors} with tool errors") + if total_masked_empty_tc: + parts.append(f"{total_masked_empty_tc} empty-content tool calls") + if total_masked_bare_tc: + parts.append(f"{total_masked_bare_tc} bare-text tool calls") + if total_stripped_thinking: + parts.append(f"{total_stripped_thinking} thinking blocks stripped") + mask_msg = ", ".join(parts) if parts else "none" + + logger.info( + f"Loaded {records_in} trajectories, " + f"kept {len(trajectories)} for training " + f"(masked: {mask_msg})" + ) + + if augment and patterns_per_traj: + _log_thinking_augmentation_stats( + n_thinking_variants, + random_strip_thinking_prob, + records_in, + thinking_turns_per_traj, + total_asst_turns_per_traj, + patterns_per_traj, + ) + + return trajectories, error_indices_list, all_tools + + +def _tokenize_samples( + messages_list, + tools_list, + tokenizer, + *, + split_mode: str = "pair", + error_indices_list: list | None = None, + max_length: int | None = None, + num_proc: int | None = None, + no_tools: bool = False, + dump_dir: str | None = None, + dump_n_samples: int = 0, + parse_tool_call_args: bool = False, +): + """Tokenize message lists into a training-ready Dataset. + + Works for both progressive pairs (``split_mode="pair"``) and + full trajectories (``split_mode="trajectory"``). + + In pair mode, only the last assistant turn per sample gets + ``loss_mask=1``. In trajectory mode, all assistant turns get + ``loss_mask=1`` except those at error segment indices. + + Args: + tools_list: Per-sample tool definitions (parallel to + *messages_list*). Each element is either ``None`` or a + list of tool dicts. + """ + if num_proc is None: + num_proc = max(1, min(os.cpu_count() or 1, DATASET_NUM_PROC)) + + # Find representative tools for template detection. + first_tools = None + if tools_list: + first_tools = next((t for t in tools_list if t is not None), None) + + if no_tools: + tools_list = None + first_tools = None + logger.info("Tool definitions disabled (no_tools=True)") + elif first_tools is not None: + all_tool_names = set() + for t_list in tools_list: + if t_list is not None: + for t in t_list: + all_tool_names.add(t.get("function", {}).get("name", "?")) + logger.info(f"Using tools for chat template: {sorted(all_tool_names)}") + + if not messages_list: + raise ValueError("No valid samples to tokenize") + + # Build dataset columns. + data = {"messages": messages_list} + # Serialize per-sample tools as JSON strings for the Dataset column. + data["tools_json"] = ( + [json.dumps(t) if t else "" for t in tools_list] + if tools_list + else [""] * len(messages_list) + ) + remove_cols = ["messages", "tools_json"] + if split_mode == "trajectory": + data["error_indices"] = error_indices_list or [[] for _ in messages_list] + remove_cols.append("error_indices") + + dataset = Dataset.from_dict(data) + _patch_chat_template_for_training(tokenizer) + assistant_pattern = _detect_template_pattern(tokenizer, tools=first_tools) + + # Dump samples for inspection before the heavy map() pass. + if dump_dir and dump_n_samples != 0: + _dump_samples( + messages_list, + tokenizer, + assistant_pattern, + tools_list, + dump_dir, + dump_n_samples, + split_mode=split_mode, + error_indices_list=error_indices_list, + parse_tool_call_args=parse_tool_call_args, + ) + + process_fn = _TokenizeAndMask( + tokenizer, + assistant_pattern, + max_length=max_length, + split_mode=split_mode, + parse_tool_call_args=parse_tool_call_args, + ) + + dataset = dataset.map(process_fn, num_proc=num_proc).remove_columns(remove_cols) + + # Single filter pass: removes both apply_chat_template-failure empties and + # overlength samples (which _TokenizeAndMask also marks as empty). + before_filter = len(dataset) + dataset = dataset.filter(lambda x: len(x["input_ids"]) > 0, num_proc=num_proc) + n_filtered = before_filter - len(dataset) + if n_filtered > 0: + logger.info( + f"Filtered {n_filtered} samples " + f"(empty from template failures or exceeding max_length={max_length})" + ) + + logger.info(f"Final dataset: {len(dataset)} samples") + return dataset + + +def _process_swe_sft( + path: str, + tokenizer, + *, + max_length: int | None = None, + num_proc: int | None = None, + pre_split: bool = False, + filter_errors: bool = True, + strip_all_thinking: bool = False, + filter_empty_tool_calls: bool = False, + filter_bare_text_tool_calls: bool = False, + truncate_task_notifications: bool = False, + no_tools: bool = False, + max_no_thinking_ratio: float | None = None, + split_mode: str = "pair", + random_strip_thinking_prob: float = 0.0, + random_strip_thinking_seed: int = 42, + n_thinking_variants: int = 1, + dump_dir: str | None = None, + dump_n_samples: int = 0, + parse_tool_call_args: bool = False, +): + """Load JSONL, split into pairs, tokenize, and filter. + + Combines file loading with ``_tokenize_samples`` so that the rank-0-only + path and the single-process path share the same logic. + + When *split_mode* is ``"trajectory"``, the full trajectory is kept as a + single training sample with all assistant turns as targets. + """ + error_indices_list = None + + if split_mode == "trajectory": + messages_list, error_indices_list, tools_list = _load_full_trajectories( + path, + filter_errors=filter_errors, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + truncate_task_notifications=truncate_task_notifications, + random_strip_thinking_prob=random_strip_thinking_prob, + random_strip_thinking_seed=random_strip_thinking_seed, + n_thinking_variants=n_thinking_variants, + ) + elif pre_split: + messages_list, tools_list = _load_presplit_pairs( + path, + strip_all_thinking=strip_all_thinking, + random_strip_thinking_prob=random_strip_thinking_prob, + random_strip_thinking_seed=random_strip_thinking_seed, + n_thinking_variants=n_thinking_variants, + ) + else: + messages_list, tools_list = _load_trajectory_pairs( + path, + filter_errors=filter_errors, + strip_all_thinking=strip_all_thinking, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + truncate_task_notifications=truncate_task_notifications, + max_no_thinking_ratio=max_no_thinking_ratio, + random_strip_thinking_prob=random_strip_thinking_prob, + random_strip_thinking_seed=random_strip_thinking_seed, + n_thinking_variants=n_thinking_variants, + ) + + return _tokenize_samples( + messages_list, + tools_list, + tokenizer, + split_mode=split_mode, + error_indices_list=error_indices_list, + max_length=max_length, + num_proc=num_proc, + no_tools=no_tools, + dump_dir=dump_dir, + dump_n_samples=dump_n_samples, + parse_tool_call_args=parse_tool_call_args, + ) + + +def get_swe_sft_dataset( + path: str, + split: str | None = None, + tokenizer=None, + max_length: int | None = None, + num_proc: int | None = None, + pre_split: bool = False, + filter_errors: bool = True, + strip_all_thinking: bool = False, + filter_empty_tool_calls: bool = False, + filter_bare_text_tool_calls: bool = False, + truncate_task_notifications: bool = False, + no_tools: bool = False, + skip_pretokenized_filter: bool = False, + max_no_thinking_ratio: float | None = None, + split_mode: str = "pair", + random_strip_thinking_prob: float = 0.0, + random_strip_thinking_seed: int = 42, + n_thinking_variants: int = 1, + cache_dir: str | None = None, + dump_dir: str | None = None, + dump_samples: int = 0, + parse_tool_call_args: bool = False, +): + """Load SWE trajectory data and convert to SFT training pairs. + + By default, tool definitions are auto-extracted from the training data's + ``conversations[].tools`` field and passed to ``apply_chat_template`` + so that the tokenizer renders tool definitions in the system prompt + (e.g. Qwen3 ``# Tools`` block), matching the eval-time format. + Set *no_tools* to skip this and render without tool definitions. + + When *split_mode* is ``"trajectory"``, the full trajectory is kept as a + single training sample with all assistant turns as targets + (``loss_mask=1``). Error segments are masked (``loss_mask=0``) + when *filter_errors* is True, instead of being discarded. + Thinking is preserved by default but can be randomly stripped + per-turn via *random_strip_thinking_prob* (both modes). + + In distributed (SPMD) mode, only rank 0 performs the heavy processing + (JSONL loading, pair splitting, tokenization) and saves the result as + an Arrow dataset to *cache_dir*. Other ranks wait for rank 0 to + finish and then load the cached dataset directly via memory-mapped I/O. + + Args: + path: Path to the JSONL file containing SWE trajectories, or a + directory containing a pre-tokenized Arrow dataset (saved by + ``python -m areal.dataset.swe_sft --save-tokenized``). + split: Unused, kept for API compatibility. + tokenizer: Tokenizer with ``apply_chat_template`` support. + Not required when loading a pre-tokenized dataset. + max_length: Max token length. Longer sequences are filtered out. + num_proc: Number of parallel workers for tokenization. + Defaults to ``min(os.cpu_count(), DATASET_NUM_PROC)``. + pre_split: If True, treat input as pre-split pairs (each line is + ``{"messages": [...]}``) instead of full trajectories. + filter_errors: If True (default), discard pairs whose current segment + contains a tool result with ``is_error=True``. In trajectory + mode, sets ``loss_mask=0`` for error segments instead. + Set to False to keep/train all regardless of tool errors. + strip_all_thinking: If True, strip ``...`` from every + assistant turn including the training target. + Ignored in trajectory mode (thinking is always preserved). + filter_empty_tool_calls: If True, discard pairs whose training-target + assistant turn has no text content but has tool_calls. + filter_bare_text_tool_calls: If True, discard pairs whose + training-target assistant turn has text without ```` + tags and has tool_calls. + truncate_task_notifications: If True, truncate trajectories at the + first ```` that follows a pure-text assistant + turn, removing noise from background task completions. + no_tools: If True, do not pass tool definitions to + ``apply_chat_template`` even if the data contains them. + skip_pretokenized_filter: If True, skip the ``max_length`` filter + when loading a pre-tokenized dataset. Useful when the dataset + was already filtered during pretokenization and you want to + avoid NFS cache conflicts from concurrent ``dataset.filter()`` + calls across ranks. + max_no_thinking_ratio: Maximum ratio of non-thinking pairs to thinking + pairs. For example, ``1.0`` gives 1:1, ``2.0`` gives 1:2. + ``None`` (default) disables balancing. + split_mode: ``"pair"`` (default) splits trajectories into + progressive pairs. ``"trajectory"`` keeps the full trajectory + as a single sample — all assistant turns are targets with + ``loss_mask=1``, error segments are masked instead of filtered. + random_strip_thinking_prob: Probability of stripping thinking from + each target assistant turn. 0.0 (default) = no stripping, + 1.0 = strip all. Works in both pair and trajectory mode. + random_strip_thinking_seed: Random seed for reproducible thinking + stripping decisions. + n_thinking_variants: Number of thinking-pattern variants per + trajectory. ``1`` (default) = no augmentation. ``K > 1`` + = augment each trajectory into K variants: the first + preserves all thinking, the rest randomly strip with + *random_strip_thinking_prob*. + cache_dir: Directory to save/load the processed Arrow dataset. + When set in distributed mode, rank 0 processes the data and + saves here; other ranks load from this directory. If the + directory already contains a completed cache (``.done`` marker), + all ranks load from it directly without reprocessing. + dump_dir: Directory to write sample dump files (``.txt`` + ``.json``). + Only rank 0 writes. Set to None to disable. + dump_samples: Number of random samples to dump. ``-1`` = all, + ``0`` = disabled. + parse_tool_call_args: If True, convert OpenAI JSON-string + ``tool_calls.arguments`` to dicts before ``apply_chat_template``. + Required by GLM-4.x / GLM-5.x templates; leave at the default + (False) for Qwen / Llama / Bailing. + + Returns: + A HuggingFace ``Dataset`` with ``input_ids`` and ``loss_mask`` columns. + """ + from datasets import load_from_disk + + # Pre-tokenized Arrow dataset: load directly, skip all processing. + if os.path.isdir(path): + logger.info(f"Loading pre-tokenized dataset from {path}") + dataset = load_from_disk(path) + + if max_length is not None and not skip_pretokenized_filter: + before_filter = len(dataset) + dataset = dataset.filter( + lambda x: len(x["input_ids"]) <= max_length, num_proc=num_proc + ) + logger.info( + f"Filtered {before_filter - len(dataset)} samples " + f"exceeding max_length={max_length}" + ) + + logger.info(f"Final dataset: {len(dataset)} samples") + return dataset + + # --- Shared kwargs for _process_swe_sft --- + process_kwargs = dict( + max_length=max_length, + num_proc=num_proc, + pre_split=pre_split, + filter_errors=filter_errors, + strip_all_thinking=strip_all_thinking, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + truncate_task_notifications=truncate_task_notifications, + no_tools=no_tools, + max_no_thinking_ratio=max_no_thinking_ratio, + split_mode=split_mode, + random_strip_thinking_prob=random_strip_thinking_prob, + random_strip_thinking_seed=random_strip_thinking_seed, + n_thinking_variants=n_thinking_variants, + dump_dir=dump_dir, + dump_n_samples=dump_samples, + parse_tool_call_args=parse_tool_call_args, + ) + + # --- Distributed rank-0-only processing --- + rank = int(os.getenv("RANK", "0")) + world_size = int(os.getenv("WORLD_SIZE", "1")) + + if cache_dir is not None and world_size > 1: + done_marker = os.path.join(cache_dir, ".done") + + def _filter_by_max_length(ds): + if max_length is None: + return ds + before = len(ds) + keep = [i for i in range(len(ds)) if len(ds[i]["input_ids"]) <= max_length] + ds = ds.select(keep) + if len(ds) < before: + logger.info( + f"Rank {rank}: filtered {before - len(ds)} samples " + f"exceeding max_length={max_length}" + ) + return ds + + # Fast path: cache from a previous run (or rank 0 already finished). + if os.path.exists(done_marker): + logger.info( + f"Rank {rank}: loading cached processed dataset from {cache_dir}" + ) + dataset = load_from_disk(cache_dir) + dataset = _filter_by_max_length(dataset) + logger.info(f"Final dataset: {len(dataset)} samples") + return dataset + + if rank == 0: + # Rank 0: do the heavy processing and save for other ranks. + dataset = _process_swe_sft(path, tokenizer, **process_kwargs) + os.makedirs(cache_dir, exist_ok=True) + dataset.save_to_disk(cache_dir) + # Write marker AFTER save completes so readers see a consistent dir. + with open(done_marker, "w") as f: + f.write(str(len(dataset))) + logger.info( + f"Rank 0: saved processed dataset " + f"({len(dataset)} samples) to {cache_dir}" + ) + dataset = _filter_by_max_length(dataset) + return dataset + else: + # Other ranks: wait for rank 0, then load. + logger.info(f"Rank {rank}: waiting for rank 0 to process dataset...") + _wait_for_marker(done_marker) + dataset = load_from_disk(cache_dir) + dataset = _filter_by_max_length(dataset) + logger.info(f"Rank {rank}: loaded cached dataset ({len(dataset)} samples)") + return dataset + + # --- Non-distributed or no cache_dir: process in current process --- + return _process_swe_sft(path, tokenizer, **process_kwargs) + + +# ============================================================ +# 7. CLI — ``python -m areal.dataset.swe_sft`` +# ============================================================ + +if __name__ == "__main__": + import argparse + import sys + + from transformers import AutoTokenizer + + parser = argparse.ArgumentParser( + description="Verify SWE SFT pair generation and loss masking.", + ) + parser.add_argument("path", help="Path to SWE trajectory JSONL file") + parser.add_argument( + "--tokenizer", + default="Qwen/Qwen3-8B", + help="HuggingFace tokenizer name or path (default: Qwen/Qwen3-8B)", + ) + parser.add_argument( + "--max-length", + type=int, + default=None, + help="Filter samples exceeding this token length", + ) + parser.add_argument( + "--num-samples", + "-n", + type=int, + default=None, + help="Number of pairs to process. Controls loading, tokenization," + " display, and export. Default: all pairs.", + ) + parser.add_argument( + "--num-proc", + type=int, + default=None, + help=f"Number of parallel workers (default: min(cpu_count, {DATASET_NUM_PROC}))", + ) + parser.add_argument( + "--save-pairs", + "-o", + default=None, + metavar="FILE", + help='Save cleaned pairs to FILE (JSONL, each line: {"messages": [...]}).', + ) + parser.add_argument( + "--pre-split", + action="store_true", + help='Input is already in pair format (each line: {"messages": [...]}).' + " Skip trajectory splitting and error filtering.", + ) + parser.add_argument( + "--no-filter-errors", + action="store_true", + help="Keep pairs whose current segment contains tool results with " + "is_error=True (by default these are discarded).", + ) + parser.add_argument( + "--save-tokenized", + default=None, + metavar="DIR", + help="Save the tokenized dataset to DIR (Arrow format). " + "The saved directory can be used directly as the dataset path " + "during training, skipping all processing.", + ) + parser.add_argument( + "--strip-all-thinking", + action="store_true", + help="Strip ... from ALL assistant turns including " + "the training target. By default only context turns are stripped.", + ) + parser.add_argument( + "--no-tools", + action="store_true", + help="Do not pass tool definitions to apply_chat_template. " + "By default, tools are auto-extracted from the data and rendered " + "in the system prompt (e.g. Qwen3 '# Tools' block).", + ) + parser.add_argument( + "--parse-tool-call-args", + action="store_true", + help="Convert OpenAI JSON-string tool_calls.arguments to dicts " + "before apply_chat_template. Required by GLM-4.x / GLM-5.x " + "templates; leave off for Qwen / Llama / Bailing (which expect " + "the standard string form).", + ) + parser.add_argument( + "--filter-empty-tool-calls", + action="store_true", + help="Discard pairs whose training-target assistant turn has no " + "text content but has tool_calls (silent tool invocations).", + ) + parser.add_argument( + "--filter-bare-text-tool-calls", + action="store_true", + help="Discard pairs whose training-target assistant turn has text " + "content without tags and has tool_calls.", + ) + parser.add_argument( + "--truncate-task-notifications", + action="store_true", + help="Truncate trajectories at the first that " + "follows a pure-text assistant turn. Removes noise from background " + "task completions (e.g. pip install finishing after the model's summary).", + ) + parser.add_argument( + "--max-no-thinking-ratio", + type=float, + default=None, + help="Maximum ratio of non-thinking pairs to thinking pairs. " + "E.g. 1.0 = 1:1 balance, 2.0 = at most 2x non-thinking per " + "thinking pair. Non-thinking pairs are randomly downsampled. " + "Default: no balancing.", + ) + parser.add_argument( + "--split-mode", + choices=["pair", "trajectory"], + default="pair", + help="Sample construction mode. 'pair' (default): split trajectories " + "into progressive pairs. 'trajectory': keep the full trajectory " + "as a single sample with all assistant turns as targets.", + ) + parser.add_argument( + "--random-strip-thinking-prob", + type=float, + default=0.0, + help="Probability of stripping thinking from each target assistant " + "turn. 0.0 = no stripping (default), 1.0 = strip all. " + "Works in both pair mode and trajectory mode.", + ) + parser.add_argument( + "--random-strip-thinking-seed", + type=int, + default=42, + help="Random seed for reproducible thinking stripping decisions (default: 42).", + ) + parser.add_argument( + "--n-thinking-variants", + type=int, + default=1, + help="Number of thinking-pattern variants per trajectory. " + "1 = no augmentation (default). K > 1 = augment each trajectory " + "into K variants: the first preserves all thinking, the rest " + "randomly strip with --random-strip-thinking-prob.", + ) + parser.add_argument( + "--save-trajectories", + default=None, + metavar="FILE", + help="Save preprocessed trajectories to FILE (JSONL, original format) " + "after applying trajectory-level operations (e.g. " + "--truncate-task-notifications) but before pair splitting. " + "Each line preserves the original record structure with the " + "messages field updated.", + ) + parser.add_argument( + "--dump-samples", + default=None, + metavar="DIR", + help="Save sampled pairs to DIR, one file per pair. Each file " + "contains the rendered text and a token-by-token table with " + "token id, decoded text, and loss_mask.", + ) + parser.add_argument( + "--dump-n", + type=int, + default=None, + help="Number of pairs to dump when --dump-samples is set. " + "Default: all pairs. -1 also means all.", + ) + args = parser.parse_args() + + filter_errors = not args.no_filter_errors + strip_all_thinking = args.strip_all_thinking + filter_empty_tool_calls = args.filter_empty_tool_calls + filter_bare_text_tool_calls = args.filter_bare_text_tool_calls + truncate_task_notifications = args.truncate_task_notifications + max_no_thinking_ratio = args.max_no_thinking_ratio + + # --- Fast path: save preprocessed trajectories --- + if args.save_trajectories: + records_in = 0 + records_out = 0 + n_truncated = 0 + with ( + open(args.path, encoding="utf-8") as fin, + open(args.save_trajectories, "w", encoding="utf-8") as fout, + ): + for line in fin: + line = line.strip() + if not line: + continue + record = json.loads(line) + records_in += 1 + + messages, _ = _extract_messages(record, records_in) + + if truncate_task_notifications and messages: + truncated = _truncate_at_task_notification(messages) + if len(truncated) < len(messages): + n_truncated += 1 + _set_messages(record, truncated) + + fout.write(json.dumps(record, ensure_ascii=False) + "\n") + records_out += 1 + + parts = [] + if n_truncated: + parts.append(f"{n_truncated} truncated at task-notification") + op_msg = ", ".join(parts) if parts else "no changes" + print( + f"Saved {records_out}/{records_in} trajectories " + f"to {args.save_trajectories} ({op_msg})" + ) + sys.exit(0) + + # --- Load --- + split_mode = args.split_mode + error_indices_list = None + + if split_mode == "trajectory": + samples, error_indices_list, tools_list = _load_full_trajectories( + args.path, + filter_errors=filter_errors, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + truncate_task_notifications=truncate_task_notifications, + random_strip_thinking_prob=args.random_strip_thinking_prob, + random_strip_thinking_seed=args.random_strip_thinking_seed, + n_thinking_variants=args.n_thinking_variants, + ) + label = "trajectories" + elif args.pre_split: + samples, tools_list = _load_presplit_pairs( + args.path, + strip_all_thinking=strip_all_thinking, + random_strip_thinking_prob=args.random_strip_thinking_prob, + random_strip_thinking_seed=args.random_strip_thinking_seed, + n_thinking_variants=args.n_thinking_variants, + ) + label = "pairs" + else: + samples, tools_list = _load_trajectory_pairs( + args.path, + filter_errors=filter_errors, + strip_all_thinking=strip_all_thinking, + filter_empty_tool_calls=filter_empty_tool_calls, + filter_bare_text_tool_calls=filter_bare_text_tool_calls, + truncate_task_notifications=truncate_task_notifications, + max_no_thinking_ratio=max_no_thinking_ratio, + random_strip_thinking_prob=args.random_strip_thinking_prob, + random_strip_thinking_seed=args.random_strip_thinking_seed, + n_thinking_variants=args.n_thinking_variants, + ) + label = "pairs" + + # --- Slice + stats --- + total = len(samples) + if args.num_samples is not None: + samples = samples[: args.num_samples] + tools_list = tools_list[: args.num_samples] if tools_list else tools_list + if error_indices_list is not None: + error_indices_list = error_indices_list[: args.num_samples] + + print(f"Total {label}: {total}") + if args.num_samples is not None: + print(f"Using: {len(samples)}") + + if samples: + lengths = [len(s) for s in samples] + print( + f"Messages/sample: min={min(lengths)}, " + f"max={max(lengths)}, avg={sum(lengths) / len(lengths):.1f}" + ) + if error_indices_list is not None: + n_masked = sum(len(e) for e in error_indices_list) + print(f"Masked segments: {n_masked} (loss=0)") + + # --- Save cleaned samples as JSONL --- + if args.save_pairs: + with open(args.save_pairs, "w", encoding="utf-8") as fout: + err_iter = error_indices_list or [None] * len(samples) + tl_iter = tools_list if tools_list else [None] * len(samples) + for sample, sample_tools, err_idxs in zip(samples, tl_iter, err_iter): + record = {"messages": sample} + if sample_tools is not None: + record["tools"] = sample_tools + if err_idxs: + record["error_indices"] = err_idxs + fout.write(json.dumps(record, ensure_ascii=False) + "\n") + print(f"Wrote {len(samples)} {label} to {args.save_pairs}") + + # --- Tokenize / Dump --- + dump_dir = args.dump_samples if args.dump_samples else None + need_tokenize = args.save_tokenized + + # When --save-tokenized is set, auto-dump 50 samples alongside it + # unless the user explicitly set --dump-samples or --dump-n 0. + if need_tokenize and not dump_dir and args.dump_n != 0: + dump_dir = os.path.join(args.save_tokenized, "dumped_samples") + + if not need_tokenize and not dump_dir: + sys.exit(0) + + tok = AutoTokenizer.from_pretrained(args.tokenizer, trust_remote_code=True) + _patch_chat_template_for_training(tok) + if args.dump_n is not None: + dump_n = args.dump_n + elif args.dump_samples: + # Explicit --dump-samples without --dump-n: dump all + dump_n = -1 if args.num_samples is None else args.num_samples + elif need_tokenize: + # Auto-dump with --save-tokenized: default 50 + dump_n = 50 + else: + dump_n = -1 + + # Dump can run independently without full tokenization. + if dump_dir and dump_n != 0: + dump_tools = None if args.no_tools else tools_list + first_tools = None + if dump_tools: + first_tools = next((t for t in dump_tools if t is not None), None) + assistant_pattern = _detect_template_pattern(tok, tools=first_tools) + _dump_samples( + samples, + tok, + assistant_pattern, + dump_tools, + dump_dir, + dump_n, + split_mode=split_mode, + error_indices_list=error_indices_list, + parse_tool_call_args=args.parse_tool_call_args, + ) + + if not need_tokenize: + sys.exit(0) + + ds = _tokenize_samples( + samples, + tools_list, + tok, + split_mode=split_mode, + error_indices_list=error_indices_list, + max_length=args.max_length, + num_proc=args.num_proc, + no_tools=args.no_tools, + parse_tool_call_args=args.parse_tool_call_args, + ) + + print(f"\nTokenized: {len(ds)} samples") + if args.save_tokenized: + ds.save_to_disk(args.save_tokenized) + print(f"Saved tokenized dataset ({len(ds)} samples) to {args.save_tokenized}") diff --git a/areal/utils/logging.py b/areal/utils/logging.py index d0aa166378..a46e114375 100644 --- a/areal/utils/logging.py +++ b/areal/utils/logging.py @@ -111,6 +111,8 @@ "ToolCallParser": "light_purple", "TokenLogpReward": "light_purple", "ProxyUtils": "light_purple", + "AReaL-SWEAgent": "light_purple", + "SWETrain": "light_green", # Agent Service - purple "AgentGateway": "light_purple", "AgentBridge": "light_purple", diff --git a/examples/swe/README.md b/examples/swe/README.md new file mode 100644 index 0000000000..ca3985ec40 --- /dev/null +++ b/examples/swe/README.md @@ -0,0 +1,190 @@ +# SWE-bench RL training with AReaL-SWEAgent + +This example runs SWE-bench coding-agent RL (GRPO) in AReaL. The actual agent loop, +sandboxing and reward computation live in a **separate repository**, +[AReaL-SWEAgent](https://github.com/areal-project/AReaL-SWEAgent): for each SWE-bench +instance it edits code inside an isolated sandbox, grades the patch, and returns a +reward for the RL loop. AReaL serves the policy being trained and drives rollouts +through its OpenAI-compatible proxy. + +``` +AReaL (this repo) AReaL-SWEAgent (separate checkout) + PPOTrainer / rollout proxy ──▶ run_agent_with_reward() ──▶ sandbox + grade + ▲ │ + └──────────────── reward, stats ◀──────────────────────────────┘ +``` + +## 1. Get AReaL-SWEAgent + +Clone it next to AReaL (the default lookup path is `../AReaL-SWEAgent`) and install its +dependencies into the same environment AReaL workers run in: + +```bash +git clone https://github.com/areal-project/AReaL-SWEAgent.git +cd AReaL-SWEAgent +pip install -r requirements.txt +``` + +AReaL imports it as the Python package `aweagent`; the repository directory name +(`AReaL-SWEAgent`) and the package name (`aweagent`) intentionally differ. + +## 2. Point AReaL at the checkout + +AReaL discovers the checkout in this order (first match wins): + +1. `econfig.agent_root` in your YAML (legacy alias: `econfig.swe_agent_root`). +1. `AWEAGENT_ROOT` / `SWE_AGENT_ROOT` environment variable. +1. `../AReaL-SWEAgent` relative to the AReaL repo root. + +In a multi-node / containerized setup the checkout must be importable on every worker, +so put it on shared storage and set both the env var and `PYTHONPATH`, e.g. in the +worker `scheduling_spec.env_vars`: + +```yaml +env_vars: + AWEAGENT_ROOT: "/path/to/AReaL-SWEAgent" + SWE_AGENT_ROOT: "/path/to/AReaL-SWEAgent" + PYTHONPATH: "/path/to/AReaL-SWEAgent:/path/to/AReaL" + # URL of the AEnvironment sandbox service used by AReaL-SWEAgent. + AENV_SYSTEM_URL: "http://your-aenv-service:8080" +``` + +## 3. Configure the agent (econfig) + +The SWE example reads an `econfig` block (see `examples/swe/utils.py`): + +| Field | Meaning | +| ----------------------- | ----------------------------------------------------------------------------------------------------------- | +| `agent_type` | Agent to run: `swe` (built-in tool-use) or `cc` (Claude Code). | +| `agent_config` | Generic config name under `AReaL-SWEAgent/aweagent/configs/`. Overrides the per-type fields below when set. | +| `swe_agent_config` | Config used when `agent_type=swe` (default `1_0_0/min-swe-agent-train-top1`). | +| `cc_agent_config` | Config used when `agent_type=cc`. | +| `agent_root` | Path to the AReaL-SWEAgent checkout (see section 2). | +| `step_limit` | Max agent interaction steps per episode. | +| `max_completion_tokens` | Max completion tokens per agent LLM call. | +| `timeout` | Max wall-clock time per episode (seconds). | + +## 4. Dataset format + +`train_swe_rl.py` loads a JSONL file (`train_dataset.path`) where each line is a +SWE-bench instance with at least: + +- `instance_id` — e.g. `django__django-10097` +- `problem_statement` — the GitHub issue text +- `eval_script` — shell script used by AReaL-SWEAgent to grade the patch + +## 5. Run + +Edit the placeholder paths in `qwen3_30b_a3b_grpo.yaml` (model, dataset, +`AWEAGENT_ROOT`, caches, sandbox/W&B endpoints), then launch: + +```bash +python -m examples.swe.train_swe_rl --config examples/swe/qwen3_30b_a3b_grpo.yaml +``` + +With `scheduler.type=slurm`, AReaL launches the rollout / actor / proxy workers; each +rollout calls into AReaL-SWEAgent, which runs the agent in a sandbox and returns the +reward. + +## 6. Set up the AEnvironment backend + +`AENV_SYSTEM_URL` (section 2) must point at a running +[AEnvironment](https://github.com/inclusionAI/AEnvironment) platform. AReaL never talks +to a sandbox directly — AReaL-SWEAgent asks the AEnvironment API service to spin up two +kinds of environment per rollout: + +| Role | Environment AReaL-SWEAgent requests | Ships in the AEnvironment repo? | +| ----------------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------- | +| **Task sandbox** — where the agent runs shell commands and edits code | `persistent-bash-env` (swe agent) / `cc-bash-env` (cc agent) | No — provided by your AEnvironment deployment | +| **Grading sandbox** — where the patch is scored resolved / not-resolved | `swebench` (overridable via `SWEBENCH_EVAL_ENV`) | Yes — `aenv/builtin-envs/swebench` | + +### 6.1 Deploy the platform + +AEnvironment deploys on Kubernetes via Helm (see its +[deployment guide](https://github.com/inclusionAI/AEnvironment/blob/main/docs/getting_started/deployment.md)): + +```bash +git clone https://github.com/inclusionAI/AEnvironment.git +cd AEnvironment +helm install aenv-platform ./deploy \ + --namespace aenv --create-namespace --wait --timeout 10m +``` + +The API service is then reachable in-cluster at port `8080` — this is the value for +`AENV_SYSTEM_URL`: + +``` +AENV_SYSTEM_URL = http://api-service.aenv.svc.cluster.local:8080 +``` + +### 6.2 Register the `swebench` grading environment (guaranteed example) + +The grading environment is the reward source and is the one piece shipped in the +AEnvironment repo, so it always works out of the box. Install the CLI, point it at your +deployment, then build and publish the bundled `swebench` environment: + +```bash +pip install aenvironment +aenv config init +aenv config set hub_backend http://envhub.aenv.svc.cluster.local:8083/ +export AENV_SYSTEM_URL=http://api-service.aenv.svc.cluster.local:8080 + +cd AEnvironment/aenv/builtin-envs/swebench # config.json: name=swebench, version=1.0.0 +aenv build --push # requires a configured image registry +aenv push # publish metadata to EnvHub +aenv list # `swebench` should now appear +``` + +Its reward entrypoint is `swebench_reward(instance, model_patch, timeout)`, which is +exactly the `call_reward({"instance", "model_patch", "timeout"})` contract +AReaL-SWEAgent uses to grade patches. + +> **Version note:** the repo ships `swebench@1.0.0`, while AReaL-SWEAgent defaults to +> `swebench@1.0.4`. Either export `SWEBENCH_EVAL_ENV=swebench@1.0.0` in the worker +> `env_vars`, or bump the `version` in `config.json` before `aenv build`. + +### 6.3 Provide the task sandbox + +The `persistent-bash-env` / `cc-bash-env` task sandboxes are **not** shipped in the +AEnvironment repo. Use the image already registered in your AEnvironment cluster, or +build your own environment that exposes a shell (the repo's +`aenv/builtin-envs/mini-terminal` is the closest reference) and register it the same way +as section 6.2. AReaL-SWEAgent selects it through the agent config's `aenv_version` +field. + +## 7. Train the Claude Code (cc) agent + +Section 5 runs the built-in `swe` agent. To train the **Claude Code** agent instead — +Claude Code solves the task in a `cc-bash-env` sandbox and the same `swebench` +environment grades the resulting patch — switch `econfig.agent_type` to `cc` and select +a cc config: + +```yaml +econfig: + agent_type: cc + cc_agent_config: train_cc_time3600 # a config under AReaL-SWEAgent/aweagent/configs/ + agent_root: /path/to/AReaL-SWEAgent + timeout: 3600.0 +``` + +The cc agent's LLM calls are still routed through AReaL's OpenAI-compatible proxy (AReaL +injects the per-rollout `base_url`/`api_key`), so training stays on-policy exactly like +the swe path. + +### 7.1 Environment variables for cc + +In addition to the section 2 variables (`AWEAGENT_ROOT`, `PYTHONPATH`, +`AENV_SYSTEM_URL`), the cc agent reads these from the worker `env_vars` — they are +consumed by the cc config in AReaL-SWEAgent (`aweagent/configs/1_0_0/cc.yaml`), not by +AReaL itself: + +| Variable | Purpose | +| ---------------------------------------- | ---------------------------------------------------------- | +| `SWEBENCH_EVAL_ENV` | Grading environment name, e.g. `swebench@1.0.0` (see 6.2). | +| `REMOTE_PROXY_SERVICE_URL` | Gateway that fronts the Claude CLI traffic (proxy mode). | +| `REMOTE_PROXY_API_KEY` | API key for that proxy gateway. | +| `ANTHROPIC_BASE_URL` / `ANTHROPIC_MODEL` | Used instead when running the CLI in direct mode. | + +The concrete agent behaviour — system prompt, `cli_flags`, tool allow-list, +thinking-token budgets — lives entirely in the AReaL-SWEAgent cc config; see that +repository for the available `cc_agent_config` values and their fields. diff --git a/examples/swe/agent.py b/examples/swe/agent.py new file mode 100644 index 0000000000..40b0b8f2aa --- /dev/null +++ b/examples/swe/agent.py @@ -0,0 +1,260 @@ +"""AReaL-SWEAgent workflow for AReaL proxy mode. + +This module runs AReaL-SWEAgent-backed SWE-bench agents through AReaL's proxy +server during RL training. AReaL-SWEAgent stays in a separate checkout and is +discovered through ``AWEAGENT_ROOT`` or ``SWE_AGENT_ROOT``. AReaL passes the +proxy ``base_url`` and per-rollout ``api_key`` explicitly as kwargs instead +of mutating process-wide ``os.environ`` - the latter races across concurrent +rollouts and routes requests to the wrong proxy session. +""" + +import asyncio +import os +import sys +import time +from typing import Any + +from areal.utils import logging + +logger = logging.getLogger("AReaL-SWEAgent") + +_DEFAULT_AGENT_CONFIGS = { + "swe": "1_0_0/min-swe-agent-train-top1", + "cc": "train_cc_time3600", + "oh": "eval_oh", + "opencode": "eval_opencode", + "codex": "eval_codex", +} + + +def _default_aweagent_root() -> str: + """Return the default sibling checkout path for AReaL-SWEAgent.""" + areal_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + return os.path.join(os.path.dirname(areal_root), "AReaL-SWEAgent") + + +def _ensure_aweagent_importable(aweagent_root: str = "") -> str: + """Add the AReaL-SWEAgent checkout root to ``sys.path``. + + Resolution order: + 1. Explicit ``aweagent_root`` argument (``econfig.agent_root`` / + ``econfig.aweagent_root`` / ``econfig.swe_agent_root``). + 2. ``AWEAGENT_ROOT`` / ``SWE_AGENT_ROOT`` / ``SWEAGENT_ROOT`` env vars. + 3. ``../AReaL-SWEAgent`` relative to the AReaL repository root. + + Returns the resolved AReaL-SWEAgent checkout root. + """ + if aweagent_root: + root = os.path.abspath(aweagent_root) + elif ( + os.getenv("AWEAGENT_ROOT") + or os.getenv("SWE_AGENT_ROOT") + or os.getenv("SWEAGENT_ROOT") + ): + root = os.path.abspath( + os.getenv("AWEAGENT_ROOT") + or os.getenv("SWE_AGENT_ROOT") + or os.getenv("SWEAGENT_ROOT") + ) + else: + root = _default_aweagent_root() + + if os.path.isdir(root) and root not in sys.path: + sys.path.insert(0, root) + logger.info(f"Added AReaL-SWEAgent root to sys.path: {root}") + elif not os.path.isdir(root): + logger.warning( + f"AReaL-SWEAgent root does not exist: {root}. " + "Set AWEAGENT_ROOT (or SWE_AGENT_ROOT) env var or econfig.agent_root " + "(or the legacy econfig.swe_agent_root) to the correct path." + ) + return root + + +def _configured_aweagent_root(econfig: dict[str, Any]) -> str: + return ( + econfig.get("agent_root") + or econfig.get("aweagent_root") + or econfig.get("swe_agent_root") + or "" + ) + + +def _configured_agent_type(econfig: dict[str, Any]) -> str: + agent_type = str(econfig.get("agent_type") or "swe").strip().lower() + if not agent_type: + raise ValueError("econfig.agent_type must not be empty") + return agent_type + + +def _configured_agent_config(econfig: dict[str, Any], agent_type: str) -> str: + return ( + econfig.get("agent_config") + or econfig.get(f"{agent_type}_agent_config") + or econfig.get("swe_agent_config") + or _DEFAULT_AGENT_CONFIGS.get(agent_type) + or agent_type + ) + + +class SWEAgentWorkflow: + """AReaL-SWEAgent workflow for AReaL proxy mode. + + This workflow runs an AReaL-SWEAgent agent type (``swe``, ``cc``, ``oh``, + ``opencode``, or ``codex``) on SWE-bench style records in sandboxed + environments. The agent's LLM calls are routed through AReaL's proxy + server for on-policy RL training. + + The workflow delegates to AReaL-SWEAgent's ``run_agent_with_reward``, passing + the AReaL proxy URL + per-rollout API key as explicit kwargs so that the + current rollout's endpoint/key are used without process-wide env races. + + Args: + econfig: Environment configuration dict. Key fields include + ``agent_type``, ``agent_config``, ``swe_agent_config``, + ``cc_agent_config``, ``agent_root`` / ``swe_agent_root``, + ``llm_model``, ``opencode_provider``, ``codex_provider``, + and ``timeout``. + gen_args: Generation arguments (unused; token limits come from + the AReaL-SWEAgent config YAML). + timeout: Maximum time allowed for a single episode (default: 1800s). + """ + + def __init__( + self, + econfig: dict | None = None, + gen_args: dict | None = None, + timeout: float = 1800.0, + ): + if econfig is None: + econfig = {} + self.econfig = econfig + self.gen_args = gen_args or {} + self.timeout = econfig.get("timeout", timeout) + + # Ensure AReaL-SWEAgent is importable at construction time so we get an early + # warning if the path is wrong, rather than failing mid-training. + _ensure_aweagent_importable(_configured_aweagent_root(econfig)) + + async def run( + self, data: dict[str, Any], **extra_kwargs: Any + ) -> dict[str, float] | float: + """Run one SWE-bench style agent episode. + + Args: + data: Input data containing SWE-bench instance fields: + - instance_id (str): SWE-bench instance ID + - problem_statement (str): The GitHub issue description + - eval_script (str): Shell script for reward evaluation + **extra_kwargs: Additional kwargs injected by AReaL proxy infra: + - base_url (str): Proxy server URL for the agent LLM. + - api_key (str): Per-rollout API key for the proxy session. + + Returns: + float: The reward from the episode (0.0 or 1.0). + """ + base_url: str | None = extra_kwargs.get("base_url", None) + if base_url is None: + raise ValueError("base_url is required for SWEAgentWorkflow") + + api_key: str = extra_kwargs.get("api_key", os.getenv("OPENAI_API_KEY", "dummy")) + + econfig = self.econfig.copy() + if "econfig" in data: + econfig.update(data["econfig"]) + + agent_type = _configured_agent_type(econfig) + config_name = _configured_agent_config(econfig, agent_type) + llm_model = econfig.get("llm_model") or os.getenv("LLM_MODEL") or None + opencode_provider = ( + econfig.get("opencode_provider") or os.getenv("OPENCODE_PROVIDER") or None + ) + codex_provider = ( + econfig.get("codex_provider") or os.getenv("CODEX_PROVIDER") or None + ) + instance_id = data.get("instance_id", "unknown") + + logger.info( + f"Starting {agent_type} episode: " + f"instance_id={instance_id}, config={config_name}" + ) + start_time = time.time() + + try: + reward = await asyncio.wait_for( + self._run_episode( + data=data, + agent_type=agent_type, + config_name=config_name, + base_url=base_url, + api_key=api_key, + llm_model=llm_model, + opencode_provider=opencode_provider, + codex_provider=codex_provider, + ), + timeout=self.timeout, + ) + except TimeoutError: + elapsed = time.time() - start_time + logger.error( + f"TIMEOUT: Instance {instance_id} exceeded {self.timeout}s " + f"(elapsed: {elapsed:.1f}s). Discarding trajectory." + ) + raise + + elapsed = time.time() - start_time + logger.info( + f"Finished {agent_type} episode: instance_id={instance_id}, " + f"reward={reward}, elapsed={elapsed:.1f}s" + ) + return float(reward) + + async def _run_episode( + self, + data: dict[str, Any], + agent_type: str, + config_name: str, + base_url: str, + api_key: str, + llm_model: str | None, + opencode_provider: str | None, + codex_provider: str | None, + ) -> float: + """Execute one episode through AReaL-SWEAgent and return the reward. + + Delegates to ``aweagent.lifecycle.run_agent_with_reward``, which + handles env creation, the agent loop, reward computation, and trace + persistence. ``base_url`` / ``api_key`` are forwarded as explicit + kwargs (NOT via ``os.environ``) so concurrent rollouts can't read a + foreign session's key. + """ + # Imported lazily - AReaL-SWEAgent is only added to sys.path at run time. + from aweagent.lifecycle import ( + run_agent_with_reward, # type: ignore[import-not-found] + ) + + result_dir = os.getenv("LOG_DIR", "./logs") + try: + reward, _ = await run_agent_with_reward( + data, + agent_type=agent_type, + agent_config=config_name, + result_dir=result_dir, + override_api_key=api_key, + override_base_url=base_url, + override_llm_model=llm_model, + override_opencode_provider=opencode_provider, + override_codex_provider=codex_provider, + ) + except Exception as e: + import traceback + + instance_id = data.get("instance_id", "unknown") + logger.error( + f"[{instance_id}] Episode error: {e}\n{traceback.format_exc()}" + ) + return 0.0 + + return float(reward) diff --git a/examples/swe/filter_function.py b/examples/swe/filter_function.py new file mode 100644 index 0000000000..be397693a0 --- /dev/null +++ b/examples/swe/filter_function.py @@ -0,0 +1,26 @@ +from areal.utils import stats_tracker + + +def filter_function(sample): + # Use original_rewards if available for filtering and statistics + rewards = ( + sample["original_rewards"] + if "original_rewards" in sample + else sample["rewards"] + ) + reward0 = rewards[0] + accept = any(r != reward0 for r in rewards) + + # Track rejection statistics + if not accept: + stats_tracker.get("rollout").scalar(rejected_by_failed_or_perfect=1) + # Distinguish all-correct vs all-wrong + all_positive = all(r > 0 for r in rewards) + stats_tracker.get("rollout").scalar(rejected_by_all_correct=int(all_positive)) + stats_tracker.get("rollout").scalar(rejected_by_all_wrong=int(not all_positive)) + else: + stats_tracker.get("rollout").scalar(rejected_by_failed_or_perfect=0) + stats_tracker.get("rollout").scalar(rejected_by_all_correct=0) + stats_tracker.get("rollout").scalar(rejected_by_all_wrong=0) + + return accept diff --git a/examples/swe/prefix_matchers.py b/examples/swe/prefix_matchers.py new file mode 100644 index 0000000000..64b52bdf79 --- /dev/null +++ b/examples/swe/prefix_matchers.py @@ -0,0 +1,58 @@ +"""Custom prefix matchers for InteractionCache parent-child matching. + +The default cache matcher requires exact message equality. SWE-bench agents, +especially Claude Code style agents routed through Anthropic-compatible APIs, +can rewrite tool arguments or tool output formatting between turns while still +representing the same conversation. The matcher here keeps concat export stable +for those known rewrites. +""" + +from __future__ import annotations + + +def _tool_call_ids(tool_calls: list[dict]) -> list[str]: + """Extract ordered tool_call IDs from a tool_calls list.""" + return [tc.get("id", "") for tc in tool_calls if isinstance(tc, dict)] + + +def _messages_match(a: dict, b: dict) -> bool: + """Check whether two message dicts are semantically equivalent.""" + if a.get("role") != b.get("role"): + return False + + role = a.get("role") + + if role == "tool": + return a.get("tool_call_id") == b.get("tool_call_id") + + if a.get("content", "") != b.get("content", ""): + return False + + if a.get("thinking") != b.get("thinking"): + return False + + a_tc = a.get("tool_calls") + b_tc = b.get("tool_calls") + if a_tc is not None or b_tc is not None: + if a_tc is None or b_tc is None: + return False + if not isinstance(a_tc, list) or not isinstance(b_tc, list): + return a_tc == b_tc + if len(a_tc) != len(b_tc): + return False + if _tool_call_ids(a_tc) != _tool_call_ids(b_tc): + return False + + return True + + +def swe_prefix_matcher(a: list[dict], b: list[dict]) -> bool: + """Return True if ``a`` is a semantic prefix of ``b``.""" + if len(a) > len(b): + return False + for am, bm in zip(a, b): + if am == bm: + continue + if not _messages_match(am, bm): + return False + return True diff --git a/examples/swe/preprocessors.py b/examples/swe/preprocessors.py new file mode 100644 index 0000000000..8fff09ccee --- /dev/null +++ b/examples/swe/preprocessors.py @@ -0,0 +1,124 @@ +"""Message preprocessors for SWE-bench agent rollouts. + +Preprocessors transform messages after Anthropic-to-OpenAI translation and +before they reach AReaL's OpenAI client. They remove volatile Claude Code +metadata that otherwise breaks prefix-based parent matching in concat export. +""" + +import json +import re +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class MessagePreprocessor(Protocol): + """Protocol for message preprocessors.""" + + def __call__(self, messages: list[dict]) -> list[dict]: ... + + +class StripAnthropicBillingHeader: + """Remove per-request Anthropic billing header lines from system prompts.""" + + _PATTERN = re.compile(r"^x-anthropic-billing-header:[^\n]*\n?", re.MULTILINE) + + def __call__(self, messages: list[dict]) -> list[dict]: + if not messages: + return messages + first = messages[0] + if first.get("role") == "system" and isinstance(first.get("content"), str): + first["content"] = self._PATTERN.sub("", first["content"]) + return messages + + +class NormalizeSystemReminder: + """Remove volatile ``currentDate`` lines from system reminders.""" + + _PATTERN = re.compile( + r"# currentDate\nToday's date is \d{4}-\d{2}-\d{2}\.\n?", + ) + + def __call__(self, messages: list[dict]) -> list[dict]: + for msg in messages: + content = msg.get("content") + if isinstance(content, str) and "currentDate" in content: + msg["content"] = self._PATTERN.sub("", content) + return messages + + +class StripAllSystemReminders: + """Remove all ``...`` blocks.""" + + _PATTERN = re.compile(r"[\s\S]*?") + + def __call__(self, messages: list[dict]) -> list[dict]: + for msg in messages: + content = msg.get("content") + if isinstance(content, str) and "" in content: + msg["content"] = self._PATTERN.sub("", content) + return messages + + +class StripAnthropicCacheFields: + """Strip Anthropic-specific fields that are not preserved in stored output.""" + + def __call__(self, messages: list[dict]) -> list[dict]: + for msg in messages: + msg.pop("cache_control", None) + msg.pop("thinking_blocks", None) + if "tool_calls" in msg: + for tc in msg["tool_calls"]: + if isinstance(tc, dict): + tc.pop("cache_control", None) + tc.pop("provider_specific_fields", None) + fn = tc.get("function") + if isinstance(fn, dict): + fn.pop("cache_control", None) + return messages + + +class NormalizeToolCallArguments: + """Normalize tool_call arguments for deterministic comparison.""" + + _TOOL_ARG_DEFAULTS: dict[tuple[str, str], object] = { + ("Edit", "replace_all"): False, + } + + @classmethod + def _normalize_tool_arguments( + cls, + tool_name: str | None, + args_dict: dict, + ) -> dict: + if tool_name is not None: + for (tn, field), default in cls._TOOL_ARG_DEFAULTS.items(): + if tn == tool_name and args_dict.get(field) == default: + args_dict.pop(field, None) + return args_dict + + def __call__(self, messages: list[dict]) -> list[dict]: + for msg in messages: + if "tool_calls" in msg: + for tc in msg["tool_calls"]: + if isinstance(tc, dict): + fn = tc.get("function") + if isinstance(fn, dict): + tool_name = fn.get("name") + args = fn.get("arguments") + if isinstance(args, str): + try: + parsed = json.loads(args) + parsed = self._normalize_tool_arguments( + tool_name, + parsed, + ) + fn["arguments"] = json.dumps( + parsed, + sort_keys=True, + ensure_ascii=False, + ) + except (json.JSONDecodeError, TypeError): + pass + if msg.get("role") == "assistant" and "content" not in msg: + msg["content"] = "" + return messages diff --git a/examples/swe/qwen3_30b_a3b_grpo.yaml b/examples/swe/qwen3_30b_a3b_grpo.yaml new file mode 100644 index 0000000000..a9fc7c3c34 --- /dev/null +++ b/examples/swe/qwen3_30b_a3b_grpo.yaml @@ -0,0 +1,291 @@ +experiment_name: qwen3_30b_a3b_grpo +trial_name: trial0 + +seed: 42 +enable_offload: true +total_train_epochs: 10 +total_train_steps: 100 +tokenizer_path: ${actor.path} + +# Clean up external SWE sandbox instances after the run (LOG_DIR is injected). +post_exit_hook: 'AWE_ROOT=\${AWEAGENT_ROOT:-\${SWE_AGENT_ROOT:-/path/to/AReaL-SWEAgent}}; cd "$AWE_ROOT" && PYTHONPATH=$PWD:$PYTHONPATH pip install -q -r requirements.txt && AENV_SYSTEM_URL=\${AENV_SYSTEM_URL} python -m aweagent.maintenance.clean_instances' + +cluster: + # 4 nodes x 8 GPUs: 2 for rollout (SGLang), 2 for actor (Megatron). + n_nodes: 4 + n_gpus_per_node: 8 + fileroot: /tmp/areal/experiments + name_resolve: + type: nfs + nfs_record_root: /tmp/areal/name_resolve + +scheduler: + type: slurm + +gconfig: + n_samples: 4 + min_new_tokens: 0 + max_new_tokens: 131071 + max_tokens: 131071 + greedy: false + temperature: 1.0 + top_p: 1.0 + top_k: 1000000 + +rollout: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + backend: "sglang:d4t8p1" + max_concurrent_rollouts: 32 + queue_size: null + consumer_batch_size: ${train_dataset.batch_size} + max_head_offpolicyness: 4 + enable_rollout_tracing: true + scheduling_spec: ${actor.scheduling_spec} + fileroot: ${cluster.fileroot} + tokenizer_path: ${tokenizer_path} + dump_to_file: true + setup_timeout: 900.0 + request_timeout: 20000.0 + request_retries: 3 + pause_grace_period: 30 + agent: + mode: inline + # admin_api_key is injected by the launcher; not stored in YAML. + admin_api_key: ${oc.env:SWE_RL_ADMIN_API_KEY} + tool_call_parser: qwen3_coder + reasoning_parser: qwen3 + chat_template_type: concat + engine_max_tokens: ${gconfig.max_tokens} + export_style: concat + turn_discount: 1.0 + session_timeout_seconds: 3600 + prefix_matcher: examples.swe.prefix_matchers.swe_prefix_matcher + message_preprocessors: + - examples.swe.preprocessors.StripAnthropicBillingHeader + - examples.swe.preprocessors.StripAllSystemReminders + - examples.swe.preprocessors.StripAnthropicCacheFields + - examples.swe.preprocessors.NormalizeToolCallArguments + +actor: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + # attn DP4 PP1 TP4 CP2; ffn DP4 PP1 EP8. CP=2 shards long sequences. + backend: "megatron:(attn:d4p1t4c2|ffn:d4p1e8)" + path: /path/to/Qwen3-Coder-30B-A3B-Instruct + init_from_scratch: false + disable_dropout: true + gradient_checkpointing: true + dtype: bfloat16 + grad_reduce_dtype: float32 + mb_spec: + n_mbs: 1 + granularity: 1 + max_tokens_per_mb: 131072 + n_mbs_divisor: 1 + optimizer: + type: adam + lr: 3.0e-6 + weight_decay: 0.01 + beta1: 0.9 + beta2: 0.999 + eps: 1.0e-8 + lr_scheduler_type: constant + gradient_clipping: 1.0 + warmup_steps_proportion: 0.001 + megatron: + wrap_with_ddp: true + ddp: + grad_reduce_in_fp32: true + overlap_grad_reduce: true + overlap_param_gather: false + use_distributed_optimizer: true + use_deterministic_algorithms: true + enable_fp32_lm_head: false + cross_entropy_loss_fusion: false + recompute_granularity: full + recompute_method: uniform + recompute_num_layers: 1 + moe_router_dtype: fp32 + moe_token_dispatcher_type: alltoall + moe_router_bias_update_rate: 0.0 + eps_clip: 0.2 + eps_clip_higher: 0.28 + temperature: ${gconfig.temperature} + kl_ctl: 0.0 + ppo_n_minibatches: 1 + recompute_logprob: false + prox_logp_method: recompute + use_decoupled_loss: true + rejection_sampling: + level: token + action: mask + metric: ratio + upper: 5.0 + discount: 1.0 + gae_lambda: 1.0 + reward_scaling: 1.0 + reward_bias: 0.0 + reward_clip: 5.0 + reward_norm: + mean_level: group + std_level: group + group_size: ${gconfig.n_samples} + eps: 1.0e-5 + adv_norm: + mean_level: null + std_level: null + max_new_tokens: ${gconfig.max_new_tokens} + scheduling_spec: + - task_type: worker + port_count: 2 + gpu: 1 + cpu: 4 + mem: 32 + exclusive: true + reservation: null + nodelist: null + exclude: null + image: /path/to/areal.sif + cmd: python3 -m areal.infra.rpc.rpc_server + env_vars: + NCCL_TIMEOUT: 600 + PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:True" + # Point HOME and caches at shared storage to avoid filling the + # container's small default HOME during JIT compilation. + HOME: "/path/to/cache/home" + HF_HOME: "/path/to/cache/hf" + XDG_CACHE_HOME: "/path/to/cache/xdg" + # Workers must import both AReaL and the external AReaL-SWEAgent. + PYTHONPATH: "/path/to/AReaL-SWEAgent:/path/to/AReaL" + AENV_SYSTEM_URL: "http://your-aenv-service:8080" + SWE_AGENT_ROOT: "/path/to/AReaL-SWEAgent" + AWEAGENT_ROOT: "/path/to/AReaL-SWEAgent" + OPENAI_MODEL: "Qwen3-Coder-30B-A3B-Instruct" + additional_bash_cmds: + # Install AReaL-SWEAgent's aenv/fastmcp into the venv the workers use. + - "/opt/.venv/bin/python -m ensurepip --upgrade >/dev/null 2>&1 || true" + - "/opt/.venv/bin/python -m pip install --no-deps aenvironment==0.1.8rc2 || echo 'WARN: aenvironment install failed'" + - "/opt/.venv/bin/python -m pip install 'fastmcp<3' || echo 'WARN: fastmcp install failed'" + - "/opt/.venv/bin/python -c 'import aenv, aweagent' || echo 'WARN: aenv/aweagent import failed'" + +ref: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + backend: ${actor.backend} + path: ${actor.path} + init_from_scratch: false + disable_dropout: false + dtype: ${actor.dtype} + mb_spec: + n_mbs: 1 + granularity: 1 + max_tokens_per_mb: 131072 + n_mbs_divisor: 1 + optimizer: null + scheduling_strategy: + type: colocation + target: actor + scheduling_spec: ${actor.scheduling_spec} + +# SWE-bench agent environment. Set agent_type=cc to train Claude Code through +# the same proxy path. agent_root points at the external AReaL-SWEAgent checkout. +econfig: + dataset_path: "" + agent_type: swe + agent_config: "" + swe_agent_config: 1_0_0/min-swe-agent-train-top1 + cc_agent_config: train_cc_time3600 + agent_root: /path/to/AReaL-SWEAgent + swe_agent_root: /path/to/AReaL-SWEAgent + llm_model: "" + opencode_provider: "" + codex_provider: "" + max_completion_tokens: 131071 + timeout: 3600.0 + +sglang: + model_path: ${actor.path} + random_seed: ${seed} + skip_tokenizer_init: true + dtype: ${actor.dtype} + context_length: 131072 + max_prefill_tokens: 131071 + max_running_requests: 16 + mem_fraction_static: 0.85 + chunked_prefill_size: 8192 + cuda_graph_max_bs: 16 + schedule_policy: fcfs + attention_backend: fa3 + log_level: info + log_requests: false + sampling_backend: pytorch + enable_memory_saver: false + disable_radix_cache: false + triton_attention_num_kv_splits: 16 + disable_custom_all_reduce: true + disable_cuda_graph: false + +# Datasets: SWE-bench style jsonl with instance_id / problem_statement / +# eval_script per line. +train_dataset: + batch_size: 4 + shuffle: true + pin_memory: true + num_workers: 4 + path: /path/to/swe_bench_rl.jsonl + type: rl + max_length: 32768 + drop_last: true + +valid_dataset: + batch_size: 4 + shuffle: true + pin_memory: true + num_workers: 4 + path: /path/to/swe_bench_rl.jsonl + type: rl + +# Utilities +saver: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_steps: 10 + freq_secs: null + +recover: + mode: auto + retries: 3 + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: 1 + freq_secs: 7200 + no_save_optim: false + no_load_optim: false + freq_steps: 10 + +evaluator: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + freq_epochs: null + freq_steps: null + freq_secs: null + +stats_logger: + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + wandb: + mode: disabled + +perf_tracer: + enabled: false + experiment_name: ${experiment_name} + trial_name: ${trial_name} + fileroot: ${cluster.fileroot} + session_tracer: + enabled: false diff --git a/examples/swe/train_swe_rl.py b/examples/swe/train_swe_rl.py new file mode 100644 index 0000000000..57db957ef7 --- /dev/null +++ b/examples/swe/train_swe_rl.py @@ -0,0 +1,218 @@ +"""Training script for SWE-bench agent RL with AReaL proxy mode.""" + +import json +import sys +import warnings +from pathlib import Path +from typing import Any + +from datasets import Dataset + +from examples.swe.utils import SWEPPOConfig + +from areal import PPOTrainer +from areal.api.cli_args import load_expr_config +from areal.utils import logging + +logger = logging.getLogger("SWETrain") + + +def get_swe_dataset( + dataset_path: str, + split: str = "train", + min_items: int = 64, +) -> Dataset: + """Create a HuggingFace Dataset from a SWE-bench JSONL file. + + Each line in the JSONL file should be a SWE-bench instance with at minimum: + - instance_id: The SWE-bench instance ID (e.g., "django__django-10097") + - problem_statement: The GitHub issue description + - eval_script: Shell script to evaluate the agent's fix + + Args: + dataset_path: Path to the SWE-bench JSONL file. + split: Informational split label (not used for filtering). + min_items: Minimum dataset size; items are duplicated if fewer exist. + + Returns: + HuggingFace Dataset of SWE-bench instances. + """ + path = Path(dataset_path) + if not path.exists(): + raise FileNotFoundError(f"SWE-bench dataset not found: {dataset_path}") + + dataset_items = [] + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + item = json.loads(line) + if "instance_id" not in item: + logger.warning(f"Skipping item missing 'instance_id': {line[:100]}") + continue + if "problem_statement" not in item: + logger.warning( + "Skipping item missing 'problem_statement': " + f"{item.get('instance_id')}" + ) + continue + dataset_items.append(item) + + if not dataset_items: + raise ValueError(f"No valid items found in dataset: {dataset_path}") + + # Duplicate dataset if fewer than min_items for efficient batching + if len(dataset_items) < min_items: + original_items = dataset_items.copy() + while len(dataset_items) < min_items: + dataset_items.extend(original_items) + + dataset = Dataset.from_list(dataset_items) + logger.info( + f"Created SWE dataset with {len(dataset)} items " + f"from {dataset_path} (split={split})" + ) + return dataset + + +def group_filter(x: dict[str, Any]): + """Filter out groups where all rollouts already solved the task.""" + return x["rewards"].mean() <= 0.95 + + +def _install_aweagent_deps_on_ray_nodes(aweagent_root: str): + """Install AReaL-SWEAgent dependencies on all Ray GPU nodes. + + Each node runs in a separate container with its own venv, + so we must ensure packages like ``aenv`` are installed everywhere. + """ + if not aweagent_root: + aweagent_root = str( + Path(__file__).resolve().parents[2].parent / "AReaL-SWEAgent" + ) + try: + import ray + + if not ray.is_initialized(): + return + + @ray.remote(num_gpus=0) + def _install(): + import os + import socket + import subprocess + + ip = socket.gethostbyname(socket.gethostname()) + req_path = os.path.join(aweagent_root, "requirements.txt") + result = subprocess.run( + ["uv", "pip", "install", "-r", req_path], + capture_output=True, + text=True, + timeout=120, + ) + return ( + ip, + result.returncode, + result.stderr[-200:] if result.stderr else "", + ) + + nodes = [ + n + for n in ray.nodes() + if n.get("Alive") and n.get("Resources", {}).get("GPU", 0) > 0 + ] + refs = [] + for node in nodes: + node_ip = node["NodeManagerAddress"] + refs.append(_install.options(resources={f"node:{node_ip}": 0.01}).remote()) + + results = ray.get(refs, timeout=180) + for ip, rc, err in results: + if rc != 0: + logger.warning(f"Failed to install AReaL-SWEAgent deps on {ip}: {err}") + else: + logger.info(f"AReaL-SWEAgent deps installed on {ip}") + except Exception as e: + logger.warning(f"Could not install AReaL-SWEAgent deps on Ray nodes: {e}") + + +def _resolve_aweagent_root(econfig) -> str: + return ( + getattr(econfig, "agent_root", "") + or getattr(econfig, "aweagent_root", "") + or getattr(econfig, "swe_agent_root", "") + ) + + +def main(args): + warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") + + config, _ = load_expr_config(args, SWEPPOConfig) + econfig = config.econfig + + # When using Ray scheduler, ensure SWEAgent deps are on all nodes + if config.scheduler.type == "ray": + import ray + + ray.init(address="auto", ignore_reinit_error=True) + _install_aweagent_deps_on_ray_nodes(_resolve_aweagent_root(econfig)) + + # Resolve dataset paths from config + train_path = config.train_dataset.path + valid_path = config.valid_dataset.path + + def resolve_path(p: str) -> str: + if Path(p).is_absolute() or Path(p).exists(): + return p + if econfig.dataset_path: + candidate = Path(econfig.dataset_path) / p + if candidate.exists(): + return str(candidate) + return p + + train_dataset = get_swe_dataset( + dataset_path=resolve_path(train_path), + split="train", + ) + valid_dataset = get_swe_dataset( + dataset_path=resolve_path(valid_path), + split="test", + ) + + # Build workflow kwargs + from dataclasses import asdict + + econfig_dict = asdict(econfig) + workflow_kwargs = dict( + econfig=econfig_dict, + gen_args=dict( + temperature=config.gconfig.temperature, + max_completion_tokens=config.gconfig.max_new_tokens, + ), + timeout=econfig.timeout, + ) + + # Eval workflow with lower temperature for deterministic evaluation + eval_workflow_kwargs = workflow_kwargs.copy() + eval_workflow_kwargs["gen_args"] = dict( + temperature=0.0, + max_completion_tokens=config.gconfig.max_new_tokens, + ) + + with PPOTrainer( + config, + train_dataset=train_dataset, + valid_dataset=valid_dataset, + ) as trainer: + trainer.train( + workflow="examples.swe.agent.SWEAgentWorkflow", + workflow_kwargs=workflow_kwargs, + eval_workflow=None, + eval_workflow_kwargs=eval_workflow_kwargs, + dynamic_filter_fn=getattr(config, "should_accept_fn", None), + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/examples/swe/utils.py b/examples/swe/utils.py new file mode 100644 index 0000000000..c53881cbff --- /dev/null +++ b/examples/swe/utils.py @@ -0,0 +1,123 @@ +"""Utilities for SWE-bench agent training with AReaL.""" + +from dataclasses import dataclass, field + +from areal.api.cli_args import PPOConfig + + +@dataclass +class SWEEnvConfig: + """Environment configuration for AReaL-SWEAgent-backed SWE-bench training. + + Attributes: + dataset_path: Path to the SWE-bench JSONL dataset file. + agent_type: AReaL-SWEAgent agent type to train, e.g. ``swe`` or ``cc``. + agent_config: Generic AReaL-SWEAgent config name. When set, this overrides + the compatibility fields below. + swe_agent_config: Compatibility config field for ``agent_type=swe``. + cc_agent_config: Compatibility config field for ``agent_type=cc``. + agent_root: Root directory of the external AReaL-SWEAgent checkout. + swe_agent_root: Legacy alias for ``agent_root``. + llm_model: Optional LLM model override for OH/OpenCode/Codex agents. + opencode_provider: Optional OpenCode provider override. + codex_provider: Optional Codex provider override. + step_limit: Maximum number of agent interaction steps per episode. + max_completion_tokens: Maximum completion tokens for the agent LLM. + timeout: Maximum time allowed for a single episode in seconds. + """ + + dataset_path: str = field( + default="", + metadata={"help": "Path to the SWE-bench JSONL dataset file."}, + ) + agent_type: str = field( + default="swe", + metadata={ + "help": ( + "AReaL-SWEAgent agent type to run. Supported by AReaL-SWEAgent main: " + "'swe', 'cc', 'oh', 'opencode', and 'codex'." + ) + }, + ) + agent_config: str = field( + default="", + metadata={ + "help": ( + "Generic AReaL-SWEAgent YAML config name. When non-empty, overrides " + "swe_agent_config / cc_agent_config." + ) + }, + ) + swe_agent_config: str = field( + default="1_0_0/min-swe-agent-train-top1", + metadata={ + "help": ( + "Name of the AReaL-SWEAgent YAML config under the external AReaL-SWEAgent " + "checkout. Defaults to the Qwen SWE-RL training config." + ) + }, + ) + cc_agent_config: str = field( + default="train_cc_time3600", + metadata={ + "help": ( + "Name of the AReaL-SWEAgent YAML config used when agent_type='cc'. " + "Kept separate for compatibility with swe/main configs." + ) + }, + ) + agent_root: str = field( + default="", + metadata={ + "help": ( + "Root directory of the external AReaL-SWEAgent checkout. Defaults to " + "../AReaL-SWEAgent relative to the AReaL repository when unset." + ) + }, + ) + swe_agent_root: str = field( + default="", + metadata={ + "help": ( + "Legacy alias for agent_root / AWEAGENT_ROOT. Kept so older " + "SWE launch scripts keep working." + ) + }, + ) + llm_model: str = field( + default="", + metadata={"help": "Optional model name override for OH/OpenCode/Codex agents."}, + ) + opencode_provider: str = field( + default="", + metadata={"help": "Optional provider override for OpenCode agents."}, + ) + codex_provider: str = field( + default="", + metadata={"help": "Optional provider override for Codex agents."}, + ) + step_limit: int = field( + default=100, + metadata={"help": "Maximum number of agent interaction steps per episode."}, + ) + max_completion_tokens: int = field( + default=16384, + metadata={"help": "Maximum completion tokens for the agent LLM."}, + ) + timeout: float = field( + default=1800.0, + metadata={"help": "Maximum time allowed for a single episode in seconds."}, + ) + + +@dataclass +class SWEPPOConfig(PPOConfig): + """PPO configuration with SWE-bench-specific settings.""" + + econfig: SWEEnvConfig = field(default_factory=SWEEnvConfig) + should_accept_fn: str | None = field( + default=None, + metadata={ + "help": "Import path of the filter function for accepting rollout samples." + }, + ) From f2958b6fe6946169549f954c63e942dc89972fe1 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:18:07 +0800 Subject: [PATCH 28/36] fix: fix safe-to-test CI workflow (#1456) * fix: fix safe-to-test CI workflow * fix(tests): remove test_hermes_agent.py Companion cleanup to the openclaw removal in the previous commit. Co-Authored-By: Claude Opus 4.7 * fix: fix wu ci test --------- Co-authored-by: Claude Opus 4.7 --- .github/workflows/test-areal.yml | 2 +- tests/v2/agent_service/test_hermes_agent.py | 234 ------------------ tests/v2/agent_service/test_openclaw_agent.py | 210 ---------------- tests/v2/weight_update/test_wu_controller.py | 2 + 4 files changed, 3 insertions(+), 445 deletions(-) delete mode 100644 tests/v2/agent_service/test_hermes_agent.py delete mode 100644 tests/v2/agent_service/test_openclaw_agent.py diff --git a/.github/workflows/test-areal.yml b/.github/workflows/test-areal.yml index f38d1a01fb..01357a8deb 100644 --- a/.github/workflows/test-areal.yml +++ b/.github/workflows/test-areal.yml @@ -322,7 +322,7 @@ jobs: VIRTUAL_ENV: /opt/.venv run: | export PATH="/opt/.venv/bin:$PATH" - pytest -m "(not slow or ci) and not ${EXCLUDE_BACKEND}" --durations=20 -s -vv tests/test_*.py tests/experimental/ tests/infra/ + pytest -m "(not slow or ci) and not ${EXCLUDE_BACKEND}" --durations=20 -s -vv tests/test_*.py tests/experimental/ tests/infra/ tests/v2/ cleanup: name: Tear down GCP runner (${{ matrix.variant }}) diff --git a/tests/v2/agent_service/test_hermes_agent.py b/tests/v2/agent_service/test_hermes_agent.py deleted file mode 100644 index 5759732e4d..0000000000 --- a/tests/v2/agent_service/test_hermes_agent.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Unit tests for the Hermes agent runtime. - -These tests exercise :class:`HermesAgent` without the real ``hermes-agent`` -package or a GPU: a stub ``AIAgent`` (whose ``run_conversation`` returns a -canned result) is injected per session, and the missing-binary path is driven -by leaving ``_build_agent`` unpatched while no session exists. -""" - -from __future__ import annotations - -import json - -import pytest -from examples.agent_service.hermes.hermes import ( - HermesAgent, - _SessionState, - _Upstream, -) - -from areal.v2.agent_service.types import AgentRequest - - -class _RecordingEmitter: - def __init__(self) -> None: - self.deltas: list[str] = [] - self.tool_calls: list[tuple[str, str]] = [] - self.tool_results: list[tuple[str, str]] = [] - - async def emit_delta(self, text: str) -> None: - self.deltas.append(text) - - async def emit_tool_call(self, name: str, args: str) -> None: - self.tool_calls.append((name, args)) - - async def emit_tool_result(self, name: str, result: str) -> None: - self.tool_results.append((name, result)) - - -class _StubAIAgent: - """Stand-in for a Hermes ``AIAgent`` that records calls and returns a canned result.""" - - def __init__(self, result: dict) -> None: - self._result = result - self.calls: list[dict] = [] - - def run_conversation( - self, user_message, conversation_history=None, task_id=None, **_ - ): - self.calls.append( - { - "user_message": user_message, - "conversation_history": conversation_history, - "task_id": task_id, - } - ) - return self._result - - -def _attach_stub(agent: HermesAgent, session_key: str, result: dict) -> _StubAIAgent: - stub = _StubAIAgent(result) - agent._sessions[session_key] = _SessionState( - session_key=session_key, - agent=stub, - upstream=_Upstream(base_url="http://u", api_key="k", model="m"), - ) - return stub - - -@pytest.mark.asyncio -async def test_run_returns_final_response_and_tool_calls(): - """Structured run emits the final text once and surfaces tool calls in metadata. - - Tool calls are reported in ``metadata['tool_calls']`` for observability but - are deliberately *not* emitted as ``tool_call`` events: Hermes executes - tools internally and the framework does not feed a paired ``tool_result``, - so emitting one would build an invalid replay history. - """ - result = { - "final_response": "Hello world", - "completed": True, - "api_calls": 3, - "messages": [ - { - "role": "assistant", - "tool_calls": [ - {"function": {"name": "search", "arguments": '{"q":"hi"}'}} - ], - }, - {"role": "tool", "content": "ok"}, - {"role": "assistant", "content": "Hello world"}, - ], - } - agent = HermesAgent() - stub = _attach_stub(agent, "s1", result) - emitter = _RecordingEmitter() - req = AgentRequest( - message="hi", - session_key="s1", - run_id="r1", - history=[{"role": "user", "content": "earlier"}], - ) - - resp = await agent.run(req, emitter=emitter) - await agent.close_all_sessions() - - assert resp.summary == "Hello world" - assert emitter.deltas == ["Hello world"] - assert emitter.tool_calls == [] - assert resp.metadata["tool_calls"] == [{"name": "search", "input": '{"q":"hi"}'}] - assert resp.metadata["completed"] is True - assert resp.metadata["api_calls"] == 3 - # The replayed history is forwarded verbatim as the single source of truth. - assert stub.calls[0]["conversation_history"] == [ - {"role": "user", "content": "earlier"} - ] - assert stub.calls[0]["user_message"] == "hi" - - -@pytest.mark.asyncio -async def test_chat_request_synthesizes_streaming_completion(): - """A streaming chat_request yields an OpenAI chat-completions SSE StreamResponse.""" - agent = HermesAgent() - _attach_stub(agent, "s2", {"final_response": "Hi there", "messages": []}) - chat_body = { - "model": "hermes-x", - "stream": True, - "messages": [ - {"role": "user", "content": "prev"}, - {"role": "assistant", "content": "ok"}, - {"role": "user", "content": "now"}, - ], - } - req = AgentRequest( - message="now", - session_key="s2", - run_id="r1", - history=[], - metadata={"chat_request": chat_body}, - ) - - resp = await agent.run(req, emitter=_RecordingEmitter()) - chunks = b"".join([chunk async for chunk in resp.body]).decode("utf-8") - await agent.close_all_sessions() - - assert resp.status_code == 200 - assert resp.headers["content-type"] == "text/event-stream" - assert "data: [DONE]" in chunks - # The assistant content delta carries Hermes' final text. - content_lines = [ - json.loads(line[len("data: ") :]) - for line in chunks.splitlines() - if line.startswith("data: ") and line != "data: [DONE]" - ] - contents = [ - c["choices"][0]["delta"].get("content") - for c in content_lines - if c["choices"][0]["delta"].get("content") - ] - assert contents == ["Hi there"] - assert content_lines[0]["object"] == "chat.completion.chunk" - - -@pytest.mark.asyncio -async def test_chat_request_non_streaming_json(): - """A non-streaming chat_request yields a single chat.completion JSON object.""" - agent = HermesAgent() - _attach_stub(agent, "s3", {"final_response": "Done", "messages": []}) - chat_body = { - "model": "hermes-x", - "messages": [{"role": "user", "content": "go"}], - } - req = AgentRequest( - message="go", - session_key="s3", - run_id="r1", - history=[], - metadata={"chat_request": chat_body}, - ) - - resp = await agent.run(req, emitter=_RecordingEmitter()) - body = b"".join([chunk async for chunk in resp.body]).decode("utf-8") - await agent.close_all_sessions() - - assert resp.headers["content-type"] == "application/json" - payload = json.loads(body) - assert payload["object"] == "chat.completion" - assert payload["choices"][0]["message"]["content"] == "Done" - assert payload["choices"][0]["finish_reason"] == "stop" - - -@pytest.mark.asyncio -async def test_extract_chat_splits_last_user_message(): - """_extract_chat returns the last user text and the preceding history.""" - message, history = HermesAgent._extract_chat( - { - "messages": [ - {"role": "system", "content": "sys"}, - {"role": "user", "content": "first"}, - {"role": "assistant", "content": "reply"}, - {"role": "user", "content": "second"}, - ] - } - ) - assert message == "second" - assert history == [ - {"role": "system", "content": "sys"}, - {"role": "user", "content": "first"}, - {"role": "assistant", "content": "reply"}, - ] - - -@pytest.mark.asyncio -async def test_close_session_drops_lock(): - """close_session removes the per-session lock to bound _locks growth.""" - agent = HermesAgent() - await agent._session_lock("sX") - assert "sX" in agent._locks - - await agent.close_session("sX") - assert "sX" not in agent._locks - - # idempotent: closing an already-closed session is safe - await agent.close_session("sX") - assert "sX" not in agent._locks - - -@pytest.mark.asyncio -async def test_run_without_session_or_env_raises(): - """run() with no live session and no env upstream raises clearly.""" - agent = HermesAgent() - agent._env_upstream = None - req = AgentRequest(message="hi", session_key="missing", run_id="r1", history=[]) - with pytest.raises(RuntimeError, match="No agent for session"): - await agent.run(req, emitter=_RecordingEmitter()) diff --git a/tests/v2/agent_service/test_openclaw_agent.py b/tests/v2/agent_service/test_openclaw_agent.py deleted file mode 100644 index 89a3800aeb..0000000000 --- a/tests/v2/agent_service/test_openclaw_agent.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Unit tests for the OpenClaw agent runtime. - -These tests exercise :class:`OpenClawAgent` without a real ``openclaw`` -binary or GPU: the per-turn SSE parsing is driven by an -``httpx.MockTransport`` and subprocess-spawn failure is triggered by -pointing ``OPENCLAW_BIN`` at a non-existent executable. -""" - -from __future__ import annotations - -import glob -import os -import tempfile - -import pytest -from examples.agent_service.openclaw.openclaw import ( - OpenClawAgent, - _SessionState, - _Upstream, -) - -from areal.v2.agent_service.types import AgentRequest - -httpx = pytest.importorskip("httpx") - - -class _RecordingEmitter: - def __init__(self) -> None: - self.deltas: list[str] = [] - self.tool_calls: list[tuple[str, str]] = [] - self.tool_results: list[tuple[str, str]] = [] - - async def emit_delta(self, text: str) -> None: - self.deltas.append(text) - - async def emit_tool_call(self, name: str, args: str) -> None: - self.tool_calls.append((name, args)) - - async def emit_tool_result(self, name: str, result: str) -> None: - self.tool_results.append((name, result)) - - -class _ExitedProc: - """Stand-in for an already-terminated subprocess used by mock sessions. - - A non-None ``returncode`` makes ``_teardown_state`` skip the kill path. - """ - - returncode = 0 - - async def wait(self) -> int: - return 0 - - -def _attach_mock_session(agent: OpenClawAgent, session_key: str, sse: str) -> None: - """Inject a session whose subprocess HTTP client returns ``sse``.""" - - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( - 200, text=sse, headers={"content-type": "text/event-stream"} - ) - - client = httpx.AsyncClient( - base_url="http://mock-openclaw", - transport=httpx.MockTransport(handler), - ) - agent._sessions[session_key] = _SessionState( - port=1, - gateway_token="t", - config_dir=tempfile.mkdtemp(prefix="openclaw-mock-"), - process=_ExitedProc(), # type: ignore[arg-type] - client=client, - log_file=None, # type: ignore[arg-type] - ) - - -@pytest.mark.asyncio -async def test_run_accumulates_streamed_tool_calls(): - """Streamed tool-call name/args are buffered by index into metadata. - - They are surfaced in ``metadata['tool_calls']`` for observability but are - deliberately *not* emitted as ``tool_call`` events: OpenClaw runs tools in - its own subprocess and never returns a matching tool result, so emitting a - ``tool_call`` without a paired ``tool_result`` would make the DataProxy - build an invalid chat-completions history that the upstream rejects on - replay. - """ - sse = ( - 'data: {"choices":[{"delta":{"content":"Hel"}}]}\n' - 'data: {"choices":[{"delta":{"content":"lo"}}]}\n' - # name arrives only in the first chunk; arguments stream across chunks - 'data: {"choices":[{"delta":{"tool_calls":' - '[{"index":0,"function":{"name":"search"}}]}}]}\n' - 'data: {"choices":[{"delta":{"tool_calls":' - '[{"index":0,"function":{"arguments":"{\\"q\\":"}}]}}]}\n' - # no space after "data:" — valid per the SSE spec - 'data:{"choices":[{"delta":{"tool_calls":' - '[{"index":0,"function":{"arguments":"\\"hi\\"}"}}]}}]}\n' - # a second tool call at a different index - 'data: {"choices":[{"delta":{"tool_calls":' - '[{"index":1,"function":{"name":"calc","arguments":"1+1"}}]}}]}\n' - "data: [DONE]\n" - ) - agent = OpenClawAgent() - _attach_mock_session(agent, "s1", sse) - emitter = _RecordingEmitter() - req = AgentRequest(message="hi", session_key="s1", run_id="r1", history=[]) - - resp = await agent.run(req, emitter=emitter) - await agent.close_all_sessions() - - assert resp.summary == "Hello" - assert emitter.deltas == ["Hel", "lo"] - # tool calls are NOT streamed as events (no paired tool_result available) - assert emitter.tool_calls == [] - # but they are fully accumulated by index into the response metadata - assert resp.metadata["tool_calls"] == [ - {"name": "search", "input": '{"q":"hi"}'}, - {"name": "calc", "input": "1+1"}, - ] - - -@pytest.mark.asyncio -async def test_run_keeps_full_summary_for_replay(): - """summary is returned in full, not truncated, so replay stays faithful. - - The DataProxy stores ``summary`` as the assistant turn and replays it on - later turns; truncating it would corrupt both the replayed prompt and the - user-visible output. - """ - long_text = "x" * 500 - sse = ( - f'data: {{"choices":[{{"delta":{{"content":"{long_text}"}}}}]}}\ndata: [DONE]\n' - ) - agent = OpenClawAgent() - _attach_mock_session(agent, "s_long", sse) - emitter = _RecordingEmitter() - req = AgentRequest(message="hi", session_key="s_long", run_id="r1", history=[]) - - resp = await agent.run(req, emitter=emitter) - await agent.close_all_sessions() - - assert resp.summary == long_text - assert len(resp.summary) == 500 - - -@pytest.mark.asyncio -async def test_run_skips_malformed_and_indexless_chunks(): - """Malformed JSON and tool-call chunks without an index are ignored.""" - sse = ( - "data: not-json\n" - 'data: {"choices":[]}\n' - 'data: {"choices":[{"delta":{"tool_calls":' - '[{"function":{"name":"orphan"}}]}}]}\n' # missing index - 'data: {"choices":[{"delta":{"content":"ok"}}]}\n' - "data: [DONE]\n" - ) - agent = OpenClawAgent() - _attach_mock_session(agent, "s2", sse) - emitter = _RecordingEmitter() - req = AgentRequest(message="hi", session_key="s2", run_id="r1", history=[]) - - resp = await agent.run(req, emitter=emitter) - await agent.close_all_sessions() - - assert resp.summary == "ok" - assert emitter.tool_calls == [] - assert resp.metadata["tool_calls"] == [] - - -@pytest.mark.asyncio -async def test_spawn_cleans_up_on_failure(monkeypatch): - """A spawn failure leaks neither the temp config dir nor a file handle.""" - monkeypatch.setenv("OPENCLAW_BIN", "areal-nonexistent-openclaw-binary") - agent = OpenClawAgent() - upstream = _Upstream(base_url="http://upstream", api_key="k", model="m") - - tmp_glob = os.path.join(tempfile.gettempdir(), "openclaw-*") - before = set(glob.glob(tmp_glob)) - - with pytest.raises(FileNotFoundError): - await agent._spawn("s3", upstream) - - after = set(glob.glob(tmp_glob)) - assert after - before == set(), "spawn failure leaked a temp config dir" - - -@pytest.mark.asyncio -async def test_close_session_drops_lock(): - """close_session removes the per-session lock to bound _locks growth.""" - agent = OpenClawAgent() - await agent._session_lock("sX") - assert "sX" in agent._locks - - await agent.close_session("sX") - assert "sX" not in agent._locks - - # idempotent: closing an already-closed session is safe - await agent.close_session("sX") - assert "sX" not in agent._locks - - -@pytest.mark.asyncio -async def test_run_without_session_or_env_raises(): - """run() with no open session and no env upstream raises clearly.""" - agent = OpenClawAgent() - agent._env_upstream = None - req = AgentRequest(message="hi", session_key="missing", run_id="r1", history=[]) - with pytest.raises(RuntimeError, match="No subprocess for session"): - await agent.run(req, emitter=_RecordingEmitter()) diff --git a/tests/v2/weight_update/test_wu_controller.py b/tests/v2/weight_update/test_wu_controller.py index ba029d3bf2..68e3b93432 100644 --- a/tests/v2/weight_update/test_wu_controller.py +++ b/tests/v2/weight_update/test_wu_controller.py @@ -81,6 +81,7 @@ def test_connect_sends_correct_request(self, ctrl): "save_path": "", "use_lora": False, "lora_name": "", + "lora_keep_versions": 0, "colocate": False, "nccl_master_addr": "", "nccl_master_port": 0, @@ -113,6 +114,7 @@ def test_connect_disk_mode_sends_disk_fields(self, ctrl): "save_path": "/shared/weights", "use_lora": True, "lora_name": "my-lora", + "lora_keep_versions": 0, "colocate": False, "nccl_master_addr": "", "nccl_master_port": 0, From 7f3b021dd2133ca526d87751d615639986dbcfbb Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:35:30 +0800 Subject: [PATCH 29/36] feat(swe): add SWE agent proxy, message preprocessors and tool-call support (#1458) Extend the OpenAI-compatible proxy rollout server with pluggable message preprocessors and a configurable interaction-cache prefix matcher, add Anthropic-to-OpenAI content handling and tool-call streaming, the Qwen-style tool-call parser, and the accompanying client/proxy/tool-call tests. --- areal/api/cli_args.py | 24 ++ areal/experimental/openai/cache.py | 149 +++++++++++-- areal/experimental/openai/client.py | 56 +++++ .../openai/proxy/proxy_rollout_server.py | 55 +++-- areal/experimental/openai/proxy/server.py | 7 +- areal/experimental/openai/tool_call_parser.py | 206 ++++++++++++++++++ docs/en/cli_reference.md | 2 + docs/zh/cli_reference.md | 2 + .../openai/test_client_with_tool.py | 25 ++- .../openai/test_resolve_max_total_tokens.py | 36 +++ .../openai/test_tool_call_parser.py | 108 +++++++++ 11 files changed, 627 insertions(+), 43 deletions(-) create mode 100644 tests/experimental/openai/test_resolve_max_total_tokens.py diff --git a/areal/api/cli_args.py b/areal/api/cli_args.py index 34783b2db7..4ddcf613ac 100644 --- a/areal/api/cli_args.py +++ b/areal/api/cli_args.py @@ -2161,6 +2161,30 @@ class AgentConfig: "choices": ["individual", "concat"], }, ) + message_preprocessors: list[str] = field( + default_factory=list, + metadata={ + "help": ( + "List of message preprocessor class paths applied, in order, to " + "Anthropic-compatible `/v1/messages` requests after translating " + "them to OpenAI-compatible requests. Native OpenAI " + "`/chat/completions` and `/responses` requests are not " + "preprocessed. Each entry is a dotted import path to a callable " + "class." + ), + }, + ) + prefix_matcher: str | None = field( + default=None, + metadata={ + "help": ( + "Dotted import path to a custom prefix matcher function for " + "InteractionCache parent-child matching. The function must accept " + "two list[dict] arguments (candidate prefix, full messages) and " + "return bool. When None, exact element-wise equality is used." + ), + }, + ) subproc_max_workers: int = field( default=4, metadata={ diff --git a/areal/experimental/openai/cache.py b/areal/experimental/openai/cache.py index 20fee73ca8..33888a4d6c 100644 --- a/areal/experimental/openai/cache.py +++ b/areal/experimental/openai/cache.py @@ -1,17 +1,55 @@ # SPDX-License-Identifier: Apache-2.0 +import json +import os import threading +import time from collections import OrderedDict -from typing import Any +from collections.abc import Callable +from pathlib import Path +from typing import Any, Protocol, runtime_checkable from areal.experimental.openai.types import InteractionWithTokenLogpReward from areal.utils import logging logger = logging.getLogger("OpenAICache") +# Debug-only: dumping mismatched parent/child messages is OFF by default because +# the payloads can contain full conversations. Set the dump dir env var to opt in. +_MISMATCH_DUMP_DIR_ENV = "AREAL_OPENAI_CACHE_MISMATCH_DUMP_DIR" +_MISMATCH_DUMP_LIMIT_ENV = "AREAL_OPENAI_CACHE_MISMATCH_DUMP_LIMIT" +_DEFAULT_MISMATCH_DUMP_LIMIT = 20 + + +@runtime_checkable +class PrefixMatcher(Protocol): + """Protocol for custom message prefix matching functions.""" + + def __call__(self, a: list[dict], b: list[dict]) -> bool: ... + + +def default_prefix_matcher(a: list[dict], b: list[dict]) -> bool: + """Default exact prefix matcher: ``b[:len(a)] == a``.""" + if len(a) > len(b): + return False + return b[: len(a)] == a + class InteractionCache(OrderedDict[str, InteractionWithTokenLogpReward]): - def __init__(self, *args, **kwargs): + def __init__( + self, + *args, + session_id: str = "unknown", + prefix_matcher: PrefixMatcher + | Callable[[list[dict], list[dict]], bool] + | None = None, + **kwargs, + ): + self._match_fail_count = 0 + self._session_id = session_id + self._prefix_matcher: Callable[[list[dict], list[dict]], bool] = ( + prefix_matcher if prefix_matcher is not None else default_prefix_matcher + ) super().__init__(*args, **kwargs) self._apply_reward_discount_called = False self._total_reward = 0.0 @@ -29,7 +67,10 @@ def __deepcopy__(self, memo): assert len(self) == 0, ( f"InteractionCache must be empty when deep-copied, but has {len(self)} items" ) - new = InteractionCache() + new = InteractionCache( + session_id=self._session_id, + prefix_matcher=self._prefix_matcher, + ) memo[id(self)] = new return new @@ -115,12 +156,6 @@ def __setitem__( "Interaction messages must be set to find parent relationship." ) - def _is_prefix(a: list[dict], b: list[dict]) -> bool: - # True if a is a prefix of b - if len(a) > len(b): - return False - return b[: len(a)] == a - def _is_similar_on_last_message( a: list[dict], b: list[dict] ) -> tuple[bool, dict[str, Any] | None, dict[str, Any] | None]: @@ -143,13 +178,12 @@ def _is_similar_on_last_message( } return True, diff_a_message, diff_b_message - # Construct parent-child relationships using longest prefix rule - # Sort potential parents by message length to find the longest prefix match first. + # Construct parent-child relationships using longest prefix rule. interactions = sorted( self.values(), key=lambda x: len(x.messages), reverse=True ) + child_msgs = value.messages - # Find parent for the new interaction for parent in interactions: # Skip interactions that are still being processed (output_message_list not set yet) # This can happen with concurrent requests where a streaming request hasn't @@ -157,24 +191,103 @@ def _is_similar_on_last_message( if parent.output_message_list is None or parent.messages is None: continue parent_data = parent.messages + parent.output_message_list - if _is_prefix(parent_data, value.messages): + if self._prefix_matcher(parent_data, child_msgs): value.parent = parent break - elif _is_prefix(parent.messages, value.messages): + elif self._prefix_matcher(parent.messages, child_msgs): + self._match_fail_count += 1 + + logger.warning( + "Prefix mismatch (occurrence %d, session=%s): " + "parent.messages (len=%d) is a prefix of child (len=%d), " + "but parent_data (messages+output, len=%d) is not.", + self._match_fail_count, + self._session_id, + len(parent.messages), + len(child_msgs), + len(parent_data), + ) + self._dump_mismatch( + parent_data=parent_data, + child_msgs=child_msgs, + parent_id=parent.interaction_id, + child_key=key, + ) + is_similar, diff_a, diff_b = _is_similar_on_last_message( - parent_data, value.messages + parent_data, child_msgs ) if is_similar: logger.warning( "Found a parent interaction with similar last message content, " - "but not a strict prefix match. If you wish to use concat mode and build a conversation tree:\n" + "but not a strict prefix match (occurrence %d, " + "session=%s, parent_len=%d, child_len=%d). " + "If you wish to use concat mode and build a conversation tree:\n" "1. For completion, append `chat_completion.choices[0].message.model_dump()` to your messages.\n" "2. For response, extend `[o.model_dump() for o in response.output]` to your messages.\n" - f"Different keys in parent last message: {diff_a}\n" - f"Different keys in child last message: {diff_b}\n" + "Different keys in parent last message: %s\n" + "Different keys in child last message: %s", + self._match_fail_count, + self._session_id, + len(parent_data), + len(child_msgs), + diff_a, + diff_b, ) super().__setitem__(key, value) + def _dump_mismatch( + self, + parent_data: list[dict], + child_msgs: list[dict], + parent_id: str | None, + child_key: str, + ) -> None: + """Dump mismatched parent/child messages to a JSON file for debugging.""" + dump_dir_env = os.environ.get(_MISMATCH_DUMP_DIR_ENV) + if not dump_dir_env: + return + try: + dump_limit = int( + os.environ.get( + _MISMATCH_DUMP_LIMIT_ENV, str(_DEFAULT_MISMATCH_DUMP_LIMIT) + ) + ) + if dump_limit > 0 and self._match_fail_count > dump_limit: + return + + dump_dir = Path(dump_dir_env) + dump_dir.mkdir(parents=True, exist_ok=True) + + ts = time.strftime("%Y%m%d_%H%M%S") + filename = f"mismatch_{self._session_id}_{self._match_fail_count}_{ts}.json" + dump_path = dump_dir / filename + + first_diff_idx = None + for i, (pm, cm) in enumerate(zip(parent_data, child_msgs)): + if pm != cm: + first_diff_idx = i + break + + dump = { + "session_id": self._session_id, + "occurrence": self._match_fail_count, + "parent_id": parent_id, + "child_key": child_key, + "parent_len": len(parent_data), + "child_len": len(child_msgs), + "first_diff_idx": first_diff_idx, + "parent_data": parent_data, + "child_msgs": child_msgs, + } + + with open(dump_path, "w", encoding="utf-8") as f: + json.dump(dump, f, indent=2, ensure_ascii=False, default=str) + + logger.info("Mismatch dump saved to %s", dump_path) + except Exception as e: + logger.warning("Failed to dump mismatch: %s", e) + def export_interactions( self, style: str, reward_discount: float | None = None ) -> dict[str, InteractionWithTokenLogpReward]: diff --git a/areal/experimental/openai/client.py b/areal/experimental/openai/client.py index 2655b0eba1..dc68117fed 100644 --- a/areal/experimental/openai/client.py +++ b/areal/experimental/openai/client.py @@ -76,6 +76,8 @@ async def agenerate(self, req: ModelRequest) -> ModelResponse: logger = logging.getLogger("OpenAIClient") +_DEFAULT_MAX_TOTAL_TOKENS = 32768 + def _ensure_message_dict_list( name: str, @@ -343,6 +345,18 @@ def _build_messages_list(item: dict) -> list[dict]: return messages_list +def _resolve_max_total_tokens( + prompt_len: int, + max_new_tokens: int, + engine_max_tokens: int | None, +) -> int: + # Fall back to a fixed context-length ceiling when the deployment does not + # configure engine_max_tokens, so a large client max_tokens cannot push + # prompt + generation past the backend model's context window. + cap = engine_max_tokens or _DEFAULT_MAX_TOTAL_TOKENS + return min(prompt_len + max_new_tokens, cap) + + def _parse_tool_call_arguments(messages: list[dict]) -> list[dict]: """Return a new message list with tool_call arguments parsed from JSON strings to dicts. Some chat templates (e.g. GLM-5.1) iterate over arguments with @@ -759,6 +773,11 @@ async def create( n_samples=n, temperature=temp, max_new_tokens=max_new_tokens, + max_tokens=_resolve_max_total_tokens( + prompt_len=len(prompt_token_ids), + max_new_tokens=max_new_tokens, + engine_max_tokens=self.engine_max_tokens, + ), top_p=top_p_val, stop=stop_tokens, greedy=temp == 0, @@ -898,9 +917,17 @@ async def _create_stream( ) # Tool calls chunks (if any) + # Split each tool call into two chunks (name then arguments) to + # match standard OpenAI streaming behavior. LiteLLM's Anthropic + # streaming adapter treats the first chunk with a function name as + # a content_block_start trigger and discards the processed delta + # from that same chunk. If name and arguments are combined in a + # single chunk, the arguments are lost and Anthropic clients see + # input={}. if tool_calls: for idx, tool_call in enumerate(tool_calls): tool_call = cast(ChatCompletionMessageFunctionToolCall, tool_call) + # Chunk 1: name + id, with empty arguments. yield ChatCompletionChunk( id=completion_id, choices=[ @@ -913,6 +940,30 @@ async def _create_stream( type="function", function=ChoiceDeltaToolCallFunction( name=tool_call.function.name, + arguments="", + ), + ) + ] + ), + index=0, + finish_reason=None, + ) + ], + created=current_time, + model="None", + object="chat.completion.chunk", + ) + # Chunk 2: arguments only, emitted as input_json_delta by + # Anthropic adapters after the tool_use block has started. + yield ChatCompletionChunk( + id=completion_id, + choices=[ + ChunkChoice( + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + index=idx, + function=ChoiceDeltaToolCallFunction( arguments=tool_call.function.arguments, ), ) @@ -1121,6 +1172,11 @@ async def create( n_samples=1, temperature=temp, max_new_tokens=max_new_tokens, + max_tokens=_resolve_max_total_tokens( + prompt_len=len(prompt_token_ids), + max_new_tokens=max_new_tokens, + engine_max_tokens=self.engine_max_tokens, + ), top_p=top_p_val, stop=stop, greedy=temp == 0, diff --git a/areal/experimental/openai/proxy/proxy_rollout_server.py b/areal/experimental/openai/proxy/proxy_rollout_server.py index 38915590e4..5d5453d92d 100644 --- a/areal/experimental/openai/proxy/proxy_rollout_server.py +++ b/areal/experimental/openai/proxy/proxy_rollout_server.py @@ -110,6 +110,14 @@ def _warn_once(msg: str) -> None: _api_key_to_session: dict[str, str] = {} _session_to_api_key: dict[str, str] = {} # Reverse mapping for O(1) cleanup +# Pluggable message preprocessors loaded from config at setup time. +# Applied in order after Anthropic-to-OpenAI translation, before content +# reaches the ArealOpenAI client. +_message_preprocessors: list = [] + +# Pluggable prefix matcher for InteractionCache parent-child matching. +_prefix_matcher = None + # Server address (set at startup) _server_host: str = "0.0.0.0" _server_port: int = 8000 @@ -262,6 +270,7 @@ async def alloc_ports(raw_request: Request): def _setup_openai_client(): global _openai_client, _session_timeout_seconds, _admin_api_key + global _message_preprocessors, _prefix_matcher config = _engine.config tokenizer = load_hf_tokenizer(config.tokenizer_path) agent_cfg = config.agent @@ -292,6 +301,18 @@ def _setup_openai_client(): with _lock: _admin_api_key = agent_cfg.admin_api_key + _message_preprocessors = [] + for path in agent_cfg.message_preprocessors: + cls = import_from_string(path) + _message_preprocessors.append(cls()) + logger.info("Loaded message preprocessor: %s", path) + + if agent_cfg.prefix_matcher: + _prefix_matcher = import_from_string(agent_cfg.prefix_matcher) + logger.info("Loaded prefix matcher: %s", agent_cfg.prefix_matcher) + else: + _prefix_matcher = None + @app.post("/configure") async def configure(raw_request: Request): @@ -464,7 +485,10 @@ def start_session(request: StartSessionRequest) -> StartSessionResponse: session_api_key = secrets.token_urlsafe(32) _capacity -= 1 - _session_cache[session_id] = SessionData(session_id=session_id) + _session_cache[session_id] = SessionData( + session_id=session_id, + prefix_matcher=_prefix_matcher, + ) _api_key_to_session[session_api_key] = session_id _session_to_api_key[session_id] = session_api_key @@ -698,6 +722,19 @@ async def responses( ) +def _flatten_content_lists(messages: list[dict]) -> None: + """Flatten Anthropic content block lists to strings in-place.""" + for msg in messages: + if isinstance(msg.get("content"), list): + text_parts = [] + for block in msg["content"]: + if isinstance(block, dict) and block.get("type") == "text": + text_parts.append(block.get("text", "")) + elif isinstance(block, str): + text_parts.append(block) + msg["content"] = "\n".join(text_parts) + + def _translate_anthropic_to_openai_request(anthropic_request: dict[str, Any]) -> dict: """Translate an Anthropic Messages API request to OpenAI format.""" openai_request = _adapter.translate_completion_input_params( @@ -707,20 +744,10 @@ def _translate_anthropic_to_openai_request(anthropic_request: dict[str, Any]) -> raise ValueError("Failed to translate request") openai_request = dict(openai_request) - # Fix message content if it's a list (Anthropic format with content blocks) - # LiteLLM's adapter may not properly convert content from list to string - # Claude Code CLI sends content as: [{"type":"text","text":"...","cache_control":{...}}, ...] if "messages" in openai_request: - for msg in openai_request["messages"]: - if isinstance(msg.get("content"), list): - # Convert list of content blocks to string - text_parts = [] - for block in msg["content"]: - if isinstance(block, dict) and block.get("type") == "text": - text_parts.append(block.get("text", "")) - elif isinstance(block, str): - text_parts.append(block) - msg["content"] = "\n".join(text_parts) + _flatten_content_lists(openai_request["messages"]) + for preprocessor in _message_preprocessors: + openai_request["messages"] = preprocessor(openai_request["messages"]) return openai_request diff --git a/areal/experimental/openai/proxy/server.py b/areal/experimental/openai/proxy/server.py index 6e160c4644..54e4885e21 100644 --- a/areal/experimental/openai/proxy/server.py +++ b/areal/experimental/openai/proxy/server.py @@ -66,11 +66,14 @@ class ExportTrajectoriesResponse(BaseModel): class SessionData: """Data associated with a single RL session.""" - def __init__(self, session_id: str): + def __init__(self, session_id: str, prefix_matcher=None): self.session_id = session_id self._completed = False - self._completions = InteractionCache() + self._completions = InteractionCache( + session_id=session_id, + prefix_matcher=prefix_matcher, + ) self._completed_event = threading.Event() self._start_time = time.time() self._last_access_time = time.time() diff --git a/areal/experimental/openai/tool_call_parser.py b/areal/experimental/openai/tool_call_parser.py index 850b020bd0..4425e241a5 100644 --- a/areal/experimental/openai/tool_call_parser.py +++ b/areal/experimental/openai/tool_call_parser.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 +import json +import re import traceback import uuid from types import SimpleNamespace @@ -15,6 +17,19 @@ logger = logging.getLogger("ToolCallParser") +_QWEN3_CODER_TOOL_CALL_RE = re.compile( + r"\s*(?P.*?)\s*", + re.DOTALL, +) +_QWEN3_CODER_FUNCTION_RE = re.compile( + r"[^>\s]+)>\s*(?P.*?)\s*", + re.DOTALL, +) +_QWEN3_CODER_PARAMETER_RE = re.compile( + r"[^>\s]+)>(?P.*?)", + re.DOTALL, +) + _SGLANG_TO_VLLM_TOOL_PARSER: dict[str, str] = { "qwen": "qwen3_xml", "qwen25": "qwen3_xml", @@ -58,6 +73,187 @@ def _detect_think_and_return_ori_think( return think_start_token + reasoning_text + think_end_token, normal_text +def _iter_tool_definitions(tools: list[Any]) -> list[dict[str, Any]]: + """Return OpenAI chat/responses tool definitions in a common shape.""" + tool_defs: list[dict[str, Any]] = [] + for tool in tools: + if not isinstance(tool, dict): + continue + if isinstance(tool.get("function"), dict): + tool_defs.append(tool["function"]) + elif tool.get("type") == "function": + tool_defs.append(tool) + return tool_defs + + +def _tool_argument_schemas(tools: list[Any]) -> dict[str, dict[str, dict[str, Any]]]: + schemas: dict[str, dict[str, dict[str, Any]]] = {} + for tool_def in _iter_tool_definitions(tools): + name = tool_def.get("name") + parameters = tool_def.get("parameters") + if not isinstance(name, str) or not isinstance(parameters, dict): + continue + properties = parameters.get("properties") + if isinstance(properties, dict): + schemas[name] = { + key: value + for key, value in properties.items() + if isinstance(value, dict) + } + return schemas + + +def _clean_qwen3_coder_parameter(raw_value: str) -> str: + if raw_value.startswith("\n"): + raw_value = raw_value[1:] + if raw_value.endswith("\n"): + raw_value = raw_value[:-1] + return raw_value + + +def _coerce_qwen3_coder_parameter( + value: str, + schema: dict[str, Any] | None, +) -> Any: + if not schema: + return value + + typ = schema.get("type") + if isinstance(typ, list): + typ = next((t for t in typ if t != "null"), None) + stripped = value.strip() + + try: + if typ == "boolean": + lowered = stripped.lower() + if lowered == "true": + return True + if lowered == "false": + return False + if typ == "integer": + return int(stripped) + if typ == "number": + return float(stripped) + if typ in {"array", "object"}: + return json.loads(stripped) + except (TypeError, ValueError, json.JSONDecodeError): + return value + + return value + + +def _build_tool_call( + name: str, + arguments: str, + use_responses: bool, +) -> ChatCompletionMessageFunctionToolCall | ResponseFunctionToolCall: + if use_responses: + return ResponseFunctionToolCall( + type="function_call", + id=f"fc-{uuid.uuid4().hex[:24]}", + call_id=f"call_{uuid.uuid4().hex[:24]}", + name=name, + arguments=arguments, + status="completed", + ) + return ChatCompletionMessageFunctionToolCall( + type="function", + id=f"call_{uuid.uuid4().hex[:24]}", + function=Function(name=name, arguments=arguments), + ) + + +def _process_tool_calls_qwen3_coder_xml( + text: str, + tools: list[Any], + finish_reason: str, + use_responses: bool = False, +) -> tuple[ + list[ChatCompletionMessageFunctionToolCall | ResponseFunctionToolCall] | None, + str, + str, +]: + """Parse Qwen3-Coder XML tool calls with ```` tags. + + SGLang's qwen3_coder parser recognizes the tool name in this format but + can return empty arguments for Claude Code style parameters. That makes + Anthropic tool_use.input become ``{}``, so Claude Code rejects calls like + Bash/Read/Write as missing required parameters. + """ + + reasoning_text, content_text = _detect_think_and_return_ori_think( + text, "", "" + ) + arg_schemas = _tool_argument_schemas(tools) + + tool_calls: list[ChatCompletionMessageFunctionToolCall | ResponseFunctionToolCall] + tool_calls = [] + remove_spans: list[tuple[int, int]] = [] + + for tool_match in _QWEN3_CODER_TOOL_CALL_RE.finditer(content_text): + block_calls: list[ + ChatCompletionMessageFunctionToolCall | ResponseFunctionToolCall + ] = [] + body = tool_match.group("body") + for fn_match in _QWEN3_CODER_FUNCTION_RE.finditer(body): + tool_name = fn_match.group("name") + fn_body = fn_match.group("body") + # A parameter value that itself contains a literal "" + # makes the non-greedy regex truncate at the wrong tag, silently + # producing wrong arguments. When opening/closing tags are unbalanced + # the block is ambiguous, so bail out and let the fallback parser run. + if fn_body.count(""): + return None, text, finish_reason + args: dict[str, Any] = {} + for param_match in _QWEN3_CODER_PARAMETER_RE.finditer(fn_body): + param_name = param_match.group("name") + raw_param_value = param_match.group("value") + # A balanced count of tags still hides nested pairs, e.g. a + # value that contains its own "...". + # The non-greedy regex truncates the outer value at the inner + # closing tag, so treat any parameter marker inside the value as + # ambiguous and fall back to the backend parser. + if ( + "" in raw_param_value + ): + return None, text, finish_reason + param_value = _clean_qwen3_coder_parameter(raw_param_value) + args[param_name] = _coerce_qwen3_coder_parameter( + param_value, + arg_schemas.get(tool_name, {}).get(param_name), + ) + + # No parsed arguments means either a genuinely empty block or a + # truncated/malformed one. Skip it so process_tool_calls falls back + # to the sglang parser instead of committing empty arguments. + if not args: + continue + + arguments = json.dumps(args, ensure_ascii=False) + block_calls.append(_build_tool_call(tool_name, arguments, use_responses)) + + if block_calls: + tool_calls.extend(block_calls) + remove_spans.append(tool_match.span()) + + if not tool_calls: + return None, text, finish_reason + + if finish_reason == "stop": + finish_reason = "tool_calls" + + chunks: list[str] = [] + last = 0 + for start, end in remove_spans: + chunks.append(content_text[last:start]) + last = end + chunks.append(content_text[last:]) + cleaned_text = "".join(chunks).replace("<|im_end|>", "") + + return tool_calls, reasoning_text + cleaned_text, finish_reason + + def _process_tool_calls_sglang( text: str, tools: list[Any], @@ -267,6 +463,16 @@ def process_tool_calls( str, ]: """Process tool calls in the response""" + if tool_call_parser == "qwen3_coder": + tool_calls, output_text, finish_reason = _process_tool_calls_qwen3_coder_xml( + text, + tools, + finish_reason, + use_responses, + ) + if tool_calls is not None: + return tool_calls, output_text, finish_reason + try: return _process_tool_calls_sglang( text, diff --git a/docs/en/cli_reference.md b/docs/en/cli_reference.md index 14008390dd..5e4b9ca5d6 100644 --- a/docs/en/cli_reference.md +++ b/docs/en/cli_reference.md @@ -899,6 +899,8 @@ Consolidates proxy settings (mode, parsers, export) with agent-service orchestra | `engine_max_tokens` | integer \| None | `None` | Maximum total tokens for the engine (prompt + completion). | | `turn_discount` | float | `1.0` | Discount factor for multi-turn reward propagation. | | `export_style` | string | `"individual"` | Export style: 'individual' (all interactions) or 'concat' (leaf nodes only). The 'individual' style exports each interaction (input-output-reward) step separately, and treats them as independent samples to train the model. The 'concat' style exports only the final concatenated trajectory from the root. It is only suitable for linear conversation histories without token mismatching (whether valid depends on the tokenizer). **Choices:** `individual`, `concat` | +| `message_preprocessors` | list of string | **Required** | List of message preprocessor class paths applied, in order, to Anthropic-compatible `/v1/messages` requests after translating them to OpenAI-compatible requests. Native OpenAI `/chat/completions` and `/responses` requests are not preprocessed. Each entry is a dotted import path to a callable class. | +| `prefix_matcher` | string \| None | `None` | Dotted import path to a custom prefix matcher function for InteractionCache parent-child matching. The function must accept two list\[dict\] arguments (candidate prefix, full messages) and return bool. When None, exact element-wise equality is used. | | `subproc_max_workers` | integer | `4` | Maximum number of worker processes for subprocess mode execution pool. | | `session_timeout_seconds` | integer | `3600` | Session timeout in seconds. Sessions inactive longer than this will be garbage collected. | | `set_reward_finish_timeout` | float | `0.0` | Timeout in seconds to wait for additional reward updates before finalizing a session. | diff --git a/docs/zh/cli_reference.md b/docs/zh/cli_reference.md index f2951ecbe5..53a04daa75 100644 --- a/docs/zh/cli_reference.md +++ b/docs/zh/cli_reference.md @@ -897,6 +897,8 @@ Consolidates proxy settings (mode, parsers, export) with agent-service orchestra | `engine_max_tokens` | integer \| None | `None` | Maximum total tokens for the engine (prompt + completion). | | `turn_discount` | float | `1.0` | Discount factor for multi-turn reward propagation. | | `export_style` | string | `"individual"` | Export style: 'individual' (all interactions) or 'concat' (leaf nodes only). The 'individual' style exports each interaction (input-output-reward) step separately, and treats them as independent samples to train the model. The 'concat' style exports only the final concatenated trajectory from the root. It is only suitable for linear conversation histories without token mismatching (whether valid depends on the tokenizer). **Choices:** `individual`, `concat` | +| `message_preprocessors` | list of string | **Required** | List of message preprocessor class paths applied, in order, to Anthropic-compatible `/v1/messages` requests after translating them to OpenAI-compatible requests. Native OpenAI `/chat/completions` and `/responses` requests are not preprocessed. Each entry is a dotted import path to a callable class. | +| `prefix_matcher` | string \| None | `None` | Dotted import path to a custom prefix matcher function for InteractionCache parent-child matching. The function must accept two list\[dict\] arguments (candidate prefix, full messages) and return bool. When None, exact element-wise equality is used. | | `subproc_max_workers` | integer | `4` | Maximum number of worker processes for subprocess mode execution pool. | | `session_timeout_seconds` | integer | `3600` | Session timeout in seconds. Sessions inactive longer than this will be garbage collected. | | `set_reward_finish_timeout` | float | `0.0` | Timeout in seconds to wait for additional reward updates before finalizing a session. | diff --git a/tests/experimental/openai/test_client_with_tool.py b/tests/experimental/openai/test_client_with_tool.py index f53dfdddb5..50c0465cdb 100644 --- a/tests/experimental/openai/test_client_with_tool.py +++ b/tests/experimental/openai/test_client_with_tool.py @@ -232,16 +232,23 @@ async def test_streaming_with_tool_calling(openai_client): and len(chunk.choices[0].delta.tool_calls) > 0 ] - # Verify tool calls are present and properly structured + # Verify tool calls are present and split into name/id and arguments chunks. if tool_call_chunks: - for chunk in tool_call_chunks: - for tool_call in chunk.choices[0].delta.tool_calls: - # Verify tool_call has required fields (simplified access after our fix) - assert tool_call.id is not None - assert tool_call.type == "function" - assert tool_call.function is not None - assert tool_call.function.name is not None - assert tool_call.function.arguments is not None + assert len(tool_call_chunks) % 2 == 0 + for first_chunk, second_chunk in zip( + tool_call_chunks[::2], tool_call_chunks[1::2], strict=True + ): + first_tool_call = first_chunk.choices[0].delta.tool_calls[0] + second_tool_call = second_chunk.choices[0].delta.tool_calls[0] + assert first_tool_call.id is not None + assert first_tool_call.type == "function" + assert first_tool_call.function is not None + assert first_tool_call.function.name is not None + assert first_tool_call.function.arguments == "" + assert second_tool_call.index == first_tool_call.index + assert second_tool_call.function is not None + assert second_tool_call.function.arguments is not None + assert len(second_tool_call.function.arguments) > 0 # Find the final chunk with finish_reason final_chunks = [ diff --git a/tests/experimental/openai/test_resolve_max_total_tokens.py b/tests/experimental/openai/test_resolve_max_total_tokens.py new file mode 100644 index 0000000000..826e6f2062 --- /dev/null +++ b/tests/experimental/openai/test_resolve_max_total_tokens.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 + +from areal.experimental.openai.client import ( + _DEFAULT_MAX_TOTAL_TOKENS, + _resolve_max_total_tokens, +) + + +def test_engine_max_tokens_caps_total_length(): + out = _resolve_max_total_tokens( + prompt_len=1000, + max_new_tokens=100_000, + engine_max_tokens=4096, + ) + + assert out == 4096 + + +def test_fallback_ceiling_applies_when_engine_max_tokens_is_none(): + out = _resolve_max_total_tokens( + prompt_len=1000, + max_new_tokens=100_000, + engine_max_tokens=None, + ) + + assert out == _DEFAULT_MAX_TOTAL_TOKENS + + +def test_returns_prompt_plus_generation_when_under_cap(): + out = _resolve_max_total_tokens( + prompt_len=100, + max_new_tokens=200, + engine_max_tokens=4096, + ) + + assert out == 300 diff --git a/tests/experimental/openai/test_tool_call_parser.py b/tests/experimental/openai/test_tool_call_parser.py index 4d3da68501..c08c4aebe3 100644 --- a/tests/experimental/openai/test_tool_call_parser.py +++ b/tests/experimental/openai/test_tool_call_parser.py @@ -1,3 +1,4 @@ +import json from types import SimpleNamespace import pytest @@ -69,6 +70,52 @@ '\n{"name": "search", "arguments": {"query": "director of The Preacher\'s Wife country"}}\n<|im_end|>' ) +QWEN3_CODER_TOOLS: list[ChatCompletionToolParam] = [ + { + "type": "function", + "function": { + "name": "Bash", + "description": "Run a shell command.", + "parameters": { + "type": "object", + "properties": { + "command": {"type": "string"}, + "timeout": {"type": "integer"}, + }, + "required": ["command"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "Write", + "description": "Write a file.", + "parameters": { + "type": "object", + "properties": { + "file_path": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["file_path", "content"], + }, + }, + }, +] + +QWEN3_CODER_TEXT = """I'll inspect the repository. + + + + +ls -la + + +120 + + +<|im_end|>""" + def _assert_tool_calls(tool_calls, new_text: str, new_finish_reason: str) -> None: assert new_finish_reason == "tool_calls" @@ -101,6 +148,67 @@ def _run_process_tool_calls(text: str): ) +def test_process_tool_calls_qwen3_coder_xml_parameters_without_sglang(): + tool_calls, new_text, new_finish_reason = parser_module.process_tool_calls( + text=QWEN3_CODER_TEXT, + tools=QWEN3_CODER_TOOLS, + tool_call_parser="qwen3_coder", + reasoning_parser="qwen3", + finish_reason="stop", + use_responses=False, + tokenizer=object(), + ) + + assert new_finish_reason == "tool_calls" + assert tool_calls is not None + assert len(tool_calls) == 1 + assert tool_calls[0].function.name == "Bash" + assert json.loads(tool_calls[0].function.arguments) == { + "command": "ls -la", + "timeout": 120, + } + assert "" not in new_text + assert "<|im_end|>" not in new_text + assert "I'll inspect the repository." in new_text + + +def test_qwen3_coder_xml_empty_args_returns_no_parse(): + text = "\n\n\n" + + tool_calls, new_text, finish_reason = ( + parser_module._process_tool_calls_qwen3_coder_xml( + text=text, + tools=QWEN3_CODER_TOOLS, + finish_reason="stop", + ) + ) + + assert tool_calls is None + assert new_text == text + assert finish_reason == "stop" + + +def test_qwen3_coder_xml_literal_closing_tag_is_not_silently_truncated(): + text = ( + "\n\n" + "\na.xml\n\n" + "\nsome XML text: more content\n\n" + "\n" + ) + + tool_calls, new_text, finish_reason = ( + parser_module._process_tool_calls_qwen3_coder_xml( + text=text, + tools=QWEN3_CODER_TOOLS, + finish_reason="stop", + ) + ) + + assert tool_calls is None + assert new_text == text + assert finish_reason == "stop" + + @pytest.mark.sglang def test_process_tool_calls_qwen25_chat_completions_sglang(): pytest.importorskip( From 7c1d22d359742d19a4661e7c22b92f70a579a3ce Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:36:03 +0800 Subject: [PATCH 30/36] test(ppo): update singleton leave-one-out group expectation (#1464) After #1454, a size-1 leave-one-out group uses the sample itself as the baseline and normalizes to zero instead of passing through the raw advantage. Update test_group_size_edge_cases to assert the new behavior. --- tests/test_adv_norm_config.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_adv_norm_config.py b/tests/test_adv_norm_config.py index fb1553fee4..6e9adf8b14 100644 --- a/tests/test_adv_norm_config.py +++ b/tests/test_adv_norm_config.py @@ -874,11 +874,15 @@ def test_group_size_edge_cases(): adv_norm = Normalization(config) normalized = adv_norm(advantages, loss_mask) - # With group size 1 and leave-one-out, each element should remain approximately unchanged - # because mean=0 (no other elements) and std=1 (for stability), so result ≈ (x-0)/1 = x + + # With singleton leave-one-out groups, there is no peer baseline. + # We use the sample itself as the baseline, so each singleton group + # normalizes to zero instead of passing through the raw advantage. assert torch.allclose( - normalized, advantages, atol=1e-3 - ) # Should remain approximately unchanged + normalized, + torch.zeros_like(advantages), + atol=1e-6, + ) def test_precision_and_numerical_stability(): From 77458d0d22c3be6205ed8ba569d7e001999f6cfc Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:07:21 +0800 Subject: [PATCH 31/36] chore: update AReaL 2.0 report paper (#1465) --- docs/paper/AReaL2.0_report.pdf | Bin 1498405 -> 1498427 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/paper/AReaL2.0_report.pdf b/docs/paper/AReaL2.0_report.pdf index d0b1f0247a3a3afd799f77e24d758e01cdb7f372..cb720dbf88d880c7f277ad962c84a8a4fbb0d62f 100644 GIT binary patch delta 5453 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 delta 5414 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 From fee938eada49208a5aabdbc1095730a13076a349 Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:12:18 +0800 Subject: [PATCH 32/36] chore: bump v2.0.0 (#1466) --- .github/workflows/tag-release-image.yml | 2 +- docs/en/tutorial/installation.md | 8 ++++---- docs/zh/tutorial/installation.md | 8 ++++---- examples/skypilot/README.md | 4 ++-- examples/skypilot/ray_cluster.sky.yaml | 2 +- examples/skypilot/single_node.sky.yaml | 2 +- pyproject.toml | 2 +- pyproject.vllm.toml | 2 +- uv.lock | 2 +- uv.vllm.lock | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tag-release-image.yml b/.github/workflows/tag-release-image.yml index 0c3589d8b7..18651a4bc2 100644 --- a/.github/workflows/tag-release-image.yml +++ b/.github/workflows/tag-release-image.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: tag: - description: 'Release tag to build (e.g., v1.0.4-sglang)' + description: 'Release tag to build (e.g., v2.0.0-sglang)' required: true type: string diff --git a/docs/en/tutorial/installation.md b/docs/en/tutorial/installation.md index 8e2a4de9ca..cd6a846023 100644 --- a/docs/en/tutorial/installation.md +++ b/docs/en/tutorial/installation.md @@ -24,7 +24,7 @@ The following hardware configuration has been extensively tested: | Git LFS | Required for downloading models, datasets, and AReaL code. See [installation guide](https://docs.github.com/en/repositories/working-with-files/managing-large-files/installing-git-large-file-storage) | | Docker | 27.5.1 | | NVIDIA Container Toolkit | See [installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) | -| AReaL Image | `ghcr.io/areal-project/areal-runtime:v1.0.4-sglang` (default) or `v1.0.4-vllm`. Includes runtime dependencies and Ray components. | +| AReaL Image | `ghcr.io/areal-project/areal-runtime:v2.0.0-sglang` (default) or `v2.0.0-vllm`. Includes runtime dependencies and Ray components. | **Note**: This tutorial does not cover the installation of NVIDIA Drivers, CUDA, or shared storage mounting, as these depend on your specific node configuration and system @@ -42,11 +42,11 @@ We recommend using Docker with our provided image. The Dockerfile is available i top-level directory of the AReaL repository. ```bash -docker pull ghcr.io/areal-project/areal-runtime:v1.0.4-sglang +docker pull ghcr.io/areal-project/areal-runtime:v2.0.0-sglang docker run -it --name areal-node1 \ --privileged --gpus all --network host \ --shm-size 700g -v /path/to/mount:/path/to/mount \ - ghcr.io/areal-project/areal-runtime:v1.0.4-sglang \ + ghcr.io/areal-project/areal-runtime:v2.0.0-sglang \ /bin/bash git clone https://github.com/areal-project/AReaL /path/to/mount/AReaL cd /path/to/mount/AReaL @@ -54,7 +54,7 @@ uv pip install -e . --no-deps ``` A vLLM variant of the Docker image is also available at -`ghcr.io/areal-project/areal-runtime:v1.0.4-vllm`. Replace the image tag in the commands +`ghcr.io/areal-project/areal-runtime:v2.0.0-vllm`. Replace the image tag in the commands above if you prefer vLLM as the inference backend. ### Option 2: Custom Environment Installation diff --git a/docs/zh/tutorial/installation.md b/docs/zh/tutorial/installation.md index ca6ce017e1..3027291164 100644 --- a/docs/zh/tutorial/installation.md +++ b/docs/zh/tutorial/installation.md @@ -24,7 +24,7 @@ | Git LFS | 用于下载模型、数据集和 AReaL 代码。请参阅[安装指南](https://docs.github.com/en/repositories/working-with-files/managing-large-files/installing-git-large-file-storage) | | Docker | 27.5.1 | | NVIDIA Container Toolkit | 请参阅[安装指南](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) | -| AReaL 镜像 | `ghcr.io/areal-project/areal-runtime:v1.0.4-sglang`(默认)或 `v1.0.4-vllm`。包含运行时依赖和 Ray 组件。 | +| AReaL 镜像 | `ghcr.io/areal-project/areal-runtime:v2.0.0-sglang`(默认)或 `v2.0.0-vllm`。包含运行时依赖和 Ray 组件。 | **注意**:本教程不涵盖 NVIDIA 驱动、CUDA 或共享存储挂载的安装,因为这些取决于您具体的节点配置和系统版本。请独立完成这些安装。 @@ -37,18 +37,18 @@ 我们推荐使用 Docker 和提供的镜像。Dockerfile 位于 AReaL 仓库的顶级目录。 ```bash -docker pull ghcr.io/areal-project/areal-runtime:v1.0.4-sglang +docker pull ghcr.io/areal-project/areal-runtime:v2.0.0-sglang docker run -it --name areal-node1 \ --privileged --gpus all --network host \ --shm-size 700g -v /path/to/mount:/path/to/mount \ - ghcr.io/areal-project/areal-runtime:v1.0.4-sglang \ + ghcr.io/areal-project/areal-runtime:v2.0.0-sglang \ /bin/bash git clone https://github.com/areal-project/AReaL /path/to/mount/AReaL cd /path/to/mount/AReaL uv pip install -e . --no-deps ``` -vLLM 变体的 Docker 镜像也可使用: `ghcr.io/areal-project/areal-runtime:v1.0.4-vllm`。如果您偏好使用 vLLM +vLLM 变体的 Docker 镜像也可使用: `ghcr.io/areal-project/areal-runtime:v2.0.0-vllm`。如果您偏好使用 vLLM 作为推理后端,请将上述命令中的镜像标签替换为该变体。 ### 方式 2:自定义环境安装 diff --git a/examples/skypilot/README.md b/examples/skypilot/README.md index c358d0634e..74e152acfa 100644 --- a/examples/skypilot/README.md +++ b/examples/skypilot/README.md @@ -25,7 +25,7 @@ resources: cpus: 8+ memory: 32GB+ disk_size: 256GB - image_id: docker:ghcr.io/areal-project/areal-runtime:v1.0.4-sglang + image_id: docker:ghcr.io/areal-project/areal-runtime:v2.0.0-sglang num_nodes: 1 @@ -78,7 +78,7 @@ Specify the resources and image used to run the experiment. ```yaml resources: accelerators: A100:8 - image_id: docker:ghcr.io/areal-project/areal-runtime:v1.0.4-sglang + image_id: docker:ghcr.io/areal-project/areal-runtime:v2.0.0-sglang memory: 256+ cpus: 32+ diff --git a/examples/skypilot/ray_cluster.sky.yaml b/examples/skypilot/ray_cluster.sky.yaml index 341f7c7eeb..e79f838397 100644 --- a/examples/skypilot/ray_cluster.sky.yaml +++ b/examples/skypilot/ray_cluster.sky.yaml @@ -1,7 +1,7 @@ resources: accelerators: A100:8 - image_id: docker:ghcr.io/areal-project/areal-runtime:v1.0.4-sglang + image_id: docker:ghcr.io/areal-project/areal-runtime:v2.0.0-sglang memory: 32+ cpus: 8+ diff --git a/examples/skypilot/single_node.sky.yaml b/examples/skypilot/single_node.sky.yaml index 1bd90b8518..bf4c7f29cf 100644 --- a/examples/skypilot/single_node.sky.yaml +++ b/examples/skypilot/single_node.sky.yaml @@ -8,7 +8,7 @@ resources: cpus: 8+ memory: 32GB+ disk_size: 256GB - image_id: docker:ghcr.io/areal-project/areal-runtime:v1.0.4-sglang + image_id: docker:ghcr.io/areal-project/areal-runtime:v2.0.0-sglang num_nodes: 1 diff --git a/pyproject.toml b/pyproject.toml index 4ea91a4391..b325587691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "AReaL: A Large-Scale Asynchronous Reinforcement Learning System" readme = "README.md" license = {text = "Apache-2.0"} requires-python = ">=3.11,<3.13" -version = "1.0.4" +version = "2.0.0" authors = [ {name = "AReaL Team"}, ] diff --git a/pyproject.vllm.toml b/pyproject.vllm.toml index 572a213d5f..c71d4049d0 100644 --- a/pyproject.vllm.toml +++ b/pyproject.vllm.toml @@ -24,7 +24,7 @@ description = "AReaL: A Large-Scale Asynchronous Reinforcement Learning System" readme = "README.md" license = {text = "Apache-2.0"} requires-python = ">=3.11,<3.13" -version = "1.0.4" +version = "2.0.0" authors = [ {name = "AReaL Team"}, ] diff --git a/uv.lock b/uv.lock index 389832ba4e..3ee372d5e1 100644 --- a/uv.lock +++ b/uv.lock @@ -308,7 +308,7 @@ wheels = [ [[package]] name = "areal" -version = "1.0.4" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "aiofiles", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, diff --git a/uv.vllm.lock b/uv.vllm.lock index bbd554432d..5cbac42895 100644 --- a/uv.vllm.lock +++ b/uv.vllm.lock @@ -347,7 +347,7 @@ wheels = [ [[package]] name = "areal" -version = "1.0.4" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "aiofiles", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, From 90cb574b3ae8377106979f2dbf9b802fae2ac941 Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:09:22 +0800 Subject: [PATCH 33/36] chore(swe): remove unused SWE SFT dataset loader (#1467) swe_sft.py is an SFT-only dataset builder that is not used by the SWE-bench RL training example (examples/swe/train_swe_rl.py loads raw problem instances directly) and is referenced only by its own dataset registration. Remove the module and its registration so the RL example does not ship unused SFT data-processing code. --- areal/dataset/__init__.py | 11 - areal/dataset/swe_sft.py | 2559 ------------------------------------- 2 files changed, 2570 deletions(-) delete mode 100644 areal/dataset/swe_sft.py diff --git a/areal/dataset/__init__.py b/areal/dataset/__init__.py index 5e06d98681..1ab23a4cb9 100644 --- a/areal/dataset/__init__.py +++ b/areal/dataset/__init__.py @@ -19,7 +19,6 @@ "virl39k", "hh-rlhf", "torl_data", - "swe_sft", ] logger = logging.getLogger("Dataset") @@ -134,16 +133,6 @@ def _get_custom_dataset( max_length=max_length, **kwargs, ) - elif "swe" in path.lower() and type == "sft": - from .swe_sft import get_swe_sft_dataset - - return get_swe_sft_dataset( - path=path, - split=split, - tokenizer=tokenizer, - max_length=max_length, - **kwargs, - ) else: # Fallback: try loading as a generic HuggingFace dataset from disk. # This supports arbitrary datasets saved via dataset.save_to_disk(). diff --git a/areal/dataset/swe_sft.py b/areal/dataset/swe_sft.py deleted file mode 100644 index 20dd14ad61..0000000000 --- a/areal/dataset/swe_sft.py +++ /dev/null @@ -1,2559 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -"""SWE SFT dataset loader. - -Loads SWE-bench trajectory data and converts it into progressive SFT -training pairs. Each trajectory is split at assistant-turn boundaries -so that every pair ends with an assistant segment (assistant message + -its subsequent tool responses). - -Example trajectory:: - - [system, user, asst1, tool1a, tool1b, asst2, tool2, asst3] - -Produces three pairs:: - - Pair 1: [system, user, asst1, tool1a, tool1b] - Pair 2: [system, user, asst1, tool1a, tool1b, asst2, tool2] - Pair 3: [system, user, asst1, tool1a, tool1b, asst2, tool2, asst3] - -In each pair, only the **last** assistant segment is trained (loss=1); -earlier assistant turns are treated as context (loss=0). - -By default, pairs whose current segment contains a tool result with -``is_error=True`` are discarded. Set ``filter_errors=False`` to keep them. - -The file is organized into the following sections: - -1. **Constants & Infrastructure** — shared constants, distributed sync -2. **Cleaning** — message content transforms (thinking tags, field cleanup) -3. **Filters** — keep/discard predicates (error, empty, bare-text, truncation) -4. **Splitting** — trajectory → progressive pairs (segment detection + split) -5. **Tokenization** — template detection, render→tokenize→loss_mask, dump -6. **Pipeline** — loading, processing, distributed cache, public API -7. **CLI** — ``python -m areal.dataset.swe_sft`` entry point -""" - -import json -import os -import random -import re -import time - -from datasets import Dataset - -from areal.utils import logging - -logger = logging.getLogger("SWESFTDataset") - - -# ============================================================ -# 1. Constants & Infrastructure -# ============================================================ - -DATASET_NUM_PROC = 1 - -# Timeout (seconds) for non-rank-0 workers waiting for rank 0 to finish -# dataset processing. Large datasets with tokenization can take minutes; -# 30 min is a generous upper bound. -_RANK0_CACHE_TIMEOUT = 36000 -_RANK0_CACHE_POLL_INTERVAL = 5 - - -def _wait_for_marker(marker_path: str): - """Block until *marker_path* exists on disk, with timeout.""" - start = time.monotonic() - while not os.path.exists(marker_path): - elapsed = time.monotonic() - start - if elapsed > _RANK0_CACHE_TIMEOUT: - raise TimeoutError( - f"Waited {_RANK0_CACHE_TIMEOUT}s for rank 0 to finish dataset " - f"processing (marker: {marker_path}). Check rank 0 logs." - ) - time.sleep(_RANK0_CACHE_POLL_INTERVAL) - - -def _extract_messages(record, record_idx): - """Extract messages and tools from a parsed JSONL record. - - Handles nested (``conversations`` wrapper) and flat formats. - Warns if multiple conversations are present. - - Returns: - Tuple of ``(messages, record_tools)``. *messages* may be empty. - """ - convs = record.get("conversations", []) - if convs: - if len(convs) > 1: - logger.warning( - "Record %d has %d conversations, using only the last one.", - record_idx, - len(convs), - ) - conv = convs[-1] - return conv.get("messages", []), conv.get("tools") - return record.get("messages", []), record.get("tools") - - -def _set_messages(record, messages): - """Write *messages* back into *record* (inverse of ``_extract_messages``). - - Used by the ``--save-trajectories`` CLI path to update truncated - messages in the original record structure before serialization. - """ - convs = record.get("conversations", []) - if convs: - convs[-1]["messages"] = messages - else: - record["messages"] = messages - - -def _iter_jsonl_records(path): - """Iterate trajectory JSONL records. - - Yields ``(record_idx, messages, record_tools)`` tuples. Handles - nested (``conversations`` wrapper) vs flat format auto-detection - via ``_extract_messages``. Records with empty messages are skipped. - - Warns about multi-user trajectories which break think-tag rendering - in templates with ``ns.last_query_index`` logic (e.g. Bailing). - """ - record_idx = 0 - n_multi_user = 0 - with open(path, encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line: - continue - record = json.loads(line) - record_idx += 1 - messages, record_tools = _extract_messages(record, record_idx) - if not messages: - continue - n_user = sum(1 for m in messages if m.get("role") == "user") - if n_user > 1: - n_multi_user += 1 - if n_multi_user <= 3: - logger.warning( - "Record %d has %d user messages. Templates with " - "ns.last_query_index logic (e.g. Bailing) will NOT " - "render for assistant turns before the last " - "user message. Consider filtering!", - record_idx, - n_user, - ) - yield record_idx, messages, record_tools - if n_multi_user > 0: - logger.warning( - "Total %d/%d records have multiple user messages. " - "These may produce no-think training signal.", - n_multi_user, - record_idx, - ) - - -# ============================================================ -# 2. Cleaning — message content transforms -# ============================================================ - -# Match reasoning blocks with any common tag variant: -# ... (Qwen standard) -# ... (Claude) -# The opening and closing tag names need not match exactly — mixed pairs -# like ``...`` (seen in distillation data) are handled. -_THINK_OPEN_RE = re.compile(r"") -_THINK_CLOSE_RE = re.compile(r"") -_THINK_RE = re.compile(r"(.*?)", re.DOTALL) - - -def _normalize_thinking_tags(content): - """Normalise all thinking tag variants to ````/````. - - Distillation data from different models may use ```` (Claude) - vs ```` (Qwen). Non-standard variants are multi-token for the - Qwen tokenizer which breaks think/tool_call boundaries. - """ - if not content: - return content - content = _THINK_OPEN_RE.sub("", content) - content = _THINK_CLOSE_RE.sub("", content) - return content - - -def _extract_thinking(content): - """Strip thinking blocks from *content*. - - Callers must run ``_normalize_thinking_tags`` first so that all - tag variants have been converted to ````/````. - - Returns: - Cleaned content with thinking blocks removed, or the original - content unchanged if no thinking tags are found. - """ - if not content: - return content - cleaned = _THINK_RE.sub("", content).strip() - return cleaned if cleaned != content.strip() else content - - -def _clean_message(msg, strip_thinking=True, ensure_thinking=False): - """Remove non-standard fields before tokenization. - - Keeps only the fields expected by tokenizer chat templates: - role, content, reasoning_content (for assistant), tool_calls - (for assistant), tool_call_id (for tool). - - Handles thinking content in two representations: - - - Inline ``...`` tags in ``content`` - - Separate ``reasoning_content`` field (DeepSeek, Qwen3 API style) - - If both are present, inline tags take priority and - ``reasoning_content`` is dropped with a warning to avoid double - thinking blocks in the rendered template. - - Args: - msg: Raw message dict. - strip_thinking: If True, remove thinking from assistant messages - (both inline ```` tags and ``reasoning_content``). - Used for context turns. If False, preserve thinking as-is - (used for the training-target assistant turn). - ensure_thinking: If True, inject inline ``\n`` - on assistant turns that lack a thinking block (either - inline or in ``reasoning_content``). Requires the - patched Bailing template (via - ``_patch_chat_template_for_training``) which detects - ``had_think_tags`` and preserves empty think blocks. - """ - cleaned = {"role": msg["role"]} - - # Handle content — some assistant messages have content=None when - # they only contain tool_calls. Preserve None so chat templates - # that distinguish None vs "" render correctly. - content = msg.get("content") - # Some APIs (DeepSeek, Qwen3 with enable_thinking) return thinking - # in a separate ``reasoning_content`` field instead of inline - # ```` tags. Handle both representations. - raw_reasoning = msg.get("reasoning_content") if msg["role"] == "assistant" else None - has_thinking = False - if content is not None: - if msg["role"] == "assistant": - content = _normalize_thinking_tags(content) - has_inline_thinking = bool(_THINK_RE.search(content)) - if has_inline_thinking and raw_reasoning and raw_reasoning.strip(): - # Conflict: both reasoning_content and inline tags. - # Keep inline tags (they are already in the content the - # tokenizer will see) and drop reasoning_content to avoid - # double thinking blocks in the rendered template. - if not strip_thinking: - logger.warning( - "Message has both reasoning_content and inline " - " tags. Keeping inline tags, dropping " - "reasoning_content." - ) - raw_reasoning = None - elif not has_inline_thinking and raw_reasoning and raw_reasoning.strip(): - # Convert reasoning_content → inline in content. - # This ensures a single representation that templates - # render identically to the reasoning_content path, while - # being more transparent and debuggable. - if not strip_thinking: - content = ( - f"\n{raw_reasoning.strip(chr(10))}\n" - f"\n\n{content.lstrip(chr(10))}" - ) - has_inline_thinking = True - raw_reasoning = None - has_thinking = has_inline_thinking or bool( - raw_reasoning and raw_reasoning.strip() - ) - if strip_thinking: - content = _extract_thinking(content) - cleaned["content"] = content - elif msg["role"] == "assistant" and msg.get("tool_calls"): - # Assistant with tool_calls but content=None. - if raw_reasoning and raw_reasoning.strip(): - has_thinking = True - if not strip_thinking: - # Convert reasoning_content → inline in content. - cleaned["content"] = ( - f"\n{raw_reasoning.strip(chr(10))}\n" - ) - else: - cleaned["content"] = None - raw_reasoning = None - else: - cleaned["content"] = None - else: - # Non-assistant messages without content: default to empty string. - cleaned["content"] = "" - - # Preserve reasoning_content for target turns only when it was NOT - # already inlined above (i.e. only when raw_reasoning is still set). - if not strip_thinking and raw_reasoning is not None: - cleaned["reasoning_content"] = raw_reasoning - - # For the target assistant turn without a thinking block, inject - # inline ``\n`` so that the (patched) template detects - # think intent via ``had_think_tags`` and renders - # ``\n\n\n\n`` — identical token output to the old - # ``reasoning_content='\n'`` approach. - # - # Requires ``_patch_chat_template_for_training`` to have been called - # on the tokenizer, otherwise the stock Bailing template will extract - # and discard the empty ```` block. - if ensure_thinking and msg["role"] == "assistant" and not has_thinking: - cur_content = cleaned.get("content") - if cur_content is None or cur_content == "": - cleaned["content"] = "\n" - else: - cleaned["content"] = f"\n\n\n{cur_content.lstrip(chr(10))}" - - # Copy tool_calls for assistant messages - if msg["role"] == "assistant" and msg.get("tool_calls"): - cleaned_tool_calls = [] - for tc in msg["tool_calls"]: - cleaned_tc = { - "type": tc.get("type", "function"), - "function": { - "name": tc["function"]["name"], - "arguments": json.dumps(tc["function"]["arguments"]) - if isinstance(tc["function"]["arguments"], dict) - else tc["function"]["arguments"], - }, - } - if "id" in tc: - cleaned_tc["id"] = tc["id"] - cleaned_tool_calls.append(cleaned_tc) - cleaned["tool_calls"] = cleaned_tool_calls - - # Copy tool_call_id for tool messages - if msg["role"] == "tool" and msg.get("tool_call_id"): - cleaned["tool_call_id"] = msg["tool_call_id"] - - return cleaned - - -# ============================================================ -# 3. Filters — keep/discard predicates -# ============================================================ - - -def _segment_has_error(messages, start, end): - """Check if any tool message in ``messages[start:end]`` has ``is_error=True``.""" - for m in messages[start:end]: - if m.get("role") == "tool" and m.get("is_error") is True: - return True - return False - - -def _is_empty_tool_call(msg): - """True if assistant *msg* has no text content and no reasoning but has tool_calls.""" - content = msg.get("content") or "" - if content.strip() or not msg.get("tool_calls"): - return False - # If reasoning_content exists, the model did think — not a silent invocation. - reasoning = msg.get("reasoning_content") - if reasoning and reasoning.strip(): - return False - return True - - -def _is_bare_text_tool_call(msg): - """True if assistant *msg* has text without ```` tags and has tool_calls.""" - content = msg.get("content") or "" - if not content.strip() or not msg.get("tool_calls"): - return False - # If reasoning_content exists, thinking is in a separate field — not bare text. - reasoning = msg.get("reasoning_content") - if reasoning and reasoning.strip(): - return False - normalized = _THINK_OPEN_RE.sub("", content) - normalized = _THINK_CLOSE_RE.sub("", normalized) - match = _THINK_RE.search(normalized) - return not (match and match.group(1).strip()) - - -def _msg_has_thinking(msg): - """True if assistant *msg* has thinking content (inline or reasoning_content).""" - if msg.get("role") != "assistant": - return False - content = msg.get("content") or "" - normalized = _THINK_OPEN_RE.sub("", content) - normalized = _THINK_CLOSE_RE.sub("", normalized) - if _THINK_RE.search(normalized): - return True - rc = msg.get("reasoning_content") or "" - return bool(rc.strip()) - - -def _truncate_at_task_notification(messages): - """Truncate messages when a ```` follows a pure-text assistant. - - Claude Code emits ```` as a user message when a - background task (e.g. ``pip install``) completes. If the model has - already produced a text-only summary (no tool_calls), the notification - and all subsequent messages are noise — the model just replies - "nothing to do". Truncating here removes that noise. - - Only triggers when the pattern is: - assistant (text, no tool_calls) → user () - - Returns: - Truncated message list (or the original list if no truncation needed). - """ - for i, m in enumerate(messages): - if m.get("role") != "user": - continue - if "" not in (m.get("content") or ""): - continue - # Find preceding assistant - prev_asst = None - for j in range(i - 1, -1, -1): - if messages[j].get("role") == "assistant": - prev_asst = messages[j] - break - if prev_asst is None: - continue - content = prev_asst.get("content") or "" - if content.strip() and not prev_asst.get("tool_calls"): - # Truncate: keep everything up to (but not including) this user msg - return messages[:i] - return messages - - -# ============================================================ -# 3b. Balancing — downsample non-thinking pairs -# ============================================================ - - -def _classify_pair(pair): - """Classify a pair by its target assistant turn's content type. - - Returns one of: - ``"thinking"`` — target has actual ```` content or - non-empty ``reasoning_content``. - ``"no_thinking_tool_call"`` — target has no thinking but has - ``tool_calls`` (the dominant category that causes distribution - skew). - ``"pure_text"`` — target has no thinking and no tool_calls - (typically the final summary turn in a trajectory). - """ - target = pair[-1] - if target.get("role") != "assistant": - return "pure_text" - - content = target.get("content") or "" - rc = target.get("reasoning_content") or "" - has_thinking = bool(_THINK_RE.search(content)) or bool(rc.strip()) - - if has_thinking: - return "thinking" - if target.get("tool_calls"): - return "no_thinking_tool_call" - return "pure_text" - - -def _balance_thinking_pairs(pairs, max_no_thinking_ratio, seed=42, tools_list=None): - """Downsample non-thinking **tool-call** pairs to control balance. - - Only ``no_thinking_tool_call`` pairs (no thinking but has tool_calls) - are subject to downsampling. ``thinking`` pairs and ``pure_text`` - pairs (the final summary turn, no thinking and no tool_calls) are - always kept — the latter are critical for the model to learn when - to stop calling tools and give a final answer. - - Args: - pairs: List of progressive SFT pairs. - max_no_thinking_ratio: Maximum ratio of non-thinking tool-call - pairs to thinking pairs. For example, ``1.0`` means at most - 1:1, ``2.0`` means at most 2 non-thinking per 1 thinking pair. - ``None`` disables downsampling. - seed: Random seed for reproducible downsampling. - - Returns: - Balanced list of pairs (order preserved, randomly sampled for - the downsampled category). - """ - if max_no_thinking_ratio is None: - return pairs, tools_list - - thinking = [] - no_think_tc = [] - pure_text = [] - for i, pair in enumerate(pairs): - cat = _classify_pair(pair) - if cat == "thinking": - thinking.append(i) - elif cat == "no_thinking_tool_call": - no_think_tc.append(i) - else: - pure_text.append(i) - - n_think = len(thinking) - n_no_think_tc = len(no_think_tc) - n_pure_text = len(pure_text) - - if n_think == 0: - logger.warning( - "No thinking pairs found; skipping balance " - "(all %d pairs have empty thinking).", - n_no_think_tc + n_pure_text, - ) - return pairs, tools_list - - max_no_think_tc = int(n_think * max_no_thinking_ratio) - if n_no_think_tc <= max_no_think_tc: - logger.info( - "Thinking balance OK: %d thinking + %d no-think-tc + %d pure-text " - "(ratio %.1f <= %.1f), no downsampling needed.", - n_think, - n_no_think_tc, - n_pure_text, - n_no_think_tc / n_think, - max_no_thinking_ratio, - ) - return pairs, tools_list - - rng = random.Random(seed) - sampled_tc = set(rng.sample(no_think_tc, max_no_think_tc)) - keep_indices = sorted(set(thinking) | sampled_tc | set(pure_text)) - balanced = [pairs[i] for i in keep_indices] - balanced_tools = ( - [tools_list[i] for i in keep_indices] if tools_list is not None else None - ) - - logger.info( - "Balanced thinking pairs: %d thinking + %d no-think-tc " - "(downsampled from %d, ratio %.1f → %.1f) + %d pure-text (kept all).", - n_think, - max_no_think_tc, - n_no_think_tc, - n_no_think_tc / n_think, - max_no_thinking_ratio, - n_pure_text, - ) - return balanced, balanced_tools - - -# ============================================================ -# 3c. Thinking augmentation stats -# ============================================================ - - -def _log_thinking_augmentation_stats( - n_variants, - prob, - n_total_trajs, - thinking_turns_per_traj, - total_asst_turns_per_traj, - patterns_per_traj, -): - """Log adaptive-thinking augmentation quality metrics. - - Called after the augmentation loop in loaders to report how well - the ``n_thinking_variants`` / ``random_strip_thinking_prob`` settings - produce diverse thinking-pattern variants. - - Args: - n_variants: ``n_thinking_variants`` setting (K). - prob: ``random_strip_thinking_prob`` setting. - n_total_trajs: Total number of source trajectories processed. - thinking_turns_per_traj: List of N_thinking per source trajectory. - total_asst_turns_per_traj: List of N_total_asst per source trajectory. - patterns_per_traj: List of ``set[frozenset]`` — the unique strip - patterns generated for each source trajectory (including the - empty frozenset for the original unstripped variant). - """ - n_eligible = sum(1 for n in thinking_turns_per_traj if n > 0) - total_thinking = sum(thinking_turns_per_traj) - total_asst = sum(total_asst_turns_per_traj) - - # 1. Thinking Turn Coverage - avg_thinking = total_thinking / max(n_total_trajs, 1) - thinking_ratio = total_thinking / max(total_asst, 1) - - # 2. Pattern Diversity - diversity_ratios = [] - for n_think, patterns in zip(thinking_turns_per_traj, patterns_per_traj): - if n_think == 0: - continue - theoretical_max = min(n_variants, 2**n_think) - actual_unique = len(patterns) - diversity_ratios.append(actual_unique / theoretical_max) - avg_diversity = sum(diversity_ratios) / max(len(diversity_ratios), 1) - - # 3. Augmentation Efficiency - n_non_trivial = 0 - for patterns in patterns_per_traj: - # Count variants that differ from the original (non-empty strip set) - n_non_trivial += sum(1 for p in patterns if p) - expected_aug = (n_variants - 1) * max(n_eligible, 1) - efficiency = n_non_trivial / max(expected_aug, 1) - - # 4. Total sample count - n_total_samples = sum(len(p) for p in patterns_per_traj) - - logger.info( - f"Thinking augmentation stats (K={n_variants}, p={prob:.2f}):\n" - f" Source trajectories: {n_total_trajs} " - f"({n_eligible} with thinking turns)\n" - f" Thinking coverage: {avg_thinking:.1f} thinking turns/traj, " - f"{thinking_ratio:.1%} of all assistant turns\n" - f" Pattern diversity: {avg_diversity:.2f} " - f"(1.0 = all variants unique)\n" - f" Augmentation efficiency: {efficiency:.2f} " - f"({n_non_trivial}/{expected_aug} non-trivial variants)\n" - f" Total samples after augmentation: {n_total_samples}" - ) - - -# ============================================================ -# 4. Splitting — trajectory → progressive pairs -# ============================================================ - - -def _find_segments(messages): - """Find assistant+tools segment boundaries. - - Returns: - List of ``(assistant_start_idx, segment_end_idx)`` tuples. - """ - segments = [] - i = 0 - while i < len(messages): - if messages[i].get("role") == "assistant": - j = i + 1 - while j < len(messages) and messages[j].get("role") == "tool": - j += 1 - segments.append((i, j)) - i = j - else: - i += 1 - return segments - - -def _split_and_filter( - messages, - filter_errors=True, - strip_all_thinking=False, - filter_empty_tool_calls=False, - filter_bare_text_tool_calls=False, - random_strip_thinking_prob=0.0, - rng=None, -): - """Split trajectory into progressive pairs and optionally filter. - - By default, thinking (``...``) is stripped from context - assistant turns only; the last assistant turn (training target) keeps - its content unchanged. Set *strip_all_thinking* to strip from every - assistant turn including the target. - - When *random_strip_thinking_prob* > 0, each target assistant turn that - has thinking content is independently stripped with that probability. - Stripped turns use the context-cleaned version (thinking fully removed, - no empty ```` injected). - - Args: - messages: Raw trajectory messages. - filter_errors: If True (default), discard pairs whose current segment - contains a tool result with ``is_error=True``. Set to False to - keep all pairs regardless of tool errors. - strip_all_thinking: If True, strip ```` blocks from every - assistant turn including the training target. - filter_empty_tool_calls: If True, discard pairs whose training-target - assistant turn has no text content but has tool_calls. - filter_bare_text_tool_calls: If True, discard pairs whose - training-target assistant turn has text content without - ```` tags and has tool_calls. - random_strip_thinking_prob: Probability of stripping thinking - from each target assistant turn. 0.0 = no stripping. - rng: ``random.Random`` instance for reproducible sampling. - - Returns: - Tuple of ``(pairs, n_filtered_errors, n_filtered_empty_tc, - n_filtered_bare_tc, n_stripped)``. - """ - segments = _find_segments(messages) - if not segments: - return [], 0, 0, 0, 0 - - pairs = [] - n_filtered_errors = 0 - n_filtered_empty_tc = 0 - n_filtered_bare_tc = 0 - n_stripped = 0 - - # Pre-clean all messages in context mode (thinking stripped). - # This avoids re-cleaning the same message for every progressive pair - # (O(N+K) instead of O(N*K) where K = number of segments). - context_cleaned = [_clean_message(m, strip_thinking=True) for m in messages] - - # For target assistant turns, clean with thinking preserved (unless - # strip_all_thinking is set, in which case context_cleaned is reusable). - # When stripping is active (augmented variant), use ensure_thinking=False - # so empty-thinking turns don't get \n injected. - stripping_active = random_strip_thinking_prob > 0.0 and rng is not None - target_ensure = not stripping_active - target_cleaned = {} - if not strip_all_thinking: - for asst_start, _ in segments: - target_cleaned[asst_start] = _clean_message( - messages[asst_start], - strip_thinking=False, - ensure_thinking=target_ensure, - ) - - for asst_start, seg_end in segments: - # Check if current segment has any tool errors - if filter_errors and _segment_has_error(messages, asst_start, seg_end): - n_filtered_errors += 1 - continue - - # Content-type filters operate on the raw assistant message. - asst_msg = messages[asst_start] - if filter_empty_tool_calls and _is_empty_tool_call(asst_msg): - n_filtered_empty_tc += 1 - continue - if filter_bare_text_tool_calls and _is_bare_text_tool_call(asst_msg): - n_filtered_bare_tc += 1 - continue - - # Build pair: include context up to the target assistant turn, - # truncating tool responses that follow it. This ensures the - # target assistant is always the *last* message so that chat - # templates with ``loop.last``-dependent rendering (e.g. Qwen3 - # ```` injection) behave consistently. The tool responses - # would have loss_mask=0 anyway and only add noise. - pair = list(context_cleaned[: asst_start + 1]) - if not strip_all_thinking: - # Randomly strip: leave context_cleaned version (thinking - # already removed) instead of replacing with target_cleaned. - should_strip = ( - rng is not None - and _msg_has_thinking(messages[asst_start]) - and rng.random() < random_strip_thinking_prob - ) - if not should_strip: - pair[asst_start] = target_cleaned[asst_start] - else: - n_stripped += 1 - pairs.append(pair) - - return pairs, n_filtered_errors, n_filtered_empty_tc, n_filtered_bare_tc, n_stripped - - -def _prepare_trajectory( - messages, - filter_errors=True, - filter_empty_tool_calls=False, - filter_bare_text_tool_calls=False, - random_strip_thinking_prob=0.0, - rng=None, -): - """Prepare a full trajectory for trajectory-level training. - - Cleans all messages preserving thinking for every assistant turn - (``strip_thinking=False``, ``ensure_thinking=True``). Identifies - which assistant segments should be masked (``loss_mask=0``) based - on error tool responses, empty tool calls, or bare-text tool calls. - - When *random_strip_thinking_prob* > 0, each assistant turn that has - thinking content is independently stripped with that probability. - Stripped turns have their ```` blocks and ``reasoning_content`` - completely removed (no empty ```` injected). - - Args: - messages: Raw trajectory messages. - filter_errors: If True (default), mask segments with error tool - responses. - filter_empty_tool_calls: If True, mask segments whose assistant - turn has no text content but has tool_calls. - filter_bare_text_tool_calls: If True, mask segments whose - assistant turn has text without ```` tags and has - tool_calls. - random_strip_thinking_prob: Probability of stripping thinking - from each assistant turn that has thinking content. - 0.0 (default) = no stripping, 1.0 = strip all. - rng: ``random.Random`` instance for reproducible sampling. - - Returns: - Tuple of ``(cleaned_messages, masked_segment_indices, - n_error, n_empty_tc, n_bare_tc, stripped_pattern)`` or ``None`` - if the trajectory has no assistant turns. *stripped_pattern* is - a ``frozenset`` of message indices whose thinking was stripped - (empty if no stripping occurred). - """ - segments = _find_segments(messages) - if not segments: - return None - - masked_indices = set() - n_error = 0 - n_empty_tc = 0 - n_bare_tc = 0 - for idx, (asst_start, seg_end) in enumerate(segments): - if filter_errors and _segment_has_error(messages, asst_start, seg_end): - masked_indices.add(idx) - n_error += 1 - continue - asst_msg = messages[asst_start] - if filter_empty_tool_calls and _is_empty_tool_call(asst_msg): - masked_indices.add(idx) - n_empty_tc += 1 - continue - if filter_bare_text_tool_calls and _is_bare_text_tool_call(asst_msg): - masked_indices.add(idx) - n_bare_tc += 1 - - # Determine which assistant turns to randomly strip thinking from. - strip_thinking_indices = set() - stripping_active = random_strip_thinking_prob > 0.0 and rng is not None - if stripping_active: - for asst_start, _seg_end in segments: - if _msg_has_thinking(messages[asst_start]): - if rng.random() < random_strip_thinking_prob: - strip_thinking_indices.add(asst_start) - - # When stripping is active (augmented variant), use ensure_thinking=False - # for ALL turns so that empty-thinking turns don't get \n - # injected. Only real thinking content is preserved. - # When stripping is inactive (variant 0 or no augmentation), keep - # ensure_thinking=True to match the standard training format. - default_ensure = not stripping_active - - cleaned = [] - for i, m in enumerate(messages): - if i in strip_thinking_indices: - cleaned.append( - _clean_message(m, strip_thinking=True, ensure_thinking=False) - ) - else: - cleaned.append( - _clean_message(m, strip_thinking=False, ensure_thinking=default_ensure) - ) - - return ( - cleaned, - sorted(masked_indices), - n_error, - n_empty_tc, - n_bare_tc, - frozenset(strip_thinking_indices), - ) - - -# ============================================================ -# 5. Tokenization — template detection, render, loss mask, dump -# ============================================================ - - -# -- Chat template patch (runtime, no file modification) -------- - -# Both Bailing and Qwen3 templates have ``ns.last_query_index`` logic -# that prevents ```` rendering for assistant turns BEFORE the -# last user message, AND discards inline empty ``\n`` -# extracted from content. -# -# This breaks trajectory-mode training: -# - Multi-user trajectories: turns before the last user msg lack -# - Empty ensure_thinking via inline gets stripped -# -# The patch below handles both Bailing (`ASSISTANT` style) -# and Qwen3 (`<|im_start|>assistant` style) templates: -# 1. Adds ``had_think_tags`` detection so empty ```` survives. -# 2. Removes the ``ns.last_query_index`` gate so all assistant turns -# render ```` uniformly when think intent is detected. -# -# Applied at runtime via ``tokenizer.chat_template = patched`` — the -# original template file on disk is never modified. - -_BAILING_OLD_BLOCK = ( - "{%- if loop.index0 > ns.last_query_index %}\n" - " {%- if reasoning_content != '' %}\n" - " {{- 'ASSISTANT\\n' + '\\n'" - " + reasoning_content.strip('\\n') + '\\n\\n\\n'" - " + content.lstrip('\\n') }}\n" - " {%- else %}\n" - " {{- 'ASSISTANT\\n' + content }}\n" - " {%- endif %}\n" - " {%- else %}\n" - " {{- 'ASSISTANT\\n' + content }}\n" - " {%- endif %}" -) -_BAILING_NEW_BLOCK = ( - "{%- if reasoning_content != '' or had_think_tags %}\n" - " {{- 'ASSISTANT\\n' + '\\n'" - " + reasoning_content.strip('\\n') + '\\n\\n\\n'" - " + content.lstrip('\\n') }}\n" - " {%- else %}\n" - " {{- 'ASSISTANT\\n' + content }}\n" - " {%- endif %}" -) - -# Qwen3 uses `loop.last or (not loop.last and reasoning_content)` so the -# last turn always renders even with empty reasoning. We -# preserve `loop.last` and add `had_think_tags` for inline-empty support. -_QWEN3_OLD_BLOCK = ( - "{%- if loop.index0 > ns.last_query_index %}\n" - " {%- if loop.last or (not loop.last and reasoning_content) %}\n" - " {{- '<|im_start|>' + message.role + '\\n\\n'" - " + reasoning_content.strip('\\n') + '\\n\\n\\n'" - " + content.lstrip('\\n') }}\n" - " {%- else %}\n" - " {{- '<|im_start|>' + message.role + '\\n' + content }}\n" - " {%- endif %}\n" - " {%- else %}\n" - " {{- '<|im_start|>' + message.role + '\\n' + content }}\n" - " {%- endif %}" -) -_QWEN3_NEW_BLOCK = ( - "{%- if loop.last or reasoning_content != '' or had_think_tags %}\n" - " {{- '<|im_start|>' + message.role + '\\n\\n'" - " + reasoning_content.strip('\\n') + '\\n\\n\\n'" - " + content.lstrip('\\n') }}\n" - " {%- else %}\n" - " {{- '<|im_start|>' + message.role + '\\n' + content }}\n" - " {%- endif %}" -) - -_OLD_DETECT = "{%- set reasoning_content = '' %}" -_NEW_DETECT = ( - "{%- set reasoning_content = '' %}\n" - " {%- set had_think_tags = ('' in content) %}" -) - - -def _patch_chat_template_for_training(tokenizer): - """Patch Bailing/Qwen3 chat templates to render ```` uniformly. - - Detects template family by matching known render blocks: - - Bailing: ``ASSISTANT`` markers - - Qwen3: ``<|im_start|>assistant`` markers - - Other templates (e.g. plain ChatML without ``last_query_index``) - are left unchanged. If the template has ``last_query_index`` but - neither known block matches, logs a warning. - """ - template = getattr(tokenizer, "chat_template", None) - if not template or "last_query_index" not in template: - return - - if _BAILING_OLD_BLOCK in template: - family = "Bailing" - patched = template.replace(_BAILING_OLD_BLOCK, _BAILING_NEW_BLOCK) - elif _QWEN3_OLD_BLOCK in template: - family = "Qwen3" - patched = template.replace(_QWEN3_OLD_BLOCK, _QWEN3_NEW_BLOCK) - else: - logger.warning( - "Chat template has last_query_index but matches neither " - "Bailing nor Qwen3 render block. Patch skipped — empty " - " may be discarded, multi-user turns may lack ." - ) - return - - patched = patched.replace(_OLD_DETECT, _NEW_DETECT) - - tokenizer.chat_template = patched - logger.info( - f"Patched {family} chat template for training: removed " - "last_query_index gate, added had_think_tags detection." - ) - - -_TEMPLATE_PATTERNS = [ - # ChatML (Qwen, etc.): <|im_start|>assistant\n ... <|im_end|> - (r"<\|im_start\|>assistant\n", r"<\|im_end\|>"), - # Llama 3: <|start_header_id|>assistant<|end_header_id|>\n\n ... <|eot_id|> - (r"<\|start_header_id\|>assistant<\|end_header_id\|>\n\n", r"<\|eot_id\|>"), - # GLM: <|assistant|> ... (ends at next <|user|>, <|observation|>, or end of string) - (r"<\|assistant\|>", r"(?=<\|user\|>|<\|observation\|>|\Z)"), -] - - -def _parse_tool_call_arguments(messages): - """Parse JSON-string arguments in tool_calls to dicts. - - OpenAI returns tool_call arguments as JSON strings, but some chat - templates (e.g. GLM-4.x / GLM-5.x) expect parsed dicts. Most other - templates (Qwen / ChatML, Llama 3, Bailing, ...) accept the standard - OpenAI string form, so this conversion must be opt-in. - """ - patched = [] - for m in messages: - tool_calls = m.get("tool_calls") - if not tool_calls: - patched.append(m) - continue - new_tcs = [] - for tc in tool_calls: - fn = tc.get("function", tc) - args = fn.get("arguments") - if isinstance(args, str): - try: - parsed = json.loads(args) - except (json.JSONDecodeError, TypeError): - parsed = args - fn = {**fn, "arguments": parsed} - tc = {**tc, "function": fn} if "function" in tc else fn - new_tcs.append(tc) - patched.append({**m, "tool_calls": new_tcs}) - return patched - - -def _render_tokenize_mask( - messages, - tokenizer, - assistant_pattern, - tools=None, - *, - split_mode="pair", - error_indices=None, - parse_tool_call_args=False, -): - """Render, tokenize, and build loss_mask for a message list. - - In **pair mode** (default), only the **last** assistant turn gets - ``loss_mask=1``. In **trajectory mode**, **all** assistant turns - get ``loss_mask=1`` except those at indices in *error_indices*. - - When *parse_tool_call_args* is True, JSON-string ``tool_calls`` arguments - are converted to dicts before rendering (required by GLM chat templates; - other templates such as Qwen / Llama / Bailing must keep the OpenAI - string form). - - Returns: - Tuple of ``(full_text, input_ids, loss_mask, offset_mapping)``, or - ``None`` if ``apply_chat_template`` fails. - """ - # 1) Render the full template text. - try: - kwargs = {"tokenize": False} - if tools is not None: - kwargs["tools"] = tools - if parse_tool_call_args: - messages = _parse_tool_call_arguments(messages) - full_text = tokenizer.apply_chat_template(messages, **kwargs) - except Exception as e: - logger.warning( - "apply_chat_template failed: %s. Skipping sample.", - e, - ) - return None - - # 2) Tokenize with offset mapping so we can map char→token. - encoding = tokenizer( - full_text, add_special_tokens=False, return_offsets_mapping=True - ) - input_ids = encoding["input_ids"] - offset_mapping = encoding["offset_mapping"] - - # 3) Build loss_mask. - loss_mask = [0] * len(input_ids) - - if split_mode == "trajectory": - # Trajectory mode: mask ALL assistant segments, skip error_indices. - skip = set(error_indices) if error_indices else set() - matches = list(assistant_pattern.finditer(full_text)) - - # Verify regex matches correspond 1:1 to assistant messages. - n_asst = sum(1 for m in messages if m.get("role") == "assistant") - if len(matches) != n_asst: - logger.warning( - "Segment count mismatch: %d assistant messages but %d regex " - "matches in rendered text. Training all matched segments " - "(error masking disabled for this sample).", - n_asst, - len(matches), - ) - skip = set() - - for seg_idx, m in enumerate(matches): - if seg_idx in skip: - continue - rs, re_ = m.start(1), m.end(0) - for tok_idx, (cs, ce) in enumerate(offset_mapping): - if ce > rs and cs < re_: - loss_mask[tok_idx] = 1 - else: - # Pair mode: mask only the LAST assistant segment. - last_match = None - for m in assistant_pattern.finditer(full_text): - last_match = m - if last_match is not None: - rs, re_ = last_match.start(1), last_match.end(0) - for tok_idx, (cs, ce) in enumerate(offset_mapping): - if ce > rs and cs < re_: - loss_mask[tok_idx] = 1 - - return full_text, input_ids, loss_mask, offset_mapping - - -class _TokenizeAndMask: - """Picklable callable for ``Dataset.map(num_proc=N)``.""" - - def __init__( - self, - tokenizer, - assistant_pattern, - max_length=None, - *, - split_mode="pair", - parse_tool_call_args=False, - ): - self.tokenizer = tokenizer - self.assistant_pattern = assistant_pattern - self.max_length = max_length - self.split_mode = split_mode - self.parse_tool_call_args = parse_tool_call_args - - def __call__(self, sample): - error_indices = ( - sample.get("error_indices", []) if self.split_mode == "trajectory" else None - ) - tools_json = sample.get("tools_json") - tools = json.loads(tools_json) if tools_json else None - result = _render_tokenize_mask( - sample["messages"], - self.tokenizer, - self.assistant_pattern, - tools, - split_mode=self.split_mode, - error_indices=error_indices, - parse_tool_call_args=self.parse_tool_call_args, - ) - if result is None: - return {"input_ids": [], "loss_mask": []} - - _full_text, input_ids, loss_mask, _offset_mapping = result - - # Early exit: overlength or empty → return empty so a single - # filter pass removes it together with template-failure empties. - if self.max_length is not None and len(input_ids) > self.max_length: - return {"input_ids": [], "loss_mask": []} - - return {"input_ids": input_ids, "loss_mask": loss_mask} - - -def _detect_template_pattern(tokenizer, tools=None): - """Detect the assistant role delimiter used by this tokenizer's template. - - When *tools* is provided the probe is rendered with ``tools=`` so that - the detected delimiters match the actual training text (some templates - alter the system block when tools are present). - - Strategy: - 1. Try known ``_TEMPLATE_PATTERNS`` (fast, battle-tested). - 2. Fall back to double-probe diff: render the template with a known - marker and with empty content, then diff the two strings to extract - the exact header and end-of-turn delimiters. - - Raises: - ValueError: If both strategies fail to detect a usable pattern. - """ - _PROBE_CONTENT = "PROBE_MARKER" - - extra_kwargs = {} - if tools is not None: - extra_kwargs["tools"] = tools - - probe_msgs = [ - {"role": "user", "content": "x"}, - {"role": "assistant", "content": _PROBE_CONTENT}, - ] - probe_text = tokenizer.apply_chat_template( - probe_msgs, tokenize=False, **extra_kwargs - ) - - # --- Strategy 1: known patterns --- - for hdr_re, eot_re in _TEMPLATE_PATTERNS: - if re.search(hdr_re, probe_text): - pattern = re.compile(hdr_re + r"(.*?)" + eot_re, re.DOTALL) - logger.info( - f"Detected template style (known pattern): " - f"header_re={hdr_re!r}, eot_re={eot_re!r}" - ) - return pattern - - # --- Strategy 2: double-probe diff --- - try: - probe_empty = [ - {"role": "user", "content": "x"}, - {"role": "assistant", "content": ""}, - ] - text_empty = tokenizer.apply_chat_template( - probe_empty, tokenize=False, **extra_kwargs - ) - - marker_idx = probe_text.index(_PROBE_CONTENT) - header = probe_text[:marker_idx] - tail = probe_text[marker_idx + len(_PROBE_CONTENT) :] - - if text_empty == header + tail: - # Extract the assistant-specific header by removing the shared - # user-only prefix. - user_only = tokenizer.apply_chat_template( - [{"role": "user", "content": "x"}], - tokenize=False, - **extra_kwargs, - ) - asst_header = header[len(user_only) :] - # end-of-turn delimiter: strip leading newlines, then take - # up to the first newline (or the full string if none). - eot_stripped = tail.lstrip("\n") - eot = eot_stripped.split("\n")[0] if "\n" in eot_stripped else eot_stripped - - if asst_header and eot: - hdr_re = re.escape(asst_header) - eot_re = re.escape(eot) - pattern = re.compile(hdr_re + r"(.*?)" + eot_re, re.DOTALL) - logger.info( - f"Detected template style (probe diff): " - f"header={asst_header!r}, eot={eot!r}" - ) - return pattern - except (ValueError, IndexError): - pass # PROBE_CONTENT not found in rendered text, skip - - raise ValueError( - "Could not detect chat template assistant delimiters. " - "Unable to build a reliable loss mask. " - f"Probe text: {probe_text[:200]!r}" - ) - - -def _dump_samples( - samples, - tokenizer, - assistant_pattern, - tools_list, - dump_dir, - n_samples, - *, - split_mode="pair", - error_indices_list=None, - parse_tool_call_args=False, -): - """Dump sampled message lists as ``.txt`` + ``.json`` for inspection. - - Args: - samples: List of message-list samples (pairs or full trajectories). - tokenizer: Tokenizer with ``apply_chat_template`` support. - assistant_pattern: Compiled regex from ``_detect_template_pattern``. - tools_list: Per-sample tool definitions (parallel to *samples*), - or ``None`` when no tools are available. - dump_dir: Directory to write files into (created if needed). - n_samples: Number of random samples to dump. ``-1`` dumps all. - split_mode: ``"trajectory"`` for trajectory-mode loss masking. - error_indices_list: Per-sample error segment indices (trajectory mode). - """ - import random as _random - - os.makedirs(dump_dir, exist_ok=True) - - if n_samples == -1 or n_samples >= len(samples): - indices = list(range(len(samples))) - else: - indices = sorted(_random.sample(range(len(samples)), n_samples)) - - n_written = 0 - for i in indices: - sample = samples[i] - sample_tools = tools_list[i] if tools_list else None - err_idxs = ( - error_indices_list[i] - if split_mode == "trajectory" and error_indices_list - else None - ) - - result = _render_tokenize_mask( - sample, - tokenizer, - assistant_pattern, - sample_tools, - split_mode=split_mode, - error_indices=err_idxs, - parse_tool_call_args=parse_tool_call_args, - ) - if result is None: - continue - - full_text, input_ids, loss_mask, offset_mapping = result - n_loss = sum(loss_mask) - base = os.path.join(dump_dir, f"sample_{i}") - - # --- .txt --- - with open(base + ".txt", "w", encoding="utf-8") as fout: - fout.write( - f"Sample {i}: {len(sample)} messages, " - f"{len(input_ids)} tokens, loss=1: {n_loss}\n" - ) - fout.write(f"Last msg role: {sample[-1]['role']}\n") - fout.write(f"{'=' * 72}\n\n") - - fout.write("--- Rendered Text ---\n") - fout.write(full_text) - fout.write("\n\n") - - fout.write("--- Token / Loss Mask ---\n") - fout.write(f"{'Idx':>6} | {'TokenID':>8} | Loss | Token Text\n") - fout.write(f"{'-' * 6}-+-{'-' * 8}-+------+{'-' * 40}\n") - for t in range(len(input_ids)): - cs, ce = offset_mapping[t] - tok_text = repr(full_text[cs:ce]) - fout.write( - f"{t:>6} | {input_ids[t]:>8} | {loss_mask[t]:>4} | {tok_text}\n" - ) - - # --- .json --- - tokens_list = [] - for t in range(len(input_ids)): - cs, ce = offset_mapping[t] - tokens_list.append( - { - "idx": t, - "token_id": input_ids[t], - "text": full_text[cs:ce], - "loss": loss_mask[t], - } - ) - record = { - "sample_index": i, - "n_messages": len(sample), - "n_tokens": len(input_ids), - "n_loss_tokens": n_loss, - "rendered_text": full_text, - "tokens": tokens_list, - } - with open(base + ".json", "w", encoding="utf-8") as fout: - json.dump(record, fout, ensure_ascii=False) - - n_written += 1 - - logger.info(f"Dumped {n_written} samples to {dump_dir}/") - - -# ============================================================ -# 6. Pipeline — loading, processing, distributed cache, public API -# ============================================================ - - -def _load_trajectory_pairs( - path: str, - filter_errors: bool = True, - strip_all_thinking: bool = False, - filter_empty_tool_calls: bool = False, - filter_bare_text_tool_calls: bool = False, - truncate_task_notifications: bool = False, - max_no_thinking_ratio: float | None = None, - random_strip_thinking_prob: float = 0.0, - random_strip_thinking_seed: int = 42, - n_thinking_variants: int = 1, -): - """Load trajectory JSONL and split into progressive pairs. - - When *n_thinking_variants* > 1, each trajectory is split K times: - variant 0 preserves all thinking, variants 1~K-1 randomly strip. - - Supports nested (``conversations`` wrapper) and flat JSONL formats - (auto-detected per record via ``_iter_jsonl_records``). - - Returns: - Tuple of ``(all_pairs, tools)`` where *tools* is ``None`` when no - tool definitions are found. - """ - all_pairs = [] - all_tools = [] - records_in = 0 - total_filtered_errors = 0 - total_filtered_empty_tc = 0 - total_filtered_bare_tc = 0 - total_truncated = 0 - total_stripped_thinking = 0 - - augment = n_thinking_variants > 1 - rng = ( - random.Random(random_strip_thinking_seed) - if random_strip_thinking_prob > 0.0 - else None - ) - - if augment and random_strip_thinking_prob <= 0.0: - logger.warning( - "n_thinking_variants=%d but random_strip_thinking_prob=0; " - "all variants will be identical.", - n_thinking_variants, - ) - - # Stats collectors for augmentation logging. - thinking_turns_per_traj = [] - total_asst_turns_per_traj = [] - patterns_per_traj = [] - - for record_idx, messages, record_tools in _iter_jsonl_records(path): - records_in = record_idx - - if truncate_task_notifications: - truncated = _truncate_at_task_notification(messages) - if len(truncated) < len(messages): - total_truncated += 1 - messages = truncated - - shared_kwargs = dict( - filter_errors=filter_errors, - strip_all_thinking=strip_all_thinking, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - ) - - if augment: - # Variant 0: preserve all thinking. - pairs_orig, n_err, n_empty_tc, n_bare_tc, _ = _split_and_filter( - messages, **shared_kwargs, random_strip_thinking_prob=0.0, rng=None - ) - total_filtered_errors += n_err - total_filtered_empty_tc += n_empty_tc - total_filtered_bare_tc += n_bare_tc - all_pairs.extend(pairs_orig) - all_tools.extend([record_tools] * len(pairs_orig)) - # Collect stats. - segments = _find_segments(messages) - n_think = sum(1 for s, _ in segments if _msg_has_thinking(messages[s])) - n_asst = len(segments) - thinking_turns_per_traj.append(n_think) - total_asst_turns_per_traj.append(n_asst) - - # Variants 1 ~ K-1: random strip. - variant_patterns = {frozenset()} # original = no strip - for _k in range(n_thinking_variants - 1): - pairs_aug, _, _, _, n_stripped = _split_and_filter( - messages, - **shared_kwargs, - random_strip_thinking_prob=random_strip_thinking_prob, - rng=rng, - ) - total_stripped_thinking += n_stripped - all_pairs.extend(pairs_aug) - all_tools.extend([record_tools] * len(pairs_aug)) - # Approximate pattern: record which pairs had their target stripped. - # For stats, use the count as a proxy since _split_and_filter - # doesn't return per-pair strip info. - variant_patterns.add(frozenset([n_stripped])) - patterns_per_traj.append(variant_patterns) - else: - # Single variant (original behavior). - pairs, n_err, n_empty_tc, n_bare_tc, n_stripped = _split_and_filter( - messages, - **shared_kwargs, - random_strip_thinking_prob=random_strip_thinking_prob, - rng=rng, - ) - total_filtered_errors += n_err - total_filtered_empty_tc += n_empty_tc - total_filtered_bare_tc += n_bare_tc - total_stripped_thinking += n_stripped - all_pairs.extend(pairs) - all_tools.extend([record_tools] * len(pairs)) - - # Log extracted tools summary. - n_with_tools = sum(1 for t in all_tools if t is not None) - if n_with_tools > 0: - all_tool_names = set() - for t_list in all_tools: - if t_list is not None: - for t in t_list: - all_tool_names.add(t.get("function", {}).get("name", "?")) - logger.info( - f"Extracted tools from {n_with_tools}/{len(all_tools)} pairs: " - f"{sorted(all_tool_names)}" - ) - - filter_parts = [] - if total_truncated: - filter_parts.append( - f"{total_truncated} trajectories truncated at task-notification" - ) - if total_filtered_errors: - filter_parts.append(f"{total_filtered_errors} with tool errors") - if total_filtered_empty_tc: - filter_parts.append(f"{total_filtered_empty_tc} empty-content tool calls") - if total_filtered_bare_tc: - filter_parts.append(f"{total_filtered_bare_tc} bare-text tool calls") - if total_stripped_thinking: - filter_parts.append(f"{total_stripped_thinking} thinking blocks stripped") - filter_msg = ", ".join(filter_parts) if filter_parts else "none" - - logger.info( - f"Loaded {records_in} trajectories, " - f"generated {len(all_pairs)} pairs " - f"(filtered: {filter_msg})" - ) - - if augment and patterns_per_traj: - _log_thinking_augmentation_stats( - n_thinking_variants, - random_strip_thinking_prob, - records_in, - thinking_turns_per_traj, - total_asst_turns_per_traj, - patterns_per_traj, - ) - - # Balance thinking / no-thinking pair ratio. - all_pairs, all_tools = _balance_thinking_pairs( - all_pairs, max_no_thinking_ratio, tools_list=all_tools - ) - - return all_pairs, all_tools - - -def _load_presplit_pairs( - path: str, - strip_all_thinking: bool = False, - random_strip_thinking_prob: float = 0.0, - random_strip_thinking_seed: int = 42, - n_thinking_variants: int = 1, -): - """Load pre-split pair JSONL where each line is ``{"messages": [...]}``. - - Messages are cleaned but no splitting or error-filtering is performed. - By default, thinking is stripped from context assistant turns but - preserved for the last assistant turn (the training target). Set - *strip_all_thinking* to strip from every assistant turn. - - When *n_thinking_variants* > 1, each pair is augmented: variant 0 - preserves thinking, variants 1~K-1 randomly strip the target turn. - - Also extracts per-record ``tools`` definitions so that each pair - carries its own tools, same as ``_load_trajectory_pairs``. - - Returns: - Tuple of ``(all_pairs, all_tools)`` where *all_tools* is a - parallel list of per-sample tool definitions (may be ``None``). - """ - all_pairs = [] - all_tools = [] - n_stripped = 0 - augment = n_thinking_variants > 1 - - rng = ( - random.Random(random_strip_thinking_seed) - if random_strip_thinking_prob > 0.0 - else None - ) - - def _build_pair(messages, last_asst, strip_target): - pair = [] - for idx, m in enumerate(messages): - is_target = m.get("role") == "assistant" and idx == last_asst - strip = strip_all_thinking or not is_target or strip_target - pair.append(_clean_message(m, strip_thinking=strip)) - return pair - - with open(path, encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line: - continue - record = json.loads(line) - messages = record.get("messages", []) - if not messages: - continue - - record_tools = record.get("tools") - - # Find the last assistant index so we can preserve its thinking. - last_asst = None - for i, m in enumerate(messages): - if m.get("role") == "assistant": - last_asst = i - - has_thinking = ( - last_asst is not None - and not strip_all_thinking - and _msg_has_thinking(messages[last_asst]) - ) - - if augment: - # Variant 0: preserve all thinking. - all_pairs.append(_build_pair(messages, last_asst, strip_target=False)) - all_tools.append(record_tools) - - # Variants 1 ~ K-1: random strip. - for _k in range(n_thinking_variants - 1): - do_strip = ( - has_thinking - and rng is not None - and rng.random() < random_strip_thinking_prob - ) - if do_strip: - n_stripped += 1 - all_pairs.append( - _build_pair(messages, last_asst, strip_target=do_strip) - ) - all_tools.append(record_tools) - else: - # Single variant (original behavior). - strip_target = ( - has_thinking - and rng is not None - and rng.random() < random_strip_thinking_prob - ) - if strip_target: - n_stripped += 1 - all_pairs.append( - _build_pair(messages, last_asst, strip_target=strip_target) - ) - all_tools.append(record_tools) - - # Log extracted tools summary. - n_with_tools = sum(1 for t in all_tools if t is not None) - if n_with_tools > 0: - all_tool_names = set() - for t_list in all_tools: - if t_list is not None: - for t in t_list: - all_tool_names.add(t.get("function", {}).get("name", "?")) - logger.info( - f"Extracted tools from {n_with_tools}/{len(all_tools)} pairs: " - f"{sorted(all_tool_names)}" - ) - - strip_msg = f", {n_stripped} thinking blocks stripped" if n_stripped else "" - logger.info(f"Loaded {len(all_pairs)} pre-split pairs from {path}{strip_msg}") - return all_pairs, all_tools - - -def _load_full_trajectories( - path: str, - filter_errors: bool = True, - filter_empty_tool_calls: bool = False, - filter_bare_text_tool_calls: bool = False, - truncate_task_notifications: bool = False, - random_strip_thinking_prob: float = 0.0, - random_strip_thinking_seed: int = 42, - n_thinking_variants: int = 1, -): - """Load trajectory JSONL for trajectory-level training. - - Each trajectory becomes a single training sample with all assistant - turns as targets (``loss_mask=1``). When *filter_errors* is True, - assistant segments with error tool responses are identified so - tokenization can mask them (``loss_mask=0``) instead of discarding - the entire trajectory. - - When *n_thinking_variants* > 1, each trajectory is augmented into - K variants: the first preserves all thinking, the remaining K-1 - randomly strip thinking turns with *random_strip_thinking_prob*. - - Supports nested (``conversations`` wrapper) and flat JSONL formats - (auto-detected per record via ``_iter_jsonl_records``). - - Returns: - Tuple of ``(trajectories, error_indices_list, all_tools)`` where - *trajectories* is a list of cleaned message lists, - *error_indices_list* is a list of error segment index lists, - and *all_tools* is a parallel list of per-sample tool definitions. - """ - trajectories = [] - error_indices_list = [] - all_tools = [] - records_in = 0 - total_truncated = 0 - total_masked_errors = 0 - total_masked_empty_tc = 0 - total_masked_bare_tc = 0 - total_stripped_thinking = 0 - - augment = n_thinking_variants > 1 - rng = ( - random.Random(random_strip_thinking_seed) - if random_strip_thinking_prob > 0.0 - else None - ) - - if augment and random_strip_thinking_prob <= 0.0: - logger.warning( - "n_thinking_variants=%d but random_strip_thinking_prob=0; " - "all variants will be identical.", - n_thinking_variants, - ) - - # Stats collectors for augmentation logging. - thinking_turns_per_traj = [] - total_asst_turns_per_traj = [] - patterns_per_traj = [] - - for record_idx, messages, record_tools in _iter_jsonl_records(path): - records_in = record_idx - - if truncate_task_notifications: - truncated = _truncate_at_task_notification(messages) - if len(truncated) < len(messages): - total_truncated += 1 - messages = truncated - - shared_kwargs = dict( - filter_errors=filter_errors, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - ) - - if augment: - # Variant 0: preserve all thinking (no stripping). - result_orig = _prepare_trajectory( - messages, **shared_kwargs, random_strip_thinking_prob=0.0, rng=None - ) - if result_orig is None: - continue - cleaned_orig, masked_idxs, n_err, n_empty_tc, n_bare_tc, _ = result_orig - trajectories.append(cleaned_orig) - error_indices_list.append(masked_idxs) - all_tools.append(record_tools) - total_masked_errors += n_err - total_masked_empty_tc += n_empty_tc - total_masked_bare_tc += n_bare_tc - - # Collect stats: count thinking turns in this trajectory. - segments = _find_segments(messages) - n_think = sum(1 for s, _ in segments if _msg_has_thinking(messages[s])) - n_asst = len(segments) - thinking_turns_per_traj.append(n_think) - total_asst_turns_per_traj.append(n_asst) - - # Variants 1 ~ K-1: random strip thinking. - variant_patterns = {frozenset()} # original = empty pattern - for _k in range(n_thinking_variants - 1): - result_aug = _prepare_trajectory( - messages, - **shared_kwargs, - random_strip_thinking_prob=random_strip_thinking_prob, - rng=rng, - ) - if result_aug is None: - continue - cleaned_aug, _, _, _, _, strip_pattern = result_aug - trajectories.append(cleaned_aug) - error_indices_list.append(masked_idxs) # reuse - all_tools.append(record_tools) - total_stripped_thinking += len(strip_pattern) - variant_patterns.add(strip_pattern) - patterns_per_traj.append(variant_patterns) - else: - # Single variant (original behavior). - result = _prepare_trajectory( - messages, - **shared_kwargs, - random_strip_thinking_prob=random_strip_thinking_prob, - rng=rng, - ) - if result is None: - continue - cleaned, masked_idxs, n_err, n_empty_tc, n_bare_tc, strip_pattern = result - trajectories.append(cleaned) - error_indices_list.append(masked_idxs) - all_tools.append(record_tools) - total_masked_errors += n_err - total_masked_empty_tc += n_empty_tc - total_masked_bare_tc += n_bare_tc - total_stripped_thinking += len(strip_pattern) - - # Log extracted tools summary. - n_with_tools = sum(1 for t in all_tools if t is not None) - if n_with_tools > 0: - all_tool_names = set() - for t_list in all_tools: - if t_list is not None: - for t in t_list: - all_tool_names.add(t.get("function", {}).get("name", "?")) - logger.info( - f"Extracted tools from {n_with_tools}/{len(all_tools)} " - f"trajectories: {sorted(all_tool_names)}" - ) - - parts = [] - if total_truncated: - parts.append(f"{total_truncated} trajectories truncated at task-notification") - if total_masked_errors: - parts.append(f"{total_masked_errors} with tool errors") - if total_masked_empty_tc: - parts.append(f"{total_masked_empty_tc} empty-content tool calls") - if total_masked_bare_tc: - parts.append(f"{total_masked_bare_tc} bare-text tool calls") - if total_stripped_thinking: - parts.append(f"{total_stripped_thinking} thinking blocks stripped") - mask_msg = ", ".join(parts) if parts else "none" - - logger.info( - f"Loaded {records_in} trajectories, " - f"kept {len(trajectories)} for training " - f"(masked: {mask_msg})" - ) - - if augment and patterns_per_traj: - _log_thinking_augmentation_stats( - n_thinking_variants, - random_strip_thinking_prob, - records_in, - thinking_turns_per_traj, - total_asst_turns_per_traj, - patterns_per_traj, - ) - - return trajectories, error_indices_list, all_tools - - -def _tokenize_samples( - messages_list, - tools_list, - tokenizer, - *, - split_mode: str = "pair", - error_indices_list: list | None = None, - max_length: int | None = None, - num_proc: int | None = None, - no_tools: bool = False, - dump_dir: str | None = None, - dump_n_samples: int = 0, - parse_tool_call_args: bool = False, -): - """Tokenize message lists into a training-ready Dataset. - - Works for both progressive pairs (``split_mode="pair"``) and - full trajectories (``split_mode="trajectory"``). - - In pair mode, only the last assistant turn per sample gets - ``loss_mask=1``. In trajectory mode, all assistant turns get - ``loss_mask=1`` except those at error segment indices. - - Args: - tools_list: Per-sample tool definitions (parallel to - *messages_list*). Each element is either ``None`` or a - list of tool dicts. - """ - if num_proc is None: - num_proc = max(1, min(os.cpu_count() or 1, DATASET_NUM_PROC)) - - # Find representative tools for template detection. - first_tools = None - if tools_list: - first_tools = next((t for t in tools_list if t is not None), None) - - if no_tools: - tools_list = None - first_tools = None - logger.info("Tool definitions disabled (no_tools=True)") - elif first_tools is not None: - all_tool_names = set() - for t_list in tools_list: - if t_list is not None: - for t in t_list: - all_tool_names.add(t.get("function", {}).get("name", "?")) - logger.info(f"Using tools for chat template: {sorted(all_tool_names)}") - - if not messages_list: - raise ValueError("No valid samples to tokenize") - - # Build dataset columns. - data = {"messages": messages_list} - # Serialize per-sample tools as JSON strings for the Dataset column. - data["tools_json"] = ( - [json.dumps(t) if t else "" for t in tools_list] - if tools_list - else [""] * len(messages_list) - ) - remove_cols = ["messages", "tools_json"] - if split_mode == "trajectory": - data["error_indices"] = error_indices_list or [[] for _ in messages_list] - remove_cols.append("error_indices") - - dataset = Dataset.from_dict(data) - _patch_chat_template_for_training(tokenizer) - assistant_pattern = _detect_template_pattern(tokenizer, tools=first_tools) - - # Dump samples for inspection before the heavy map() pass. - if dump_dir and dump_n_samples != 0: - _dump_samples( - messages_list, - tokenizer, - assistant_pattern, - tools_list, - dump_dir, - dump_n_samples, - split_mode=split_mode, - error_indices_list=error_indices_list, - parse_tool_call_args=parse_tool_call_args, - ) - - process_fn = _TokenizeAndMask( - tokenizer, - assistant_pattern, - max_length=max_length, - split_mode=split_mode, - parse_tool_call_args=parse_tool_call_args, - ) - - dataset = dataset.map(process_fn, num_proc=num_proc).remove_columns(remove_cols) - - # Single filter pass: removes both apply_chat_template-failure empties and - # overlength samples (which _TokenizeAndMask also marks as empty). - before_filter = len(dataset) - dataset = dataset.filter(lambda x: len(x["input_ids"]) > 0, num_proc=num_proc) - n_filtered = before_filter - len(dataset) - if n_filtered > 0: - logger.info( - f"Filtered {n_filtered} samples " - f"(empty from template failures or exceeding max_length={max_length})" - ) - - logger.info(f"Final dataset: {len(dataset)} samples") - return dataset - - -def _process_swe_sft( - path: str, - tokenizer, - *, - max_length: int | None = None, - num_proc: int | None = None, - pre_split: bool = False, - filter_errors: bool = True, - strip_all_thinking: bool = False, - filter_empty_tool_calls: bool = False, - filter_bare_text_tool_calls: bool = False, - truncate_task_notifications: bool = False, - no_tools: bool = False, - max_no_thinking_ratio: float | None = None, - split_mode: str = "pair", - random_strip_thinking_prob: float = 0.0, - random_strip_thinking_seed: int = 42, - n_thinking_variants: int = 1, - dump_dir: str | None = None, - dump_n_samples: int = 0, - parse_tool_call_args: bool = False, -): - """Load JSONL, split into pairs, tokenize, and filter. - - Combines file loading with ``_tokenize_samples`` so that the rank-0-only - path and the single-process path share the same logic. - - When *split_mode* is ``"trajectory"``, the full trajectory is kept as a - single training sample with all assistant turns as targets. - """ - error_indices_list = None - - if split_mode == "trajectory": - messages_list, error_indices_list, tools_list = _load_full_trajectories( - path, - filter_errors=filter_errors, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - truncate_task_notifications=truncate_task_notifications, - random_strip_thinking_prob=random_strip_thinking_prob, - random_strip_thinking_seed=random_strip_thinking_seed, - n_thinking_variants=n_thinking_variants, - ) - elif pre_split: - messages_list, tools_list = _load_presplit_pairs( - path, - strip_all_thinking=strip_all_thinking, - random_strip_thinking_prob=random_strip_thinking_prob, - random_strip_thinking_seed=random_strip_thinking_seed, - n_thinking_variants=n_thinking_variants, - ) - else: - messages_list, tools_list = _load_trajectory_pairs( - path, - filter_errors=filter_errors, - strip_all_thinking=strip_all_thinking, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - truncate_task_notifications=truncate_task_notifications, - max_no_thinking_ratio=max_no_thinking_ratio, - random_strip_thinking_prob=random_strip_thinking_prob, - random_strip_thinking_seed=random_strip_thinking_seed, - n_thinking_variants=n_thinking_variants, - ) - - return _tokenize_samples( - messages_list, - tools_list, - tokenizer, - split_mode=split_mode, - error_indices_list=error_indices_list, - max_length=max_length, - num_proc=num_proc, - no_tools=no_tools, - dump_dir=dump_dir, - dump_n_samples=dump_n_samples, - parse_tool_call_args=parse_tool_call_args, - ) - - -def get_swe_sft_dataset( - path: str, - split: str | None = None, - tokenizer=None, - max_length: int | None = None, - num_proc: int | None = None, - pre_split: bool = False, - filter_errors: bool = True, - strip_all_thinking: bool = False, - filter_empty_tool_calls: bool = False, - filter_bare_text_tool_calls: bool = False, - truncate_task_notifications: bool = False, - no_tools: bool = False, - skip_pretokenized_filter: bool = False, - max_no_thinking_ratio: float | None = None, - split_mode: str = "pair", - random_strip_thinking_prob: float = 0.0, - random_strip_thinking_seed: int = 42, - n_thinking_variants: int = 1, - cache_dir: str | None = None, - dump_dir: str | None = None, - dump_samples: int = 0, - parse_tool_call_args: bool = False, -): - """Load SWE trajectory data and convert to SFT training pairs. - - By default, tool definitions are auto-extracted from the training data's - ``conversations[].tools`` field and passed to ``apply_chat_template`` - so that the tokenizer renders tool definitions in the system prompt - (e.g. Qwen3 ``# Tools`` block), matching the eval-time format. - Set *no_tools* to skip this and render without tool definitions. - - When *split_mode* is ``"trajectory"``, the full trajectory is kept as a - single training sample with all assistant turns as targets - (``loss_mask=1``). Error segments are masked (``loss_mask=0``) - when *filter_errors* is True, instead of being discarded. - Thinking is preserved by default but can be randomly stripped - per-turn via *random_strip_thinking_prob* (both modes). - - In distributed (SPMD) mode, only rank 0 performs the heavy processing - (JSONL loading, pair splitting, tokenization) and saves the result as - an Arrow dataset to *cache_dir*. Other ranks wait for rank 0 to - finish and then load the cached dataset directly via memory-mapped I/O. - - Args: - path: Path to the JSONL file containing SWE trajectories, or a - directory containing a pre-tokenized Arrow dataset (saved by - ``python -m areal.dataset.swe_sft --save-tokenized``). - split: Unused, kept for API compatibility. - tokenizer: Tokenizer with ``apply_chat_template`` support. - Not required when loading a pre-tokenized dataset. - max_length: Max token length. Longer sequences are filtered out. - num_proc: Number of parallel workers for tokenization. - Defaults to ``min(os.cpu_count(), DATASET_NUM_PROC)``. - pre_split: If True, treat input as pre-split pairs (each line is - ``{"messages": [...]}``) instead of full trajectories. - filter_errors: If True (default), discard pairs whose current segment - contains a tool result with ``is_error=True``. In trajectory - mode, sets ``loss_mask=0`` for error segments instead. - Set to False to keep/train all regardless of tool errors. - strip_all_thinking: If True, strip ``...`` from every - assistant turn including the training target. - Ignored in trajectory mode (thinking is always preserved). - filter_empty_tool_calls: If True, discard pairs whose training-target - assistant turn has no text content but has tool_calls. - filter_bare_text_tool_calls: If True, discard pairs whose - training-target assistant turn has text without ```` - tags and has tool_calls. - truncate_task_notifications: If True, truncate trajectories at the - first ```` that follows a pure-text assistant - turn, removing noise from background task completions. - no_tools: If True, do not pass tool definitions to - ``apply_chat_template`` even if the data contains them. - skip_pretokenized_filter: If True, skip the ``max_length`` filter - when loading a pre-tokenized dataset. Useful when the dataset - was already filtered during pretokenization and you want to - avoid NFS cache conflicts from concurrent ``dataset.filter()`` - calls across ranks. - max_no_thinking_ratio: Maximum ratio of non-thinking pairs to thinking - pairs. For example, ``1.0`` gives 1:1, ``2.0`` gives 1:2. - ``None`` (default) disables balancing. - split_mode: ``"pair"`` (default) splits trajectories into - progressive pairs. ``"trajectory"`` keeps the full trajectory - as a single sample — all assistant turns are targets with - ``loss_mask=1``, error segments are masked instead of filtered. - random_strip_thinking_prob: Probability of stripping thinking from - each target assistant turn. 0.0 (default) = no stripping, - 1.0 = strip all. Works in both pair and trajectory mode. - random_strip_thinking_seed: Random seed for reproducible thinking - stripping decisions. - n_thinking_variants: Number of thinking-pattern variants per - trajectory. ``1`` (default) = no augmentation. ``K > 1`` - = augment each trajectory into K variants: the first - preserves all thinking, the rest randomly strip with - *random_strip_thinking_prob*. - cache_dir: Directory to save/load the processed Arrow dataset. - When set in distributed mode, rank 0 processes the data and - saves here; other ranks load from this directory. If the - directory already contains a completed cache (``.done`` marker), - all ranks load from it directly without reprocessing. - dump_dir: Directory to write sample dump files (``.txt`` + ``.json``). - Only rank 0 writes. Set to None to disable. - dump_samples: Number of random samples to dump. ``-1`` = all, - ``0`` = disabled. - parse_tool_call_args: If True, convert OpenAI JSON-string - ``tool_calls.arguments`` to dicts before ``apply_chat_template``. - Required by GLM-4.x / GLM-5.x templates; leave at the default - (False) for Qwen / Llama / Bailing. - - Returns: - A HuggingFace ``Dataset`` with ``input_ids`` and ``loss_mask`` columns. - """ - from datasets import load_from_disk - - # Pre-tokenized Arrow dataset: load directly, skip all processing. - if os.path.isdir(path): - logger.info(f"Loading pre-tokenized dataset from {path}") - dataset = load_from_disk(path) - - if max_length is not None and not skip_pretokenized_filter: - before_filter = len(dataset) - dataset = dataset.filter( - lambda x: len(x["input_ids"]) <= max_length, num_proc=num_proc - ) - logger.info( - f"Filtered {before_filter - len(dataset)} samples " - f"exceeding max_length={max_length}" - ) - - logger.info(f"Final dataset: {len(dataset)} samples") - return dataset - - # --- Shared kwargs for _process_swe_sft --- - process_kwargs = dict( - max_length=max_length, - num_proc=num_proc, - pre_split=pre_split, - filter_errors=filter_errors, - strip_all_thinking=strip_all_thinking, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - truncate_task_notifications=truncate_task_notifications, - no_tools=no_tools, - max_no_thinking_ratio=max_no_thinking_ratio, - split_mode=split_mode, - random_strip_thinking_prob=random_strip_thinking_prob, - random_strip_thinking_seed=random_strip_thinking_seed, - n_thinking_variants=n_thinking_variants, - dump_dir=dump_dir, - dump_n_samples=dump_samples, - parse_tool_call_args=parse_tool_call_args, - ) - - # --- Distributed rank-0-only processing --- - rank = int(os.getenv("RANK", "0")) - world_size = int(os.getenv("WORLD_SIZE", "1")) - - if cache_dir is not None and world_size > 1: - done_marker = os.path.join(cache_dir, ".done") - - def _filter_by_max_length(ds): - if max_length is None: - return ds - before = len(ds) - keep = [i for i in range(len(ds)) if len(ds[i]["input_ids"]) <= max_length] - ds = ds.select(keep) - if len(ds) < before: - logger.info( - f"Rank {rank}: filtered {before - len(ds)} samples " - f"exceeding max_length={max_length}" - ) - return ds - - # Fast path: cache from a previous run (or rank 0 already finished). - if os.path.exists(done_marker): - logger.info( - f"Rank {rank}: loading cached processed dataset from {cache_dir}" - ) - dataset = load_from_disk(cache_dir) - dataset = _filter_by_max_length(dataset) - logger.info(f"Final dataset: {len(dataset)} samples") - return dataset - - if rank == 0: - # Rank 0: do the heavy processing and save for other ranks. - dataset = _process_swe_sft(path, tokenizer, **process_kwargs) - os.makedirs(cache_dir, exist_ok=True) - dataset.save_to_disk(cache_dir) - # Write marker AFTER save completes so readers see a consistent dir. - with open(done_marker, "w") as f: - f.write(str(len(dataset))) - logger.info( - f"Rank 0: saved processed dataset " - f"({len(dataset)} samples) to {cache_dir}" - ) - dataset = _filter_by_max_length(dataset) - return dataset - else: - # Other ranks: wait for rank 0, then load. - logger.info(f"Rank {rank}: waiting for rank 0 to process dataset...") - _wait_for_marker(done_marker) - dataset = load_from_disk(cache_dir) - dataset = _filter_by_max_length(dataset) - logger.info(f"Rank {rank}: loaded cached dataset ({len(dataset)} samples)") - return dataset - - # --- Non-distributed or no cache_dir: process in current process --- - return _process_swe_sft(path, tokenizer, **process_kwargs) - - -# ============================================================ -# 7. CLI — ``python -m areal.dataset.swe_sft`` -# ============================================================ - -if __name__ == "__main__": - import argparse - import sys - - from transformers import AutoTokenizer - - parser = argparse.ArgumentParser( - description="Verify SWE SFT pair generation and loss masking.", - ) - parser.add_argument("path", help="Path to SWE trajectory JSONL file") - parser.add_argument( - "--tokenizer", - default="Qwen/Qwen3-8B", - help="HuggingFace tokenizer name or path (default: Qwen/Qwen3-8B)", - ) - parser.add_argument( - "--max-length", - type=int, - default=None, - help="Filter samples exceeding this token length", - ) - parser.add_argument( - "--num-samples", - "-n", - type=int, - default=None, - help="Number of pairs to process. Controls loading, tokenization," - " display, and export. Default: all pairs.", - ) - parser.add_argument( - "--num-proc", - type=int, - default=None, - help=f"Number of parallel workers (default: min(cpu_count, {DATASET_NUM_PROC}))", - ) - parser.add_argument( - "--save-pairs", - "-o", - default=None, - metavar="FILE", - help='Save cleaned pairs to FILE (JSONL, each line: {"messages": [...]}).', - ) - parser.add_argument( - "--pre-split", - action="store_true", - help='Input is already in pair format (each line: {"messages": [...]}).' - " Skip trajectory splitting and error filtering.", - ) - parser.add_argument( - "--no-filter-errors", - action="store_true", - help="Keep pairs whose current segment contains tool results with " - "is_error=True (by default these are discarded).", - ) - parser.add_argument( - "--save-tokenized", - default=None, - metavar="DIR", - help="Save the tokenized dataset to DIR (Arrow format). " - "The saved directory can be used directly as the dataset path " - "during training, skipping all processing.", - ) - parser.add_argument( - "--strip-all-thinking", - action="store_true", - help="Strip ... from ALL assistant turns including " - "the training target. By default only context turns are stripped.", - ) - parser.add_argument( - "--no-tools", - action="store_true", - help="Do not pass tool definitions to apply_chat_template. " - "By default, tools are auto-extracted from the data and rendered " - "in the system prompt (e.g. Qwen3 '# Tools' block).", - ) - parser.add_argument( - "--parse-tool-call-args", - action="store_true", - help="Convert OpenAI JSON-string tool_calls.arguments to dicts " - "before apply_chat_template. Required by GLM-4.x / GLM-5.x " - "templates; leave off for Qwen / Llama / Bailing (which expect " - "the standard string form).", - ) - parser.add_argument( - "--filter-empty-tool-calls", - action="store_true", - help="Discard pairs whose training-target assistant turn has no " - "text content but has tool_calls (silent tool invocations).", - ) - parser.add_argument( - "--filter-bare-text-tool-calls", - action="store_true", - help="Discard pairs whose training-target assistant turn has text " - "content without tags and has tool_calls.", - ) - parser.add_argument( - "--truncate-task-notifications", - action="store_true", - help="Truncate trajectories at the first that " - "follows a pure-text assistant turn. Removes noise from background " - "task completions (e.g. pip install finishing after the model's summary).", - ) - parser.add_argument( - "--max-no-thinking-ratio", - type=float, - default=None, - help="Maximum ratio of non-thinking pairs to thinking pairs. " - "E.g. 1.0 = 1:1 balance, 2.0 = at most 2x non-thinking per " - "thinking pair. Non-thinking pairs are randomly downsampled. " - "Default: no balancing.", - ) - parser.add_argument( - "--split-mode", - choices=["pair", "trajectory"], - default="pair", - help="Sample construction mode. 'pair' (default): split trajectories " - "into progressive pairs. 'trajectory': keep the full trajectory " - "as a single sample with all assistant turns as targets.", - ) - parser.add_argument( - "--random-strip-thinking-prob", - type=float, - default=0.0, - help="Probability of stripping thinking from each target assistant " - "turn. 0.0 = no stripping (default), 1.0 = strip all. " - "Works in both pair mode and trajectory mode.", - ) - parser.add_argument( - "--random-strip-thinking-seed", - type=int, - default=42, - help="Random seed for reproducible thinking stripping decisions (default: 42).", - ) - parser.add_argument( - "--n-thinking-variants", - type=int, - default=1, - help="Number of thinking-pattern variants per trajectory. " - "1 = no augmentation (default). K > 1 = augment each trajectory " - "into K variants: the first preserves all thinking, the rest " - "randomly strip with --random-strip-thinking-prob.", - ) - parser.add_argument( - "--save-trajectories", - default=None, - metavar="FILE", - help="Save preprocessed trajectories to FILE (JSONL, original format) " - "after applying trajectory-level operations (e.g. " - "--truncate-task-notifications) but before pair splitting. " - "Each line preserves the original record structure with the " - "messages field updated.", - ) - parser.add_argument( - "--dump-samples", - default=None, - metavar="DIR", - help="Save sampled pairs to DIR, one file per pair. Each file " - "contains the rendered text and a token-by-token table with " - "token id, decoded text, and loss_mask.", - ) - parser.add_argument( - "--dump-n", - type=int, - default=None, - help="Number of pairs to dump when --dump-samples is set. " - "Default: all pairs. -1 also means all.", - ) - args = parser.parse_args() - - filter_errors = not args.no_filter_errors - strip_all_thinking = args.strip_all_thinking - filter_empty_tool_calls = args.filter_empty_tool_calls - filter_bare_text_tool_calls = args.filter_bare_text_tool_calls - truncate_task_notifications = args.truncate_task_notifications - max_no_thinking_ratio = args.max_no_thinking_ratio - - # --- Fast path: save preprocessed trajectories --- - if args.save_trajectories: - records_in = 0 - records_out = 0 - n_truncated = 0 - with ( - open(args.path, encoding="utf-8") as fin, - open(args.save_trajectories, "w", encoding="utf-8") as fout, - ): - for line in fin: - line = line.strip() - if not line: - continue - record = json.loads(line) - records_in += 1 - - messages, _ = _extract_messages(record, records_in) - - if truncate_task_notifications and messages: - truncated = _truncate_at_task_notification(messages) - if len(truncated) < len(messages): - n_truncated += 1 - _set_messages(record, truncated) - - fout.write(json.dumps(record, ensure_ascii=False) + "\n") - records_out += 1 - - parts = [] - if n_truncated: - parts.append(f"{n_truncated} truncated at task-notification") - op_msg = ", ".join(parts) if parts else "no changes" - print( - f"Saved {records_out}/{records_in} trajectories " - f"to {args.save_trajectories} ({op_msg})" - ) - sys.exit(0) - - # --- Load --- - split_mode = args.split_mode - error_indices_list = None - - if split_mode == "trajectory": - samples, error_indices_list, tools_list = _load_full_trajectories( - args.path, - filter_errors=filter_errors, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - truncate_task_notifications=truncate_task_notifications, - random_strip_thinking_prob=args.random_strip_thinking_prob, - random_strip_thinking_seed=args.random_strip_thinking_seed, - n_thinking_variants=args.n_thinking_variants, - ) - label = "trajectories" - elif args.pre_split: - samples, tools_list = _load_presplit_pairs( - args.path, - strip_all_thinking=strip_all_thinking, - random_strip_thinking_prob=args.random_strip_thinking_prob, - random_strip_thinking_seed=args.random_strip_thinking_seed, - n_thinking_variants=args.n_thinking_variants, - ) - label = "pairs" - else: - samples, tools_list = _load_trajectory_pairs( - args.path, - filter_errors=filter_errors, - strip_all_thinking=strip_all_thinking, - filter_empty_tool_calls=filter_empty_tool_calls, - filter_bare_text_tool_calls=filter_bare_text_tool_calls, - truncate_task_notifications=truncate_task_notifications, - max_no_thinking_ratio=max_no_thinking_ratio, - random_strip_thinking_prob=args.random_strip_thinking_prob, - random_strip_thinking_seed=args.random_strip_thinking_seed, - n_thinking_variants=args.n_thinking_variants, - ) - label = "pairs" - - # --- Slice + stats --- - total = len(samples) - if args.num_samples is not None: - samples = samples[: args.num_samples] - tools_list = tools_list[: args.num_samples] if tools_list else tools_list - if error_indices_list is not None: - error_indices_list = error_indices_list[: args.num_samples] - - print(f"Total {label}: {total}") - if args.num_samples is not None: - print(f"Using: {len(samples)}") - - if samples: - lengths = [len(s) for s in samples] - print( - f"Messages/sample: min={min(lengths)}, " - f"max={max(lengths)}, avg={sum(lengths) / len(lengths):.1f}" - ) - if error_indices_list is not None: - n_masked = sum(len(e) for e in error_indices_list) - print(f"Masked segments: {n_masked} (loss=0)") - - # --- Save cleaned samples as JSONL --- - if args.save_pairs: - with open(args.save_pairs, "w", encoding="utf-8") as fout: - err_iter = error_indices_list or [None] * len(samples) - tl_iter = tools_list if tools_list else [None] * len(samples) - for sample, sample_tools, err_idxs in zip(samples, tl_iter, err_iter): - record = {"messages": sample} - if sample_tools is not None: - record["tools"] = sample_tools - if err_idxs: - record["error_indices"] = err_idxs - fout.write(json.dumps(record, ensure_ascii=False) + "\n") - print(f"Wrote {len(samples)} {label} to {args.save_pairs}") - - # --- Tokenize / Dump --- - dump_dir = args.dump_samples if args.dump_samples else None - need_tokenize = args.save_tokenized - - # When --save-tokenized is set, auto-dump 50 samples alongside it - # unless the user explicitly set --dump-samples or --dump-n 0. - if need_tokenize and not dump_dir and args.dump_n != 0: - dump_dir = os.path.join(args.save_tokenized, "dumped_samples") - - if not need_tokenize and not dump_dir: - sys.exit(0) - - tok = AutoTokenizer.from_pretrained(args.tokenizer, trust_remote_code=True) - _patch_chat_template_for_training(tok) - if args.dump_n is not None: - dump_n = args.dump_n - elif args.dump_samples: - # Explicit --dump-samples without --dump-n: dump all - dump_n = -1 if args.num_samples is None else args.num_samples - elif need_tokenize: - # Auto-dump with --save-tokenized: default 50 - dump_n = 50 - else: - dump_n = -1 - - # Dump can run independently without full tokenization. - if dump_dir and dump_n != 0: - dump_tools = None if args.no_tools else tools_list - first_tools = None - if dump_tools: - first_tools = next((t for t in dump_tools if t is not None), None) - assistant_pattern = _detect_template_pattern(tok, tools=first_tools) - _dump_samples( - samples, - tok, - assistant_pattern, - dump_tools, - dump_dir, - dump_n, - split_mode=split_mode, - error_indices_list=error_indices_list, - parse_tool_call_args=args.parse_tool_call_args, - ) - - if not need_tokenize: - sys.exit(0) - - ds = _tokenize_samples( - samples, - tools_list, - tok, - split_mode=split_mode, - error_indices_list=error_indices_list, - max_length=args.max_length, - num_proc=args.num_proc, - no_tools=args.no_tools, - parse_tool_call_args=args.parse_tool_call_args, - ) - - print(f"\nTokenized: {len(ds)} samples") - if args.save_tokenized: - ds.save_to_disk(args.save_tokenized) - print(f"Saved tokenized dataset ({len(ds)} samples) to {args.save_tokenized}") From 9cf6852d3a670f0c2eedbb96feb0b172d8db4b8a Mon Sep 17 00:00:00 2001 From: sitabulaixizawaluduo <71002153+sitabulaixizawaluduo@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:10:47 +0800 Subject: [PATCH 34/36] fix(docker): rename zeroclaw installer flag to --skip-quickstart (#1469) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7c3e1f8239..5d2f6f5fe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -264,7 +264,7 @@ RUN set -ex \ ENV PATH="/root/.cargo/bin:$PATH" RUN curl -fsSL \ https://github.com/zeroclaw-labs/zeroclaw/releases/latest/download/install.sh \ - | bash -s -- --skip-onboard \ + | bash -s -- --skip-quickstart \ && rm -rf /tmp/* ############################################################## From 89e05acb462406879c8786fb77445857d52843dc Mon Sep 17 00:00:00 2001 From: zhangjw <90502403+Le8r0nJames@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:58:13 +0800 Subject: [PATCH 35/36] fix(megatron): use dp_reshardable optimizer sharding and load optimizer state correctly (#1468) Since the dist_checkpointing refactor in megatron-core v0.14, flattened_range is legacy, non-reconstructable metadata and is no longer supported as a persistable layout in the checkpoint serialization path; the remaining flattened_range code paths were removed upstream in Megatron-LM PR #2126, and ShardedTensor.validate_metadata_integrity() now rejects any ShardedTensor with flattened_range set. The sharded_state_dict API still defaults to fully_sharded_model_space, which emits flattened_range, so saving a checkpoint with optimizer state fails on the pinned megatron-core 0.17.0. Request dp_reshardable sharding instead, matching the optimizer state format upstream now uses by default. Also pass is_loading=True when building the load-side template. Without it a freshly built optimizer skips megatron-core's state pre-allocation, the template only requests "param", and DCP silently drops exp_avg/exp_avg_sq on resume -- training continues with a reset optimizer state at full learning rate, which we observed to cause gradient-norm spikes and entropy collapse within ~70 steps after recovery in a large-scale RL run. --- areal/engine/megatron_utils/checkpointer.py | 22 ++++++++++-- tests/test_megatron_async_save.py | 39 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/areal/engine/megatron_utils/checkpointer.py b/areal/engine/megatron_utils/checkpointer.py index 580e2bd6a2..6bdc55e2e0 100644 --- a/areal/engine/megatron_utils/checkpointer.py +++ b/areal/engine/megatron_utils/checkpointer.py @@ -278,6 +278,7 @@ def generate_state_dict( with_model: bool = True, with_optimizer: bool = True, with_rng: bool = True, + is_loading: bool = False, ): # For save dist checkpointing state_dict = {} @@ -298,7 +299,24 @@ def generate_state_dict( # Optimizer State Dict if with_optimizer: torch.distributed.barrier() - optimizer_sharded_states = self.optimizer.sharded_state_dict(state_dict) + # megatron-core v0.14+ removed flattened_range support (Megatron-LM + # PR #2126), but the sharded_state_dict default + # (fully_sharded_model_space) still emits it, so saving optimizer + # state fails on the pinned 0.17.0. dp_reshardable is upstream's + # current default. Trade-off: the optimizer state (not the model + # weights) becomes reshardable only along DP -- load hard-asserts + # the same bucket layout (per_bucket_numel_unpadded), so save and + # load must use identical TP/PP. Fine today since recover enforces + # the same topology; switch to fully_reshardable if cross-topology + # resume is ever needed (also flattened_range-free, at the cost of + # gathering optimizer state). is_loading=True pre-allocates + # exp_avg/exp_avg_sq so the load template requests them -- + # otherwise DCP silently drops the moments on resume. + optimizer_sharded_states = self.optimizer.sharded_state_dict( + state_dict, + is_loading=is_loading, + metadata={"distrib_optim_sharding_type": "dp_reshardable"}, + ) state_dict["optimizer"] = optimizer_sharded_states if self.lr_scheduler is not None: @@ -353,7 +371,7 @@ def load_checkpoint( # Get State Dict for loading sharded_state_dict = self.generate_state_dict( - with_model, with_optimizer, with_rng + with_model, with_optimizer, with_rng, is_loading=True ) # Load Dist Checkpointing state_dict = load_dist_checkpointing( diff --git a/tests/test_megatron_async_save.py b/tests/test_megatron_async_save.py index 93044464cf..b7041c0e83 100644 --- a/tests/test_megatron_async_save.py +++ b/tests/test_megatron_async_save.py @@ -260,3 +260,42 @@ def test_sync_save_emits_no_async_metrics(patched_checkpointer, tmp_path): manager.save_checkpoint(str(tmp_path / "step0")) stats_scalar.assert_not_called() + + +def test_generate_state_dict_requests_dp_reshardable_sharding(patched_checkpointer): + _, manager, _ = patched_checkpointer + + with patch("torch.distributed.barrier"): + state_dict = manager.generate_state_dict( + with_model=False, with_optimizer=True, with_rng=False + ) + + kwargs = manager.optimizer.sharded_state_dict.call_args.kwargs + assert kwargs["metadata"] == {"distrib_optim_sharding_type": "dp_reshardable"} + assert kwargs["is_loading"] is False + assert "optimizer" in state_dict + + +def test_load_checkpoint_builds_optimizer_template_with_is_loading( + patched_checkpointer, tmp_path +): + mod, manager, _ = patched_checkpointer + + with ( + patch("os.path.exists", return_value=True), + patch("torch.distributed.barrier"), + patch.object( + mod, "load_dist_checkpointing", return_value={"optimizer": {"step": 1}} + ), + ): + manager.load_checkpoint( + str(tmp_path / "step0"), + with_model=False, + with_optimizer=True, + with_rng=False, + ) + + kwargs = manager.optimizer.sharded_state_dict.call_args.kwargs + assert kwargs["is_loading"] is True + assert kwargs["metadata"] == {"distrib_optim_sharding_type": "dp_reshardable"} + manager.optimizer.load_state_dict.assert_called_once_with({"step": 1}) From 40e7928b02b84c6536728aa72418ee4902615306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=93=E8=B7=AF=E8=87=AA=E4=BF=A1?= <182356120+daoluzixin@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:14:28 +0800 Subject: [PATCH 36/36] feat(workflow): add stage-1 stepwise reward plumbing Introduce a structured reward result that can carry per-step rewards alongside the final scalar reward, align stepwise rewards to completion tokens in RLVRWorkflow, and let PPO consume the injected token-level process rewards without changing the default scalar-only path. Key changes: - add RewardResult and scalar-to-structured normalization helpers - emit step_rewards and step_reward_mask tensors from RLVRWorkflow - inject stepwise rewards into PPO total rewards with logging support - add focused tests for reward normalization and stepwise alignment --- areal/api/__init__.py | 4 + areal/api/reward_api.py | 40 +++++ areal/trainer/ppo/actor.py | 19 +++ areal/workflow/rlvr.py | 109 +++++++++++--- tests/test_stepwise_reward_stage1.py | 215 +++++++++++++++++++++++++++ 5 files changed, 366 insertions(+), 21 deletions(-) create mode 100644 tests/test_stepwise_reward_stage1.py diff --git a/areal/api/__init__.py b/areal/api/__init__.py index 3401bf8b89..b7e7316776 100644 --- a/areal/api/__init__.py +++ b/areal/api/__init__.py @@ -3,6 +3,8 @@ __all__ = [ "RolloutWorkflow", "AsyncRewardWrapper", + "RewardResult", + "normalize_reward_result", "TrainEngine", "InferenceEngine", "Scheduler", @@ -49,6 +51,8 @@ "WorkflowLike": "areal.api.workflow_api", "AgentWorkflow": "areal.api.workflow_api", "AsyncRewardWrapper": "areal.api.reward_api", + "RewardResult": "areal.api.reward_api", + "normalize_reward_result": "areal.api.reward_api", "RolloutWorkflow": "areal.api.workflow_api", } diff --git a/areal/api/reward_api.py b/areal/api/reward_api.py index 44bef1f692..5e05d49804 100644 --- a/areal/api/reward_api.py +++ b/areal/api/reward_api.py @@ -7,7 +7,9 @@ from collections.abc import Callable from concurrent.futures import ProcessPoolExecutor from concurrent.futures.process import BrokenProcessPool +from dataclasses import dataclass, field from functools import partial +from typing import Any from areal.utils import logging @@ -59,6 +61,44 @@ def reward_fn( """ +@dataclass +class RewardResult: + """Structured reward payload for workflows that expose process rewards. + + Parameters + ---------- + final_reward: + Sequence-level terminal reward. This is the compatibility anchor used + by existing workflows and PPO code paths. + step_rewards: + Optional reward value for each process step. + step_ends: + Optional 1-based completion-token end offsets for each step. Each entry + points to the completion token position where the corresponding + ``step_rewards`` value should be injected. + metadata: + Optional debug-only payload for logging or inspection. This field is + not consumed by the trainer in Stage 1. + """ + + final_reward: float + step_rewards: list[float] | None = None + step_ends: list[int] | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + +def normalize_reward_result(value: float | RewardResult) -> RewardResult: + """Normalize legacy scalar rewards into ``RewardResult``. + + This keeps existing reward functions working unchanged while allowing new + implementations to return a structured process-reward payload. + """ + + if isinstance(value, RewardResult): + return value + return RewardResult(final_reward=float(value)) + + class AsyncRewardWrapper: """Wraps a synchronous reward function for async execution with timeout and retries. diff --git a/areal/trainer/ppo/actor.py b/areal/trainer/ppo/actor.py index 0da045557f..333f23b92f 100644 --- a/areal/trainer/ppo/actor.py +++ b/areal/trainer/ppo/actor.py @@ -210,6 +210,16 @@ def _compute_advantages( seq_no_eos_mask = seqlens == attn_mask.shape[1] rewards = -self.kl_ctl * self.kl_estimator(old_logp, ref_logp) kl_rewards = rewards.clone() + step_rewards = data.get("step_rewards") + step_reward_mask = data.get("step_reward_mask") + if step_rewards is not None: + if step_reward_mask is None: + raise ValueError( + "step_reward_mask is required when step_rewards is provided" + ) + rewards = rewards + step_rewards.to(rewards.dtype) * step_reward_mask.to( + rewards.dtype + ) # KL rewards at the next token after eos is zero. rewards[batch_indices, seqlens - 1] = 0 indices = torch.clip(seqlens - 2, min=0) @@ -306,6 +316,15 @@ def _ppo_update(self, data: dict[str, Any]) -> None: final_reward=data["tot_rewards"], ) stats_tracker.stat(**stats, denominator="n_valid_tokens") + if step_rewards is not None and step_reward_mask is not None: + step_reward_values = step_rewards.float() * step_reward_mask.float() + stats_tracker.stat( + step_reward=step_reward_values, + denominator="n_valid_tokens", + ) + stats_tracker.scalar( + step_reward_events=float(step_reward_mask.sum().item()), + ) prompt_lens = data["attention_mask"].sum(-1) - data["loss_mask"].sum(-1) seq_stats = dict( diff --git a/areal/workflow/rlvr.py b/areal/workflow/rlvr.py index aa8e28a9a6..365d0ca479 100644 --- a/areal/workflow/rlvr.py +++ b/areal/workflow/rlvr.py @@ -1,22 +1,22 @@ # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import uuid from collections.abc import Callable -from typing import Any +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any import torch from transformers import PreTrainedTokenizerFast -from areal import workflow_context from areal.api import ( AsyncRewardWrapper, - InferenceEngine, - ModelRequest, - ModelResponse, + RewardResult, RolloutWorkflow, + normalize_reward_result, ) -from areal.api.cli_args import GenerationHyperparameters -from areal.utils import logging, stats_tracker +from areal.utils import logging from areal.utils.dynamic_import import import_from_string from areal.utils.hf_utils import apply_chat_template from areal.utils.perf_tracer import ( @@ -25,9 +25,22 @@ trace_session, ) +if TYPE_CHECKING: + from areal.api.engine_api import InferenceEngine + from areal.api.io_struct import ModelResponse + logger = logging.getLogger("RLVRWorkflow") +@dataclass +class _WorkflowModelRequest: + rid: str + input_ids: list[int] + gconfig: Any + tokenizer: PreTrainedTokenizerFast | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + def default_get_input_ids_fn( data: Any, tokenizer: PreTrainedTokenizerFast, @@ -52,7 +65,7 @@ class RLVRWorkflow(RolloutWorkflow): def __init__( self, reward_fn: Callable[..., Any] | str, - gconfig: GenerationHyperparameters, + gconfig: Any, tokenizer: PreTrainedTokenizerFast | str, enable_thinking: bool = False, get_input_ids_fn: Callable[[Any, PreTrainedTokenizerFast, bool], list[int]] @@ -86,7 +99,7 @@ async def _compute_rewards( resp: ModelResponse, prompt_str: str, task_data: dict[str, Any], - ) -> float: + ) -> RewardResult: """Decode completion and compute reward. Traces reward phase execution for SessionTracer. Decodes output tokens @@ -94,8 +107,8 @@ async def _compute_rewards( Returns ------- - float - Reward value. + RewardResult + Reward payload. """ completions_str = self.tokenizer.decode(resp.output_tokens) reward = await self.async_reward_fn( @@ -106,16 +119,16 @@ async def _compute_rewards( **task_data, ) - return reward + return normalize_reward_result(reward) @session_context() async def _collect_samples( self, - engine: InferenceEngine, - req: ModelRequest, + engine: "InferenceEngine", + req: _WorkflowModelRequest, prompt_str: str, task_data: dict[str, Any], - ) -> tuple[ModelResponse, float]: + ) -> tuple["ModelResponse", RewardResult]: """Generate one sample and compute its reward. Registers a new session for this sample, calls engine.agenerate, @@ -124,20 +137,69 @@ async def _collect_samples( Returns ------- - tuple[ModelResponse, float] - Model response and reward value. + tuple[ModelResponse, RewardResult] + Model response and reward payload. """ async with atrace_session_phase("generate"): resp = await engine.agenerate(req) reward = await self._compute_rewards(resp, prompt_str, task_data) - stats_tracker.get(workflow_context.stat_scope()).scalar(reward=reward) + from areal.infra import workflow_context + from areal.utils import stats_tracker + + tracker = stats_tracker.get(workflow_context.stat_scope()) + tracker.scalar(reward=reward.final_reward) + if reward.step_rewards is not None: + tracker.scalar( + step_reward_count=len(reward.step_rewards), + step_reward_sum=float(sum(reward.step_rewards)), + ) return resp, reward + def _build_stepwise_reward_tensors( + self, + reward: RewardResult, + resp: "ModelResponse", + ) -> tuple[torch.Tensor, torch.Tensor]: + """Convert stepwise rewards into completion-aligned tensors. + + Returns tensors of shape ``[seqlen]`` aligned to the full prompt + + completion sequence. Reward values are injected at the completion token + positions indicated by ``reward.step_ends``. + """ + + seq_len = resp.input_len + resp.output_len + step_rewards = torch.zeros(seq_len, dtype=torch.float32) + step_reward_mask = torch.zeros(seq_len, dtype=torch.bool) + + if reward.step_rewards is None and reward.step_ends is None: + return step_rewards, step_reward_mask + if reward.step_rewards is None or reward.step_ends is None: + raise ValueError( + "step_rewards and step_ends must either both be provided or both be None" + ) + if len(reward.step_rewards) != len(reward.step_ends): + raise ValueError( + "step_rewards and step_ends must have the same length, got " + f"{len(reward.step_rewards)} and {len(reward.step_ends)}" + ) + + completion_len = resp.output_len + for step_reward, step_end in zip(reward.step_rewards, reward.step_ends): + if step_end <= 0 or step_end > completion_len: + raise ValueError( + f"Invalid step_end={step_end}; expected 1 <= step_end <= {completion_len}" + ) + seq_index = resp.input_len + step_end - 1 + step_rewards[seq_index] += float(step_reward) + step_reward_mask[seq_index] = True + + return step_rewards, step_reward_mask + async def arun_episode( - self, engine: InferenceEngine, data: dict[str, Any] + self, engine: "InferenceEngine", data: dict[str, Any] ) -> dict[str, torch.Tensor]: # NOTE: load reward function dynamically if given as string if isinstance(self.reward_fn, str): @@ -149,7 +211,7 @@ async def arun_episode( self.tokenizer, self.enable_thinking, ) - req = ModelRequest( + req = _WorkflowModelRequest( rid=uuid.uuid4().hex, input_ids=input_ids, gconfig=self.gconfig.new(n_samples=1), @@ -166,6 +228,9 @@ async def arun_episode( logprobs = [0.0] * resp.input_len + resp.output_logprobs loss_mask = [0] * resp.input_len + [1] * resp.output_len versions = [-1] * resp.input_len + resp.output_versions + step_rewards, step_reward_mask = self._build_stepwise_reward_tensors( + reward, resp + ) res = { "input_ids": torch.tensor(seq, dtype=torch.int32), @@ -173,6 +238,8 @@ async def arun_episode( "logprobs": torch.tensor(logprobs, dtype=torch.float32), "versions": torch.tensor(versions, dtype=torch.int32), "attention_mask": torch.ones(len(seq), dtype=torch.bool), - "rewards": torch.tensor(reward, dtype=torch.float32), + "rewards": torch.tensor(reward.final_reward, dtype=torch.float32), + "step_rewards": step_rewards, + "step_reward_mask": step_reward_mask, } return {k: v.unsqueeze(0) for k, v in res.items()} diff --git a/tests/test_stepwise_reward_stage1.py b/tests/test_stepwise_reward_stage1.py new file mode 100644 index 0000000000..381ba6820d --- /dev/null +++ b/tests/test_stepwise_reward_stage1.py @@ -0,0 +1,215 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import contextlib +import sys +import types + +import pytest +import torch + +from areal.api.reward_api import normalize_reward_result +from areal.api.reward_api import RewardResult + + +@contextlib.asynccontextmanager +async def _dummy_atrace_session_phase(_name): + yield + + +def _dummy_trace_session(_name): + def decorator(fn): + return fn + + return decorator + + +def _dummy_session_context(): + def decorator(fn): + return fn + + return decorator + + +_perf_tracer_stub = types.ModuleType("areal.utils.perf_tracer") +_perf_tracer_stub.atrace_session_phase = _dummy_atrace_session_phase +_perf_tracer_stub.trace_session = _dummy_trace_session +_perf_tracer_stub.session_context = _dummy_session_context +sys.modules.setdefault("areal.utils.perf_tracer", _perf_tracer_stub) + + +class _DummyTracker: + def scalar(self, **kwargs): + return None + + +_stats_tracker_stub = types.ModuleType("areal.utils.stats_tracker") +_stats_tracker_stub.get = lambda _scope: _DummyTracker() +sys.modules.setdefault("areal.utils.stats_tracker", _stats_tracker_stub) + +_workflow_context_stub = types.ModuleType("areal.infra.workflow_context") +_workflow_context_stub.stat_scope = lambda: "rollout" +sys.modules.setdefault("areal.infra.workflow_context", _workflow_context_stub) + +_infra_stub = types.ModuleType("areal.infra") +_infra_stub.workflow_context = _workflow_context_stub +sys.modules.setdefault("areal.infra", _infra_stub) + +from areal.workflow.rlvr import RLVRWorkflow + + +class _DummyTokenizer: + eos_token_id = 0 + pad_token_id = 0 + + def decode(self, token_ids): + return "|".join(str(x) for x in token_ids) + + +class _DummyGConfig: + def __init__(self, max_new_tokens=8, n_samples=1): + self.max_new_tokens = max_new_tokens + self.n_samples = n_samples + + def new_with_stop_and_pad_token_ids(self, _tokenizer): + return self + + def new(self, n_samples=1): + return _DummyGConfig( + max_new_tokens=self.max_new_tokens, + n_samples=n_samples, + ) + + +class _DummyModelResponse: + def __init__( + self, + input_tokens, + output_tokens, + output_logprobs, + output_versions, + stop_reason="stop", + tokenizer=None, + ): + self.input_tokens = input_tokens + self.output_tokens = output_tokens + self.output_logprobs = output_logprobs + self.output_versions = output_versions + self.stop_reason = stop_reason + self.tokenizer = tokenizer + + @property + def input_len(self): + return len(self.input_tokens) + + @property + def output_len(self): + return len(self.output_tokens) + + +class _DummyEngine: + async def agenerate(self, req): + return _DummyModelResponse( + input_tokens=req.input_ids, + output_tokens=[11, 12, 13, 14], + output_logprobs=[-0.1, -0.2, -0.3, -0.4], + output_versions=[1, 1, 1, 1], + stop_reason="stop", + tokenizer=req.tokenizer, + ) + + +def _dummy_get_input_ids_fn(_data, _tokenizer, _enable_thinking): + return [101, 102] + + +def _scalar_reward_fn(prompt, completions, prompt_ids, completion_ids, **kwargs): + del prompt, completions, prompt_ids, completion_ids, kwargs + return 1.25 + + +def _stepwise_reward_fn(prompt, completions, prompt_ids, completion_ids, **kwargs): + del prompt, completions, prompt_ids, completion_ids, kwargs + return RewardResult( + final_reward=0.5, + step_rewards=[0.2, -0.1], + step_ends=[2, 4], + metadata={"num_steps": 2}, + ) + + +def _bad_reward_fn(prompt, completions, prompt_ids, completion_ids, **kwargs): + del prompt, completions, prompt_ids, completion_ids, kwargs + return RewardResult( + final_reward=0.0, + step_rewards=[1.0], + step_ends=[5], + ) + + +class TestRewardResultNormalization: + def test_normalize_scalar_reward(self): + reward = normalize_reward_result(1.5) + assert reward == RewardResult(final_reward=1.5) + + def test_normalize_reward_result_passthrough(self): + reward = RewardResult(final_reward=2.0, step_rewards=[0.1], step_ends=[1]) + assert normalize_reward_result(reward) is reward + + +class TestRLVRWorkflowStepwiseReward: + @pytest.mark.asyncio + async def test_scalar_reward_keeps_zero_stepwise_tensors(self): + workflow = RLVRWorkflow( + reward_fn=_scalar_reward_fn, + gconfig=_DummyGConfig(max_new_tokens=8), + tokenizer=_DummyTokenizer(), + enable_thinking=False, + get_input_ids_fn=_dummy_get_input_ids_fn, + ) + + result = await workflow.arun_episode(_DummyEngine(), {"messages": []}) + + assert torch.equal(result["rewards"], torch.tensor([1.25], dtype=torch.float32)) + assert torch.equal( + result["step_rewards"], torch.zeros((1, 6), dtype=torch.float32) + ) + assert torch.equal( + result["step_reward_mask"], torch.zeros((1, 6), dtype=torch.bool) + ) + + @pytest.mark.asyncio + async def test_stepwise_reward_aligns_to_completion_end_positions(self): + workflow = RLVRWorkflow( + reward_fn=_stepwise_reward_fn, + gconfig=_DummyGConfig(max_new_tokens=8), + tokenizer=_DummyTokenizer(), + enable_thinking=False, + get_input_ids_fn=_dummy_get_input_ids_fn, + ) + + result = await workflow.arun_episode(_DummyEngine(), {"messages": []}) + + assert torch.equal(result["rewards"], torch.tensor([0.5], dtype=torch.float32)) + expected_step_rewards = torch.tensor( + [[0.0, 0.0, 0.0, 0.2, 0.0, -0.1]], dtype=torch.float32 + ) + expected_mask = torch.tensor( + [[False, False, False, True, False, True]], dtype=torch.bool + ) + assert torch.equal(result["step_rewards"], expected_step_rewards) + assert torch.equal(result["step_reward_mask"], expected_mask) + + @pytest.mark.asyncio + async def test_invalid_stepwise_reward_raises(self): + workflow = RLVRWorkflow( + reward_fn=_bad_reward_fn, + gconfig=_DummyGConfig(max_new_tokens=8), + tokenizer=_DummyTokenizer(), + enable_thinking=False, + get_input_ids_fn=_dummy_get_input_ids_fn, + ) + + with pytest.raises(ValueError, match="Invalid step_end"): + await workflow.arun_episode(_DummyEngine(), {"messages": []})

&3){c>BHT`rL?c|6>-Ph%6?< zIuxON#Qq~Q_+naKCaw#Ix-8-?SF2USA=q+|@X;2*=S~s6v$RWLk9{FkEz+y0Oe!?F zb^&m0*eaKdi1R_{S`ij)r1{WfN_kJJ<(^1NyZPsg5ee24A}|!6F}}BBpxylyymj>a z3yEL7jQ_po>$B}}#{cVR(bpHjhi_!_dAR$T-rr#1ZQ48^7QNX7ycHEV(zL;8v!akw zrg)=XpjZ*iwp%67H=Z)>I;j@!Tk#1cL33|fcN5$P;oGb!{X1(m%}epLGu-}7(L1xI z{q(_CV_{?hSMVUJFj*#a8eVKnP#r4W7O%?0n9*#|AlgS-i>l_dsL_X=I_B$Alw4?s zOeHS6`%gdDQu=dFI$$h&k*$8V9P0=PXBKtL5@(G>5#{if)RYm5j||!OrkWv{{K!JE z?X2LE)86utVBDPZzoFs@I1+|}s`JmuBLuXMT|h}e+<xS9uZoNa^9|oMSH6MdPvB)eqWZ{tDV6tWZ`%kbWX>2`jaferMU7WM zKsRA{(t0vTDz3bY4!5zG<}-<-E)h|7y!L{M9G|VCRJGgJ)LroR%lxeINY(rIhkEcg z)R+eNNr2Hvi&`#zYEgp)nGw+1%1ho#F9zKbN2iqBFrogo>i3b+L$WQf_NNx>fR2L< zKk;9v`TjS*t3~2=BMyU}rC<@CAqK%ct>o{ocPjxVFSn@8?~kG5dE_kbCkuLkck)9Y zjmjPeK7J2;c6g0m@qBv!y=?W5wx=~jvRRHmQRe@3K*u# zoxjE8evl0#1r{FcPa~o33p)n?omgYn?@Ka~Xyxt2-OAC3<_y1LZJr?P~%sU}vYItL-q^ zOO||{s_jyDANM694S?qT#_eq@%vVSF;des9xs=o$ePjntXwFz){Gc&E=cedSh-{qu zJm6Hg9lPmKK=;H|7ivN+lnnQnJ2ziH!l8i-I`e;Cj6iQT1+fiEvvACZBygJzOs$HL za?IvXY$oLWm5(S#Vk5CB8g55mjoOZSk}sPHd1CuI1Ugeo^0_idLOJC!*^(E}Lq3@o zbzm-|Yt2d&!k@*a62xBIfo9|3N=1ny;WWWLU%-Wi1!6H2+H8~0No*NQmEs;M>W$g3 zHO=}eiabeoSs28YvNB09HHWv9=)2RjMZQ3mmD&UdeYan$o2MQQI$Q9nVAChy z^7EzZyYb;EM#y4~E8EQJGM{?GNlnicK@1%d}Ar78gWO8nI(nmS;#5B>vPxaccv7^n-;XubP7nqMNsB(;ZA! zpv`Vv^p?V;bL4-yY9l{Dfm=IIYI{F=xL=;u9EAV+SO+fnLzX+Uz&$F(mvGIy?J`0X(o9^!^Haiq~EVKJ$iL z2W8Rxm3uYgVVyXa>MCF74Z_>Y>vP*05g_GaO6s_ZY_NPYQUz$3boVKi>-#v4Y>k$V z!%FIdN~v5n$`LW9H6#c@^Zsr;`vgBAIdUdCiw0@5MiGr6z!_Lo-=^*Nu28Aaw#EIa zqLeNW+v%L*#uDzoC3*hIh4aInm*cT&qfog=RE=RAk3^1#q0A@8g zjsLn0^9T|hw5+8g&|p=0|2L5rOLU@Ra0-|n+Qq9JF!Lp9=lyBB@wKb#oZnb?zi^|2;1 zWW70wKNRUk9HzvsfeUbK%gV&XC=N!=^@{~CS-Y-s_22`atLeO-F6t(@Aq!@BXH_)) z8!~%IeA$sC96r6X79}f^H8x_l;8p7|+n;hDs?9p~EDt5a zCC|p-u^Wm9Qf(+~CH*Aly%JG;OB9z(UI~g;62G6!mJysS?Y$zr6d^;nOkOpsde|xI z6(mS@yZQHCs{!fZp;v9S7c-?akMvIX;Avu7%c#OD;9qO5ZFgYl4#B`Oo05R`U9T3c zr@zci+8K(|26et)_wlq|uhV9=-(S76iVxeSYTNU_UhSm#DPA7A65dZN+-LGgpK{2} zz_5kjbCba5H4#0|^lLQ!Wu}a3HNVSW{Ot~Ltq9tk14c5>2 zKRBtj5U&NhW6hPhfMxPDB+UL!uYP zhRGCwBOL1$^QMv-*U^|Xa@oH9W4Q6k5YTD1*c~|EX*J*J-|rv}W65nD?|W-K8xuh? z%~xe67*}|=Wi}nJqP0Ton{gjU4jA{JN)bP-7k8@MKa@!QOYw(T_Mww5+cO)sh_-um zo2o?L#9CZ^tWq;<7i~6HyWBlnsp+~UIoEPx^}A5q*}DflF)Vw0-h7(}JH4J9jOPk( zJo$Ji!L9FtbJT=3+BQDz%#FNugc)%i)a1Hq&~@8RmiBeoF6yKS)U!SQ{#;aw%cS*+g;PFv;Se_)69=vq)PfDK$SxRHma4ZZ2 zoQleED7TWhXpBJc_ov(dncaH&mzp4?L80BV$8{h4Z&OoUvrbzh)rcB(7m(=4QQd)kBQ zNJY!Y|Mn|lZspU;Q`=}I~Z9ZpOn zCpIt(^?o`E+yNJausHRwzc1x2%imHEUF+@zz$)&rkq&^$OPW=13Ak{&ht*S0qkhEC zI&Li#e{XC^hi$rEP$hx%{VWSON`f@GgPB4 zkrQ7UDw(h^z`H)(Q2>jJ@9n!BNbI{M?&_Jy4^ZUzDb{1mkPT&C^j87oJC{~g{e&+tEdLxn#Yo79l~!2|gfXX>#Yri+H)NGqdV)Ad`RTxxu=*R6-BtR>{VY6g~Z@%n`k z@qdr|8=H47oqO7kfKd{W+mDEeJAha}_j-J6=oUso5fkT*Yjs~T?NK_?G2aMqw1OR9 z1pB>=EN?_CZz$Up68q0reGq+ZkuXN3?P< z#{S~8my%qXdKaa7+AHSouQ#nC|#1oSam-JFUO#&zgT=>{PsP;@o_-~Xtt z+i}r`!t=+7ehGG`%c=?K_1|0+GNMZ|oy_$^Dq7Hwg_|c@esLUQax_^rP%6thS_oX>~?f$$AgcC;)z~>>-y zHo|{O{nuOi{<{4>-pO|61c*#`Oi91Z&2+$cX9Cv zkdHQxkA&ZOH%iZpGywg`Ls>$z>I~@XX4&`eJ7l{-zGBXjR z;Su7qz@sn$e5;DIX4)uVCzIQs4N-ke`(oI6x^X{oiq`saaeV*#0c3D2{=9Yp_S#OG z6FFj%x=w8Mr}lB4ZQZ`~S@`DrA>gmj68)Ggu~SkNU{CD*7=cm@MF{f^+BhzgyA2`e znvw}I-GvTVM#od+ykwDU)isr2uFb>kie5CBP0s1VoeN`(m<>EOZZ~sUHnx4H_*}0< zcK@2V6_JuUCglAVba(y22W&ySd|Xeket(!uA3h0rV13{h@fCLQ2(&c~%;v^Ng}epX zP-Ihpy%^hz<$*Y!QNxNK=cD$TsmL(}p==MI((vix63%6E9F*dc_(^d~SuX?p3yk7! zG-&E&xpku@D`us6NG$b3!)>FK6_~YuVAyhs0{_=ZNn2JO{5`2EG%f|VDKxpwVl60? zgu4vXg@(yf43b=|EH*0wq^EQw)nUMZ+JkbbNeys9h0_mlTvRlVG#4Ut*j2q^2hqll zVYRk=rlLxo7~boF&zqW41Eh}XHLUFV+RVD$1p8@F@T$;+e_ZJLsfpo2L`;%CDZw4a z{t>|b$&#L^=Q@Q83ddqh2IZqbeHkq#rKRTeeDt|&$d3H-b~lurB1m4CE6A{I7uD_X zSJ?5ycV+$l{7&@#*B?Fd+IOvi1qh~=aK~l*+0}}uQ7V1_HhWtIkzqkHyT+Ji1s*Rm zz7+Tma~eD|zKx*_vJ+%I9On?)NU&ola;r!xF$Yx0lWOnD@pE2fIK zk7gj^r|4BXnNop>rWFwig}cR1zR7}1nT;M8KGb(HyeYmyK%l)NkK_!p0$=6s-Bnm2 zjEmNu5YH57qMUH=3nj+ZzAwq-3b$L=N%_-;lV)|YOM04UfhiMOFuZBv4wz-;auz1K z4GRVic+%+nqE1hyHtHz6{*bo*x?Fd1CNq1UxSiMfFQX#k9xx96xb{jZX08V&KFr8zN?zwj`XvZZLjVYVs@f0G#Lfzm9rT?Tt{_ZL#4lpX_?t|K4jLjnX=Oz5huqz&k$gT?lU>e7S z1&1@A19#b}|9MJ!GBqlD7aMhcBk1QQ53ya=Ur4TyyIaxyNp7o0_r*;zu@Rmsg;?- zhGbQ2>Ye?wtQUM-Y&dGQlhmrTnL-I-QG1YJQ61ajuU9=+$WNaRHv(-14tV{B1B2%DAD=J1Qsy)1vw8(39V@0OSR)R?*)TCIAFAlr@M{m0WIhc=-=jiAUG-gu6O3k#> ze}@tDW`har>u7jfjV*6ZCP%qevuNOQNa?02sB`#yzw!ip;&J&T7?6k@5Re$jDeV?$ z>m@AOJedQ@j>S~Pm5AP{!Q(M#!X;k~MQbX61!fdq+oDh+hY_g_RVzI;E6=@wxMO0q zt0EFXow!@A{Q7F`)SJ(&s__w4ap9GWAt$v8@Z2XsLgcgx7Q$B4p9PGmVdw>n#bI>0 z^Fgq>v_&!qJb)u*W}0!i6S@-hh6h)T+q+ejvjLcLg0YGi?fLF7y> zXwbM8=z)2@?@GQYBr2T&7Ju)2TqVPCzUSNG!s9f{p~s$sr%{TYZd<;3muYvZ7R&`@ zfDq#!x%UsgN6-{Q{ztDzpGrcK^e*~1430rQ8JY#F#-yZSSOuY- zIc>DOdHy4IN8Ozd$z3j{q>CQKY|KGWJXpzzEt3QXukTg_!8u<&g&NVQul;|`2sAb} z(p>(?BBv1Y3lxU2k|rB*x74?x$0I844Te!s$){takKkN&ehwvaRpWJiv10GU)%*AGo1ur5cTrt63;8Hp)at-S%VKmds+Yg4 zIm{FPQZe;jAR`iGY!)UE_Oy`yye6t0h|mi+L%a@iwpPigzrYXjr`s^Ww?3llTT;CAjv|N(-Gf- zCYL?Qa#X+HZZdCj&td5R@LYE-^5kwNdmo#x^!E0~e*E<{@a<)Sy;J_w)QFXtl{NLL z$q)6vqUqC+oWTF$b5&Q|Ig#ITY#S1=6y)Sk!}s4~8RdLlZ%T#Jo*bo9%_;cS=&qG@ z8qCL%-Kpqq3;z^ovJ$+$%)H;uhI+D(Qh%N9ZSdjlN>*2~LWq9j!91wq>(r3Sv}FHd z>7@J3%5$P*A!i1iZ_a#J`lw)c3tH&s&qt;E{n8sP>^$SN56&IOlEo+>ALiE@;lsj2Z`P8GSC@*fiFd0-cGF0B zARG8?8@O@}Hq(2N^1OI;y7w`9%rL@B-#7H%P#BhfMnlQ7{5+Y@%Jo;?HbP-4n0A~q zw*OFoV;dh$8i!)z&h=qqhx~G{ zickD!j;5r+X6{c=`GrC01w43#*j{DHra;~AwN^K#=awQOLK6DOT}?>T#*& zQuqr?J){&NF+bOdi8q7ScQEXubISzxzTyaS3A?duYccTAUd{f}9h7|j=g`7 zoR=JYVViXwoA95>*bce(-SVjZgrLG39(-9Vk!ca0F!Swyn zm7j-eB0JeaLsyi!>rR?FniA;1iAW*&xww}Kot=-yZB|4q*ag(xDI}7FEMi|6cqzxsF81ln%FQPn_EBS*?dW*?a_lF*%^{s~M8*n#l4f@D0x0?W+ z&-1E7ts3F_2LiI81n?x+;cNLWv?v)D7Wws*f5Vn4zqhkYU(AZtr#a?xI8oz1EnydU zZ#wGQq9t0_UNxk?@ z*b&fUX)ukC%GUTb#}s-(*M=n2eK?i23;vA&GrS~Nh$c?ED@gBczmVMu#7&a)G;xcw z0>szXC^HrXbTNoa7S>l+E1bgQeQ9oLVoZ}QkIfbtdp~_6r@NjVE0d4aG@KB z!lzG>IVCaD?|8YINFd{-zGZsKI|ZUSb3Qv{FY@ru=mKytJDa#zoFgByJ%CEcp8C|~ z&#?2A9Y7Mo(7S)zA8)r3MPv-N!sJe27&JLC)9B>VS#=0iRfT~WX~=X=b0xSm@1buu zyw{S;^(VA4Nr>{Br+_*p>$UwVx72wW@IoYbbbmc&MfbwSjPt0DcB?{m{|XZonI3_e zCfPAg1Hp$u790R2VI${^M#g(;(BQ0Vu*nlv$9t&1l2h=_v1)CyPhwW@@>di}EsJ-o zTt(&~yVVFn^!V3*{odBV73jFWNy_jyTJCN?JBjRZY799Ev&f#xPEM+^I#I{@A*X$) zDvqN-4@(_Qoy>&eC|`mMkI1d5I|C!AU=3Y^5SC1jeIDint&fa^zbP%Mw+OYOl1cET zl2OdeOu?fv+Kf9$wojhR+Hc*UHIZjV@UD;l?99;X>bceBWRaDh<4caBLg^B5RebSc zCU07h6bN1TmirUeY*TbeT+lK!K;6kEE5cKT7pmFI$HZmZF<4otd?ReUu9;B96R1BH z7OEgQ@fCKv+S;M%R3u;D6^<9ndAZD%{&(m+b2wX{4VC>t_rv){^kz@2lzw8&Z_TNgxofTrAU0{D+M0D*D=Ab`@fuzNOAVY6=Fg>MMm&(DxF#_C+nqUl)Q8Qz4+bM-o1Xk zub;GQNG9HMBTdhSmS=SwjN|Jsph3zBq5&^`E$>IdkEoo=T$e9%){PyG#_@?CUf`Wa za4WJ&&&f@Dzq#%4sk|z@*R1svaK2ZN=gTx3DqT1{sj`U3$+jyztbY)Isuhvr>{Wv* z&zHszuH?{<-Qgo!0pYf#Q#VtGNL`w*OE1tTbDCzx7rPJ`!<7uMk-)4ZUWgUDp_%>B z=0Znnqg%4iwBeASGPZhsfPVh9p2z)i^i6O|?9&r&f)gBFhSP;_pg-X42BL*ig#gfGU65Cf3L44l4&SLYr^0d-Cr_f_n0T7u^&c^u8 zYC<_lLWcX@4vE_F=d5wjIGSplc}%!!3b(*5fk-Biann3stjS?A>+Jw0tJmMYVy)>F zQoMz!Y~IhzJm|e<%zs7tjAc@Jqq!NV-W2VK@?r2$C{v2!Y2Dyz!s%Vmfii&_1Z#;2 zEFS~|GkP4ZIy?;YKr)4Frhf512+2YBwOJuOunMkZW3!Tz+%p9~92u;(n#2vR_BB5I z%3ioVnks~JO6h*V_^gTfB`u*j_4{Z1T%k;UpHDLhWsIj$q_*rBt*PP$7PEeG$bQ5w zX(&WZpBD8;Tk(dT4^V_!jcTH#7>pUS$<{LkeVh-p)pC1~n5e!&h z6(5#kz#sD{942?LG12c>L<6nWsU#Nb@VPWE^wa_yqA8pS4zD*-(qohpJ~*r)}?b- z!7U4XDh<5y=>`x5=RZUdlZq#M1QHUQ0A1Sq`moQ=@U*~iaeQhj?a9p_wc4Ol^S_M- zvfX2J+Mx)X0T9XoP*N%MxQ|<2$tbsM6cG_IY1z2w2`Zp{}J|9e`&R8bRH=^{o}aD<>ReE6#7`6PaY?uMZ2akXx5J$d2vk8S(A0EDX~Ppl|5JICOx|s(WKRPeXisZqD|jlP&cl@03T%$ zPd!a?p@|@z^6opByt9P!w*ZSvdWb_cuz@KkCPFYKKrhKBCcsF> zPe*0=l%WLU!l_uo*%(x$K?ZRyM06t7m)A?sS?*1=!0atZCt5V#f#4a46+t9$#oC%b z5i+cQFg9xhmE}7k3pcH?}mJyHhL+E?&+%-i`zA#{(-(eXGZTX08R&JmKCg=fZksa7#awPa>$b z+gJr^@dReoD37Z49^EC@NANnRC*cGK-~=}~znJPtNaXvvIua9O1Eyl7(L;|r^SN79 zK5O$32ZK_iDdkpLfm-3^me|u3)Rwa@t6UlX=R2`y4+q8VpATXkG_!_;e_3Li-~RBK zq~&mM8a2SZPL>d&HQ{x!9@Rpb@cyp%t7BveBL6|4K?oqQcEb_7cHSBB3&~7sZMwfj zgitjiXSkvl%9B0HV7`Fh(_HgfJZ64@b=)KTH zxIcQsTSI28EwY?&?4EL;oiY$R=jT2~>2Gi~3&&EBjq1Y4GmY#>LxmnNt(u%ctr0Le z84@>iJsB|BCyw(is@Nsn`uFvM#pf#Dm+z5=>EU$hfcJRlEY^*LY?US@5CsNoJC^@~ zM^qQM>vX%kngI0VJAj@H(%liboUQj6y0t~86uyR1SSbmBtg&gap$;T+Sx+bn1*B0Z}Y$wEDa z85l)TE-uqppi2d{%>y4-1a}|gO$7GZcF-KTN(jx>-86BmwbufshD%)FstL?CBQpD% zLx-iC9f$dOQRg+Vd%*oNnV|3lqQ5hVib2ryMo234C~+n0*SaO^anv`8It;@Sle$@& zUn`S75#WOg;A5hSx1_3UiA`v#3Imgh)XB=4shf-HHgv6+tGAHJ!3WlISfwa&m4st& zFh;ESG=FdkXe*0ZGsr0>F@dTy3MY~S-zl&JaUHi83TO^R0-%$wE#-3K-CG5Mk zZa+4Mg*JEl*t0}2T(t3 zOb{3it;dU~h^}^|i*|}ks*sD*w^L86P}qHl|M)m;x_VJs!*axm)+QfhN4sC>t{y79 z18r7aHK1@yyFB3x5mZ51LG_T7^hq&s;0FPFPis_g6>WcxIi9!pKe4D_Y;nAXVJ+;^hkPEdNj901CfuVP??zg? z?*NmzwaL>(*J*>9y~-177NI{$qPuM8Eh!J#MSZCk#%BC$`$Xxh}6a}_&bZHc52xeZ<(QaZVaRS3oyye zDEFPGyp3X~@OnbFS~rpHu(W$Hm1(Aph?y!?rqbt(qx?iJ#x|;p(qiu|i_I$5&Dx-z@n&31!WoHGpkQ^RqMHlWQl7ZAZ%k9|8W z+t)2Eg5VSK65$W`UACU_G@E5HUQDLlQQF>;ja^9&HRx{Yh=SxFaUxNrbX2CuG2y`~ zoQ)S1LXBQzgEGwDr8;2I4MxE3m6)#T;fv%xCBdhq=$_2f-E;nb^SFmnt&rqK61-B`>smwm6na0uA#`a(uW<8gr;E*QcyD_` zGtx?)NpU~ZBkKRX?N)t{G_b${G42GNq6Y2yGs?9+%B>wi#ysjtMKmTdsNpp0`lSk% zv?}}%_F-A1j*VI+F+ArGnRD^d7V~!Qhd<<0GqE@7@QioE`!N6cx-PJ)E{Ejh=Yui5 zkolRuU`PYXhsdbX8Zv=R)~zn{cc7C7^ql927V34TbHV7K>Lip&#tX=KKlZ|g6pT}j zWPbieyA@7w6k!W;q)K3T^Dfd7s`yENLPx~ycMKzHrbSH#do0f=<&G#N5CKNJW^A*= zfk^E}T8H1o73UJJ8(UQ-;+1TpEZ9s6msi&@8%2$jBQz26jj;%!ls#|A+Qm~0FFB)d zmL*_M9nR53)-gFXwB#EEPG8Z?G*26YJ$aB0nO2F39hZWB4qcT+oe88=j4Gr45f?Q> z{Y(Gg5bhZ=Vi-FY-}pLwuWWz^gMOdYGPTIW z^F<8v@nSj9i|#$!PA5K)J9xfmt4S)G_0@KKM$bWTH~+bF#+!RnaM<> zxT9i8yrzmTHZ}g)-}!|195O4d6#(^-e;1Ua$Pd%%Z|0U~Em_M?dOc;wh%AV<(E%ld zZdG$(J-DcytoJSVg>3>^*8{8*SS@ysXLDKJ4&PbI8GHE_QV;qX&t@B#6jJ((P4VYABDAyz=G`U7HG09g>f z0`fYwf#QQOGBSo^QKq&Jebq=4!s-HAzn?culmF|B`-F<3;#daP@ULI@eOP{uBdjgj zRhrR*)UGCMivNI7)iu>M@q&T^rW84#)GdDnKEGzKt0^3)-5R(aH!a8+Nb1xl%-0zG z13(#M4Ho|!0IIDjios?GST!L%7o2uA+{P`v{gCBwpL+=*t>R%WS@j8hFj`|Hvsx0F zZ8n+h3cJ;i?o3fp=HG;NM@9SuBfKXO2oJx&lex^G(K`6igDThdP{$iNJe{ z#!bRStKKHLp~}&1m-MV@xv_53k8&?FT7G9r)>?-nCGkC6U#_k7k_GFzeM9doxU*OLb+JETvg)ki zEWm6f6ZQvhw6kRJ58I7YWSNbM4@8IY0{F-w}qBrW6^vGaZpJt<^P~4X{>L&P{n6n{oz$kK(2~1+;8-P1IRoFD7HR(G({y|)H0t4SybXsKE3^BQPnF0Oi z_=IwS?9=>W>`SA%a$J@i1={xovhytMZUBFHEZ;F#fbz< z4~7?4ieYRi!SD|`SQPqhm#@H`MQ|TSS&z;K^SQY>gYq)ksC)rI!6KkwflQGPUH-13 zT0s8|1V;DD_H9pw*-o@l`Rkr&1yokuMnIV-<16or78?^IW6*F}0}jUo+V3x>GUb*3 zn$_Pg0`q;5fR*0C^#9m&qp=WPkB!#4PFHQ;vh_}XhQ*@Y5=t(>ASEsN{{LVKJ2)u=lhh_aQ-$7q|V>F`#iFI z)AIwY!iRs{e87TrtWSRAc;{>GqlMuA6X&QBwYdgJ2hDX8^)GkblKD~XH<7dU(w;AH zn~|CJzM+`0(sOYW9UO*{s1FZz0)4{9kSuBfmDNol3fk4e3Zu2WA%|O^7UKfYLF-}o zu>`s>c8=D_K^zNH$AZbB`Si)Ur)hCRfl0 zX5lZPtqdUQ!jKF)7oPsl4d0HwF~&}oBr*x6WGPLw*+&kkxLo=JiF(_0w?j&uyvtpu zS5w(9TY}OZEg>NcrQfg!&dgWf(Fp{15aSp6iz4_o?E7+bIb^*~?D4`LBVwJmSeo2N zc-t+}o?1OdXQT~L$<|n=HSV@1m5)|%|5nA`e2yaX%@J)(JuQ!O(U25i?im6Pi0pqk zpOqFOc=vA6(|%;RX8;UW-GBmt2}o5SvkXcI6W(!@FU2^~xogs9aH&;1O3w5r3~MIn zPkU#ia)q6t;Z@K0DxvnL)L{!{AyxF%R%7MBJ8ebbQ=&7<9LrE?EiU48{va!ZeGDE` z76i9YjtYt#w)q}5JHju?1~JDjY{V}}O6Y6`!zZpu<77l>X-JYf zQ)D&e{`({)>TjO#*$R_o@ByNd@~Q&3O|jfl-*kHOepU{S>MF2UNNCZNNg8BW(;DN0 zY)&ilak_aw)o+V452kCJDc1!kJpApl?)iN`n^??RvS{m_p+znX5!kQ1XsIHxX5hb; z^s6o7o7Ts*X4=Ntlqj1JbM7`eTbl?UT8YWn%$fP{b_YB#-XBrHaKy5ijnK&j@UB2C z_qdGnMp;-A9uirJ0V$GR^YPNzWK%kc+A#hQD1Lxo9k8oG=92yPk`ewTnQ=ggElXMS z;b->i>21%(eZs6!MOtP5#u?yV%y3O3tkVAN(Z)|20XES4v`Y(M@F1EMuAdcN;;~;z zpJ~n@IHG+FRHmQm%lj_0Y0!Q#_eN8Kq-4`)o5^#050&Tg}A>^0;#)X?3cc^Lr?H zoWmgahhT+iHUQYU7CLLRnsKki88ffNg{h#jH&RdP@E-&lZaMgyRCZZ>Z}F{tZkl*~ zMvxxI&?=@ipPrfNWtj$LBcP_vtM-#gg@jm<|Jc`-QiLl?nR1NDD41gq3m&nwxF?uncLEcd^IZia+c?l67u%UZL z(X1xbwnzjN-oH~8IUPgu`todd@8@2&)}R?dvEc3BvoRMN|8$4RHo_S`GodsvnxxPk5g}D3`aQPE^*E;KdahqlTEo39-6k zSyq!&Lk*KN5yK4y;!c%H)?Ju@r6NoWX$q!aYzvW)tDdkxOdNx(K4-`^U=|`KIfWVp z2$LZFXfbef%;qo_tY-6tc99UA$U`=;gUU~VwEo^mVi6`2b<84I}$3;&8b zz7<Au1K;dY0-zoS^Be% z1IoSJ2XlkbgQKG?0oQT>z!HD&YW?5f^8Wx>r3BHEJ$Fy;qMlbTl4y~j3d1)X{+Z$V zer;mEIGH*$(yY0x)Yv9WOB_6Jx=)I$o~tpyRkqJLOcWerQRi|0!lfIz#)a=OXX)I7 z!oxyXpMc09fHipftkDX0J+k8)5#H}QYkTWR>)-mNvcUAW9{-WS;ps9yU*og-d=lbtqTOhx{zc{ zPbDYPjgbZFIrT?v{3L7``_!0zrP&bIu+OLP!x#}!@=O8Wp3VvFqcIW4x=)to;{n4g zEx|jL3>jRHQdNcE95k@X7YG?E)75zPH`?LeAhQgRes);Nm)$>TzYBAP63!p02~L7L zq~e~b`OYqr-9Tgr9AuEMB^ClcgL9g4uh-$<>oVHE`Mw9rxposTeGfA(;vJN z9Sf3}RWEcDOrOi}msX`Z3C`s1G=!Gsi|vOXDM zDI&?Qh3Qg>pWwhYe5!+qjG9c5V4bZ`QL4yNX?T(mnlr6oecUYdn(X>2T&*P7$TeSq z38CaFotdXOWVG> zT4Ybt?|6Y_i*SIU%1F%>srA`jm-`OZLw&`>!r<9KBDsX$Lxiy4pHC+I1e0pTa90uu z0t6Sk_mQzu9!?jj)vpabG${amwlg14+U!1AvoTKRc|)LG@=Xx%#P#yMBV`>$B62I$jMPm>O#;q{G( z=JQrXE9E0{TsEp~EjVg96|L70uz~-fe2MNtT(btE@D5I5xSP7vBJ)6Z6t;_C#3_2drUN}X5XbK6w);zrzY z_XnN@2amfrQpRDv?7^=yI#sb#PB9d3;Fcw3=;C;s8xxBf$%;&gL&wUu`49$*IV+Qj zH3Ft#<*;P%6u(vRS^RJmw5T(q>yUKdwLYRt8B4a|D$!1kJZy8=B)llH!O@OFzMuRyz+m3pup@a2Z)#P;RHLRZJmL8Wgv25;K> zau)Y{o6o-ruDdyf?0?7HU)f?%X5xe-hASmZM#7i!MS%nY_AQTU$cO_TXHglNZ$?o$ z*XSL8zdUHark<~odW%)TlM^f!0wnSLo=^4v#y4w#DA*fI&Rp z&Kx?gT6fL1yzfpJzPO&aF^=FwaWEWw*xA8wBuRu{jXd@x+G8pwA7X2m=Na1b*SZy5 z;4)}DDWJipvdHH9^=R#5QGt>D-S+k8A6r^0SRx5=1uzMG(8N?X*a zPobywua~hv11u6!K+rr{1S%E$zpcHi8y=;3!3MsL2pGFSBNmXn| z!u!mZmorFW)b7K^OFyy-)Yjgu*3-O}xcZ0p@dfLBCl{rr;IgJ;J0txzBc*lO(QCcU zsKMUj3Yx}9O-uS-MzY#W04GMNj{X>miMGc~V#r2?koo{N2@tMb)5cWM$x_nQrD#Eo zqU!J8GmCWzU!IpI_D^R927~b>Wes-W4{#}7Ga&!f6~n?1%N&4^urgb>9M?bQX~(Nw zdI$MRrvKRC%F7}kE>HiU`kf?{l%g&_)zt64 zr^tV7H$am44#tsv-^4QM+?XSY$K$Fg^UOZSW z!p`kYLi>!-4@{=V0F>KzGrYdG4&7%B4A;z-%(wKg!as)~tr-rwtA`5$l9h=KX!g!a zb<|Y#LEXTiGG#e2f9EsAY)sHQZOs*rYOI0ba{1mZI)w-OTLyLPdvjRyp<2EXUG1Tt zuy^6AburV!W&BFu2Ad2`xV&Pk!7V{Fd}tp0EA(N=jVgid^IM2bPXu z#Fb8nwi$!p_9jz5h4jj>ek67@9)gKeX(i^(tAGv?>2|-Q5=Yy~Vy&95be>52zf7blx%O;PdN^$VW*(4kj=w%azdAXi zT-p?EPZ#axU(_T0nEl08+;Jz%#j=(1-4vB+@wMrmUfr=>C%1jiBtlDz(vh2BBB?Mh zGl@gHU>_K1Jx7lQ&NZk7IgC;o9sk9p8|#JY?n|_cJJuwQ$iiVc$Ac@E%z=BQ?dg>P&qU4cpRDJic z6&*F!c_61gqUHUzRKrI&HV`Z5h%xR#4mz-E+PFeXC#7<#|6NuOO{K7o|HCFzL{o}{ zJ}q)&`><5@&Or6S%qqsSB5BXl!a!Nk$b93zSfH z5aAAJDnLa)aGV>8Zua{ zaZ0WTRf7jcQbJB`bQZ<%1coau*tyM=Owea)a%=Pf)4e@PQLgPJ*K$|zXeuF&9-f~U z5w)AQ+NRWqMe^=|3B3dLZr-Px)uGiya?(I#d2?T|)~m6qdxv*|P}B{<_FF8FjKjGI z%BuqwoXM$H3M!)V5^NMncKmf+k_9p6-d3vM?(_D+!v+5Kj*d?h8C1S^8{w*=zwn9} z)ry~J@*0&z?j;W7=rwcw9?Ae1$Oj-!D&@tngw6tH-KSepmyT20Kkw;$tusHDEE*b; zDx27FpL=1YX3MIzF0+VC}6Ek&YUrEl&WwU;|3RUZ_8pv^_ zsoLo*q28^CX%aLgkJGl)P`C1q2Ed^2aL{}e{n@vBPzH=Bk5ce9bcJESceVaTJ;t|C zoNL0XAkS-%W8VE4x>JvkIzot>X1Sp@Q^%-zsXio>%{fNRdWkHA(5Gnpj**Oee2_yd z5gp?#Q6w5z5{GCmA>=_pLR~U%1FD-b|BfPxkwI>(mLy89_*eRZ5tP-?^Z2pmZ9CQx znZU#4%#Srb-=iI|wQl2yII)RlF=gLUOWS=9gF6p{$`X-zK&%nDC68l8UCa-`@;%|3 za^4Ghxh{p^K%)e|a~1A&#tc9LJ;EMs2c9zr;4*38GB@N(TcnQW(bl3+8$rujqz6m= z1^6}wjWuqa4wps`JVSBSdmZJ$B6Wg*9QXQRZB%jeMRk8|1K$Bl zWZST5)s7>vTgpW>Zcl~BZynG+3#E;>eWh92qkCp^rLNm)(|1@YbJ~2FA10TZ;M0Wo$4n99m5MlZFut^})jj(j$tCqu^r zt9D&TWN;V=a7H5B%U7Rzuhl1;gx2+OTz0~iAZVL)LqRAM2M%Hd;yCE8gy*aX(A56C zUU0DDJ?w)-0%$UM&*5hnVK0Lhnu>Zd))-pTE}{W{u#w2I11qIX^?~TwXhg+Jd=Hb+ zix}PHXx`N&r*(F=>XJ3Nm~TK6x_O>sl~sW48^RWXZ-nL?Eh}HleOJN)0}b1>^r-QX z;ZFc3#!l>Yh$B+4THTKT7iqdyIzFL!2T zr-cqx0D~)YFq~}(+`5#swdnw&u>Y^4XF7E2{J%Q7XT8JBQ2Aj^lZAN(pOpMp5@s{8 zR(K-#_vLT(c9qT``&TYBRJ+!y#X=q}*I18QSgxN*FDeCBcGrdDh=x6=DxB1;-V4HwTaon%^2@lm@)hZT7t#8?#Vh zq?g0{=`7p&R!_nK7F?}{m_)PD0M3n8n1R^$p(-FH4#Fr7n4w-Oit?s<-FGK8rq*22 z#8rjkQdUQ;oEY+BjiY)I6P+13_gyiCkE!Jpol3V~GclCctjF7ON4s-9HtjEIT5L`` zG@UN{3yYRV(Z4W-8w{B=SUaOyYu5Fe?rD?S7BPmsm%+s#n)}+C`kk8^krpLoas;0k zqU%)3plHjlB#kR;-R$W(Qu90}4FuTtcy9j81WOx{3w1iTdV62ZjwHSu`xyDQI_MQT zNEfXXtk>_`wDGbS z@dhkOF?>7dz1`nJ4X2k6r$_zVkGQ~~uc@A}*kv#-Ca0n^PtEV4~gB~ii zF)7|%1c0f8cGVG0>O)y6IF`hx;wvuCC;fPeZ38D7eFN9@D;+&g9s@aVbD#dgX_?oP zOec$fQze4MK%qsrLSev=EN2XnP|;Vnim5bNSNu-&hJC#?wO1X~d4Oy{XRRGz`&K7e zK2}!#dZvcQz>idTFOhaQXz3R>R&m$e@1XXQRC`ZTr?Cmb&ps@+E8yk}?=$-9IU#n@ zYYVS&)ypgNfgQ~)h4Cc00*Y}d+!hGYxbgdNIOBJYcuLyHVk!aeWELgfaDQZ!VM>8T zMeJqP9D~7-W?+2&JkGaexi;~1TvMU!-u-2`4SE~B3Ed5vVP%G^Vg^?MTF!m4?k4tq zrVHL3q|ns1z@g3=B+WSPZDdL2E8gumicuV|4QgGI&*N|V7?wHb6Il6>#s~;_5Hr67 zkNw$bUWVA7Oa>B|RbN1hHNaz}Qdvtc6C=!&FBFpHY?iT%>5Z#W<;RUVbd&n(cdjAD z1)D#`WWu7Fxl$!B{|Gwt?2awCV`5+o-%mKN9J)fVshWDGjq4<>@kV!uY$}Jg- zKrO$U>C*=7nhv>8qkzY!mHmyk%C7duZub7>hZEI$l{{r7FhVIXUWi1*!|*b*ACe14 z@q6FI!Y#sLV{uQ5jZjtS47V5;8F#O_ zzF%!epbXoq0hpx+9+CjL>y(xkMAVOgKh z+D#&}!jv^mMxOta1+$4TrfD~}k>IXl9{rK$s_h_-yXPr9o=0O(G-1%j(G1sK&V&&I zt+ADsU_f=sM_U}hf!0ZrC9eroU`|9%6X1cW1sC0~R9cU`<0C&TzIDVLleQ)|_F`fo z_i{V>K{)l=;vR#t$7PMuU1a%gQ8}vyFe|$bXZ;{L*?1cG;nwH1r+N9lkBhfk^UJk- zJdjY5+3sI=VB=+9>`76e4877FGBsi0LyAr8Re)F|4H2hWz)(3&2fz`)s3IpwO#K37 zN7b*2jj50=ZHM%Ou|vA4=#ro>X-8ww|+i zZ8-)%%~(+hiPrr}(*Wzz>Yt~d2J$Y2U&>eVN8cb4s{IL2dXmbQWo0TeCh=1otdeap zjo#zBU31be)nS_zRf_Im)oWHr=af9~qK7bRRwe*q3kBd1 zjQbC_EUCHZ^{kqz)``%FMT4xt?5`@IuKRn;Wd2jv6uws~fJ`bx=Aiw*F}7z(4}p#x zw#%oKzK_Q%Eo;pl8?f^2D-s)Ws9G@Mw&uW!dZr}=n#szshm6GDV@t^jP-ko87r^2Q%ZvM-doVtDgkxjc)$Dd*uIdrn z*PyyrZ+2+LyH*P9=n1IJ64A&TuY=quwOilx6{hcku& zg?TX8*Y|RTk1aaMyUvKImS@QrdMM-gcyM zciuZ*PEgw4-v?K_`)&KZn%veicp}|tqTS2_wcyVo z%t{EccoTS5?@RA4Y(*^D_j&5LPuf{ldDeJQ!S%7vT&@av%Rj-u7cOr^4$GZYDs^ zcR~h_*M5S9oe-IZLX4ym4*G`B#b%Bmn}Yd$>2pXR>&Tc z1VfTKtt0D8QYJgFyW)#+vl>Sq-sv5_oQXtQA(bKbG#(Vd3PnuwIB$zG=zwF04tll@c~NJD|Dd zUrF{ZhkC4wY4kQ+fA4D-wmoTE%|TV?;*_xyC@UG0V@7bWOGYPPIfHm?CD^3SMvT|$ z5{_9?c%D_SJ>THvU=0g_aO4yfQyw^S#Lk62xCVYm0929|6FID|U3jJUmwQ;4m~sZY zzup6@cE)Twklaw%9sa_LiWq#m;+{J@C^+G@n{HWHeYl;vaHzq6ubMk_`tNVsWg7p< zCI9cmufmLvdh*(Gd)l?PSgD}az9M6cx}5(89+sCH%RJylpF$4KT(_N{W}t9EN$(ra z<=lg`m#{1S;(YwWJfiR1Q~G!dPA<(+v@#lo$4Fjq_ZJRolE3BRC#6w2q-X@93_d)I zY(w%YoLA^ru4m+PfpO(H@n%17kF;-_tA|)gwbp#iEl{J!npIJmC+kAu`ezFz!MFlq z#Khml#_-uc&h&nVrI9ABj9DGG;9IW?}u-p>C}^K_}@A#W|Hd+c`iIEVMdrOW9w_h$L> z@S3;UKjv{tW4NM}_-|M`k6IFe)^rFeJ^nZ2EQy)-^d1b7?-UTz#PJLoLyJ?$xNgI2 z8%c8?16w?E{m+b)$bHSXe~+L2TIpLA2n?Txt~Ng}(fD@scXiz7tU$N^37KnfM;-lz zkeKo(0R0mO2z5apKe2f+!mkD{6K3z4|*R@`{W_!+tQ`&ZS}S1T`T ziP7+z*_hi-{LG)>UL*Y?f#F``0wJm9o5qvQo~3I#@2RIycmy^aHle02>f6vWNdHH8 zkApDE^q+x4ZI)-~L>LjiE`yGxt?|r&tL_gc{`~PdobB@31{R>JwMRd7Cv^{X=IkEy z-H;=_`VK7ScT^h~uSejg&a#JOh`Yx|nO(iH;*e*;9_Q;{TaRIEl^jeuxR0@iDUz9) z(6(wldbwxR5dCyR@ZtG(%-yH%(f&5`*0s}D>oIffvfHccbk0`LsH^>QPBPq3Z}NFJ z$;^T6*zYpxgttcECOR{&%Y9+WNci{pi*m-A-)4{7^VQhgZJKE7*@=Q(s#}-=?*}+F zOWX&Se$zqZMRC}ee3YPkg)?_5u>*LlB4S#w?YeK`IPN8X&{RqNgO0hf{?6mpZe4X3 zeP#~vX9nRFb-u-9?{=rQ9&5=`TKer_3_jnr_R7Hulm=kW#}`(*}K z1Ns&q!ghm}_1gMLX?T1*wLPl-)4N|nE=h9#+cmhMswg&_P3XWA7t6Ie88dAQPtwQ19 zsktNI0)shNEXh%j7Z08nVi<8eA5aK=eZGvOdfZ(W_{ABU`mp6iF zduF{U7Z-mlK`FoL$fhod8k{}`GAk8ugbi!_J|G7kF^uO+L(OP{b@?^0z>M~$nX@Y2% zE{($opDu@uO1(;@rKpmxVM?lz1i|B59G|p_lT~UN^YqYJL^{<3+W1J_i&4zY(U$Vj zhDufgn-{DuRgUk{eRno=)d0FmPK^~i0Y@Hzg+kD=JET)`_9fr_WNjPzE?M+B_YA^8 zt&2zT`}=Kyf|U<&XI#KNg_mhYf0$2y2pL)KZnrw!Jzve}xl#h|=#dNj+ICc0uTr8f z!2|tIlMP&8V58iVlAxE_hd+N3eEtHq!-E4OP#j1=G^`rY49HGHN%wO4y=?p7-(xfp zL(o0R&cGg;kb!ZxW;h9+x+8R#U)k`Vz%pDO3c8Ar4FN!eP7#;uS;m&tD^!p zy1VBKZ^^t5qfT0CPQ4!I$K6w1J72#YW|zEny+GTKnEl;?UzmIdOWKxg#DtvQ$rq{M z+w~E{S~8$X+Q@=LLFur+sIiJRY9um`S+vnNI{9$t(i_du@lGM6-|8be)U~%*D)^b6p&I4**Jjd=JQq*FiJD)G! z!ZSJfo*D{^E-8d>Uk3{hwu=#2u`#Uwsr&8rlb)XQXIxC2j=O8%(o+5zT{*c?*UrJA zW)`74&+GBHrKsA=^cpb6k~7%)uioA1b@Rs(4mj4noqUx_dylo!12Ar`*?<8JaJjH2 z|9_Czp=q;NA^#z-+x|`{3!guOI#%tuCkrzQuHD92MzQA50Qo{%1%XE?>Kg(}E|nPF zn%rSkwRx?T#ko$;Qs+y?+XIexnr&B!&J-(#!k=JY)1GD2{m6T$r6r8I5HBro z^#?1n6Pm|xkR2UBA`~~U);+v-4ZGG*akyTbR&H;U#IqnZZt%^d4we;Msv{n3!Iz{P zhA)Mos*AdhVx_B0Lt#@p)eh3l9ArqFcCMi>e%X%l{O~;bvT@ns-{zpRM7rkQ<0Ze8 zQ$+qqNWRG1<83r_?n^A}&E<7#adq@ds9EsxwZWaj*ePqTyQkT_LK~y6zDsePWqpsF@shR2#lG+SWoY2jTL_onXK+97A{9l`u7O-5ll#TNc! zg|RWqRoyACqsZ6a-9FFCliujO2ZL(JQy{X8VDU?A281wG6Xf5@-;$v0N#cwuURewb z=!zPQs*O=?ml&7BXsNtDD#RkUAAcI*T~S0KV8&dXQu4Prw8kq1oJ7OYdQlxLe@(m=4yvbdmh>H0HrgLTD*} z^%x}qFw58qG(dr@XO+gwHF&vUJ-R*5v^cj~$`>{W&8pzL2HZoFu~_q?qoU)6Cq2AT zgeCERk2X_#`R%t%=WRQk6;NJ5L$^edmod3_Fu1D<3<(Dp(eeRH>TO85G}{W@TbJRY z0jO9^+~Z^uxT+$2Q7d$DL2Bt@tBUq~_e*{L*kIy(zKry*-e0EvYLPY9*X^P`ogBB# z3WF@_AjR0?a;JAN&Vhh0i5c?SKsED=Psp+gV(4{%h-C&7@s=}4aD~B z54T}O`9i*k?Ubv|w81>>c!r=-*Ck}ZTiIWwYxDOI?VsBbd)4vDh~j;7XTq|bbS8a- z5lA{wN;FPHe1PHDB{m3UAc-ReZjrmd6oF2S%2Yio2E}qzYgGUC^RB3$hR}AYkXDOR zUfuiO>e%q)_8;@PmK(ilPeLz^ghUC^5J*7UJv{2UiVlBKdAHxl%6Y0zsuL-?Zb0bFBzcyay|q`_l7-Drxe(x|wa@krZqr#=id*fSk-(EsohqALqzk zZ)_7?I876f!k`s;J{uxdkj$c6`lr!prA&>SW?(?UA8KfqCE}Ywgo0|SGjYe);d63e_zTvkcR;^E}})cNK}MnIUIBhD_4j~BzJ!WMRcFUJ^ldDqDpag~v@TvYvF5Nydb(c(by z*@*HlLwr-(;8GwK!VWwvNDc!RAyHXN$H&Ewp=s;QuFGin^ueVqrCd&~V+v-6T$q6* zho#eR9SWCjvcIeD=d+2T-5n?CNKUjMgHU?wIqDSgh=6q>qrmf&?KKoqYkOvf=gYX((+ z%T#YDR6H=5eWlf3m=yDL_XP8=H4=+1$W`Fu)_tXGyP0Mk`a3npbewfdJY^UDO|jQ^ z(ge5NFV1sd;)a<*UhMwAAs|`B>Ky$yvL1QSABIAwJ~0)f1ySka6Bw6bOWrZ`PcgJ2 z5~L+4`|gW8Rb>dhrkiazI~cl9M%|E;@cUa8zV^yc)@C_TmJ;nSGoH3l5~)tCA}2rz z*7-c)-%AD3$9gdLpLChrQq)tsP#PE^WRn)PxWtAj^c;fwti8qLTtIp4^e1Z=Lt$QS z5?zA7(gu9JKe8lqLa&t2M@d&Q&rW3T++y$CFmLT+UF^^W_NmqDm>+5b})d zk(z{cWF8^xGRf69xb1t#MX7k|wkEAb#!u^30gXje%W{T>w+Awp<=<9CH)Z?*DeCR&M#)^e`i=zL0&L8VBmp_ELewWe)4y#L_ zI!@E{9Q0oCozm=6KYD-#vXaUYtW}8V-qL*GKu0Nyuwn`-Hr7Vn0edn~_hKi);xS|A zAj+%AIJB~UHCsGCYiss>o6&;K?LcJ1g3P-s5ip}q0J|~ymd)>;dI#)0fMFYcu+P)h z>xX#Q+Y6D4+=XYv69AD=jiGW|gl5i5iLrp_7n4Yfe5@L+j|V4=$OP)_YpaiZR+w@2 z61OOmE4{E6qIzS%+#1@H;*DK&^Q+kE=U%@J~056OQ z@VU)pEUtC<7s)Nu=?mJbDxo_etv*8J_|vxv_TMTz+a9bw&E=OPT}#8K2Kl)K5|+5y zxH0dU)lK<_gCQ_do_YvbEpefTce##Upav9V+Cs6pe#YLdpb8>{gS8l$lqTWxF`jjbJfo_E&F ztpAyFp6~gRy{_Ml3-|AJFX+W|65_s4oLMGU@r`aw2c42K-jq+sg(l<6T+}-qP5BM% z+Sx#Bx13LN4qvjlvG*05H-0>bYb!74m^*5)3%*1@wXS<@ZHsu=zY*Q4Jd$?&^QH|b zdXH0Tj-x#&OZ7f1DZmWp$5=2Y#t*28Mo8{gdAY)}OAWG~zF#x=7DW*J)yrKmceI-> zzaDkafe_ZY7=+*B9Z$=R7RJ*peS0#pgY;@0-(~?_lo}rDN!-JsAQn_Zs{QHfKgx&% z8Q~|%vktSq!|~QsLgnRfxyrt!(5FgtP<6G;V_%*>py;Zn(cvE4##>NKw_Q*B0x|EgI-X{GA|sQfcy-(t zAtCQO@BVh+sqR+mC8$yHXNQ40n359?bqE!+uNi_y%Zvy1+F6S+g(2N7OWqM1)DZCp zPeogi<+KYfHqxxNdj0Edl>>jS=VaEt``YZ}_S@easeWP%X2ttAxKFGu_{Jv4ZAZW+ z*lo!y^vDH#BT&#XRb)F!kXn&);;8VsnP?P~7BE-%+g(IgT%?8)bTJ%@y;}F0JPu~R zBnkbpzQjcrR*pl(<4Aoc3Q^7#hI7r_%avo2I8@comK+pdf)vwA9SS}`g$PJR>zT1G zYX5@Sg_Ol!cxEs!BEJH@&bNRl?(#YBG0*}>$Mc6lSWID#(br<^Hvw&mVQ1RfuYFO* z9D~=Q`i;(o>i`>+`@h{ewf#qe{vrm}79YN}A!n$k3ZSmjAvV z-PhbEP%bX6ttdw2koNwsvo#ulD|fv1s3wlq@+#LN(QlMNaf-{n^oiTRB%{Y z&my08b6)dKwn~Si$1A~P?9xKKw4@}mnIvIkyU5n@48JptaSkl?aZlpx29y|B!H1i9 zkr>eX$IWpih%{@Iz(Svam#RgNiqB#0c1-m6-vIyO`_d=e5YIB4tYFh2E`DUXdo@MM zfe<7*N|kIFdCftAv>&LSTE21`(S4wb7mvC4agoV011n z9LdT%mXop2T-K?ZvIDh_Ew5V_w&@=7l|Iv%=jv|KkNm<&tM~hMDiB??k$+YfFV@#F zXXyZ1x0!d)Knttwwg`0;xJE|H*Y zEWHFFqGTu*wKNtZ3Qlt=TxLZ`lBQ*1So39I+osXt!o80T+x;kkFe0^I_*NinWcRDd z#w+%Dpvn0w_C%+Nvh*MIPa~5dZ|}S$f3S#Q$y0; zY+uO?T;*-&O>t5P8scqKD8!u?xH55+MRto0ho33kyU&Qa_O^-4zgRcBnAY3)E$ZO7 z;)aTI)pMYPh2tA;fn`PZJg2Z#TV>fm2DtZ=)la*ze6w~hGF|%1G1G?0!UAy91rM9^ zZuO5JJ*e#-%o2n)SRFsRd~OzCo~~t@uN7Qx=UvYvuwjefC6GxUGBV_v)y1)7xdL_F z9#%)!X3tvvQa4`LpzZCkq(FfV4k7Vi1|{tb3L_`7gs7Rp83p~K=GQ95$JrwCJh~B^ z9$kjkJ#?*l2XwqF(XE&&X^I`yulx6R*4N4V^#3~I#O@B!y(C9nM-^@>TF7_PL(jEE zh%-2^ITq%vTq!zgVDcFxYwOX=IIA2xX)wgd|1si{;-*&xRr{RSUxv$S8CWi1 z+XdKNNJ%ua(J#m2mHBW^L-4G%;feQo6C3EJGx2Q3BVoFbeGgO&=CM@r1vYD>{bb}#>=AB%|ehedc14?{rJC6Km zoo=tJqK^kOMOv3!b+$`fKr101^sIIM83pR}TrBod8np%Z;F_l?#(+NJ=f~fo*C?Tf z@Y&kHqw?p!4@pt%105#~4Z%1f#@R}(Yo6dt#DNA!tQ?khl;IKh^3t&+O`Mj2s-&Fh4=0gTf%PKzyvIawJp`p1DAzrIDr&hwq@TY9@*7n+_Z zA`(&wZY}dqHwbJlW4YiCWIk{%w3gIy957Hg+u!kg(RLU1il!pM`>eIi$dJw=okVL{ zhPFMz`928h6oQEmDXFhG9&85ODrZjkRfZf%t~7;Wubq(X%Hc6&5l(*O^)#j4m_ZiK z-CD+_oU!LY&g)7(f1^9o{-t1{>@K~*u&&=?{(j85>(g%emj|Wg?>Yn=HczAJqNrZ_ zyWE@E)}waR!uT-*+4D9T8|JzP9IQfCEge(N=MEn0FVl68LUUKMnMB^$8{Z{Sv>8$- zlBisE7KH`+lMASk{!c3g%+gl^ZToM%@3(Q64a*U!DSo zuQf5AzGJ==H#zsVv7apw+{H3Yr3Gk&`R8C(zI#wn?z_*Nba+q^#FCHuK!zFa?|`_b zu;G7*T)_SU!Gv!R+FS!|tpNwu$UXBcTen4do@B>VJq*^A;|3dOs^5dqqlt{tbvcCtCPjjJ2ZJNT(q zOGG2jb>E=NGoBSpP05ty1CrOev-Zq#Vcw@Q)goi26jyofl`B>5E3?_eSxx=_%9ZXG zzImSh>`3`hr)gyn&Yh*%lqq;?nRG*>cUdXcrPL0Worg27GOUiQ4F{G)!d4T7#05q$ zsO0naQ#t-tX7J?C{z0=NHd~+Z!{yGQv;1yk;cKj2&*N4ed!3}4jiej%-xwR%R_^z?&ZW6BY_-A}PBt1|E^N91SsrQ-Hw z;s`MPYc^Le7oLtV7aL!8V$s;x+6`gpZHCxM8E&5Z7$8w20`h6aeIeeVo}J|WTL}~| zw|wUSpnpG1jwraffo9^)kaqxN8hZ!+3~X1@V8WXj5vw^_@5uQ7ATCRZW&GIr-~mKR znt0=}a|5~qV{cV@nt(h{fOs_W+D%?z#Ai4373sr`I%cRM3OIq>P}`M!FvYk2*O15J5N9jabIh2q~D#h>AzG!>fSQNqcNFU=z9=K{hG73r>$4}@}=R7`M z9kM!9Ig|Q5Lr!Ij%LrV_dELo)GX=jM2>ue$E4*9g@@?eF8T(gvQ30jx}cCIY))Y5qUz-K9(+j-?=rvCxxh&XJ4I&Sdy`dMY7QEI|wN}WB_z?{kgSx42v zYE@J6%P2B+{?u*0gXLsRR6TzGahr=JOr(YTl zJBbUL`o$kqvWjKEDb-)jt3i*}GlL)6Zx!P}bp%pU+(H;ECnbhwC4SJWzsh8c&8MXq zA-X*`A&!kGdwE!xfe#!^;vAxQ*t_|Jh~OTT&m5^q|MCxnX%oy;Xo)7R;KgPdeKm;P zPLTLgF@0+U@8`-WFY?LT$@%!uLW*YQK(osn({?OA{|lZB9DE9;6DXGBY$Y{K3~@Hw-bwD#>X|gEcW<`XMBf z`wXlaqQ9sy2{_x|Q=lZ=p&H3Xpvw>W(mbznGXnu_8`9Jg^WlavkB^TKd0OBZh=d8E zEFwtzg*h7Qr6cfGy-=WdrsP`X@onOJjMFlbu$W{%2tZpl2v3?Tx#f1u!V5gGd( z;GxW*3fKG=g@pe1VW;`iDO%@G{c{L-nTM{V5;xTHh=lcgr39-!c=(ol@0NmN3Zv?j zp;@r$b>2jm*-B3zwip+{G&opX->9|vUCFY?LUE}3Hle=zH_P0~+L*z-y1zIyWu|3H z+sA2DHQmu*S}E1O;OWCtmOKO zxRWP6La_oXlO+|V32+0T^MMUc><5RMC+$1f;hFDL<2b2lP zgazmIQ-$eg)x>WGS#vkIW+4RjV{w_I6zVzf#&5@|1syKzmw5sSdnej9?(IOyOC)hH zB(P&b;=_d}iDI8ky@E$QW0gp&qs!G|eE160fcq>FZw5CwiHp0Hk3|s{ ziq(7z#{ns$;zv2;kv@Xp{k)2WtopCs>g$~**%dtrvW&FFt!)e37Lbo5+N%?Lz1-XC z4u>-ZVuBcbM$^g5s|LytktYj?I+uAIVtVun?t-*S`cZvL#seV2Fw+G)4f{3UT3N4r zSW$&A*=>b9)!QE4?`KT-IUg)lPMEKL9#y+4F8%eXIrEvO1?dl1MYK{1n*|SE6rzA1 z<%G?nQ@Le!paz2VN_2u+-8OainYE3i(VrODF(OS zj6TADcQXEiJPHa*n&Iq7w*@YR-!&j*^=Wk2cqI&l1S#}vI_`~U3HjWfSj`kEWeFx9 zYUa2C05?zU{{PVps5BfeifOaGUlRhJPygcGy~$Ol5W6uTL?Ua5?g8bEt+_WG1z9m9 zPqI+0VkW})5}j%3O|bnXQv7N2^)_qYlH!PgNAL)IctRf$5LDITTBjdR zQV7SO!&%ndSuT90o8xGw$j{fGNkTnd`@4=|izzB+ooBVKt=jIKohiG;=RLu`k|vlD zZZveK6l*vb;5IHfLc)9M(eOr0BGGV?LaC|odR3uS0UUngLhS5fSFfld1OBU~9XHo` z^>yK{78s?@6AT=y)G-^V5` z7IMl#*AxoJ<(zP~-d%Iz*^}ya!fyPPS1K~HbAM#6D(PcIpPlADR973i7SkbT@P%TH zs?xxaVt4<>3OjUU2WCc zYx$=wr=rASqZubACKODwJ{<4ku&B;?ixUD4B)>^|+SmvC;Q>|0AxUWdp>az{U8?Ig zZ~I|VAwcs_7yH=;$<exZA{;|D5RTb(pwL;}MPSx6t%rVQPX`#DE`jwnM0G2U&G4yAFpF5BQY~RNXFv zj5q8rmZyd1UE^r~n*Ka*U_IhwKNGGURdp*KdDV`7X}ROwNNOO}Wyz_+8C4lfbW975 zeN+MLPm>zBN`($~aGUv_W%5IxW`0@b9-rwzTwNEU#Mf*tV_Adgp^`T?G%+^$tho#n zt=<>C(Prp#l^Ao?z!$# zkP3UoAA&kz{+sZVVd4gC?Ci`RV_SsH0LNiRX6jEMauJyCNqV=Px;D;ly!99lIvXhb z?>F1dJ8kNgcpcXD4Gmv`V0uyD!WR=t(ZV@X;7wjqjwAoc+k4$=>Z(pOQS_PAiIBhl zgI6$tE6PDmO7~=$cif_w5N`RCn?O|0!9>91Xld)ufE2#PAD2kyyByAxtQIP1IT^{h zq#;Yo7EwA#!bQ#x`8@6m@B`GubJPm^g7JSeLLoSPq+WI1WD$ep?^S?9OKGS zF58?`tue~09%r{Aql@nyyGr<^u^ zyumB^VlGl^Hq$|7C36Qg#NJBYwX66A6Dem+7LRsBhS5`|^GW+YEX38xOwr2@Gq+1I z{?KP@Kab>B+vJ*Z!GacV3GgX2%p=>|VLzCC74}eC9cdknbySP;t2SF0B_mp}F9X5| z0|gU;JT5*XcAzS&swt_4@OCIYch}?7nZ(9QFuD4q3+2fg!OfSCzTF{o!iZbhFW4UN zIsood^RUl5DR;r8FY;yTY>RR57{1k*g~w_{^7sb@$4f$@4%a?UyZ#eMQ}4V&d}%K7 zXkJBVUf2Gf+5PRP#=NtQ%vr}hWx^Cv_(RsGDtmc=q)LAfLE^w(2(cV06oq`r+%^SU zSrA(>x4KRw-Y z6pv8o47_kpzIVmX}Kf@00nXQBE_me)CQ_`_{o>STFgo zjtyy%yPd9KqvBRaL$n+LPWlv~dhMX}lS{DZsFk5ldUgK+?3xq$I&l`_D5mCZ8lHXm zvck8a3@HW)29PzTdkhQdI4>}jW41Lg?ARTWxOhAiO<RpLFoqhvU>qCe{uwK@50=5xO-5}q!4UzWG$wN^O(O?SLX zw^YCvk%rvn^*n;=G0DpGz}X)vj7Z8RkmvRjrN=OV9z%alMSb3Q`zZRRoNczU(=$le zMXAJI>Eb>;Z=#mnUzf-A>xj6i`Bk{yRZezwvZ7Xm#nGe&-e}(rDPBt$3TGNo)a4*M zHT<~N*Bzx^b!uT&wrOVZu2pkXjSRl3W7jOjk5%{N-bqpQ^*lv6D<9y)|CW}Qn|cQl z?E$Dx2if4yQ1roZS&V09XOkDDde!~~R{qQ|(2=8HOi|-U_ZHjHOo60BypBv~?@@}gf2^#R8%a2bHNzusY1IGV8K!k;VAUloGVa}0X_=~_A zG7Qg87raZ2R+zr~)t7W1hY-m7K*5ublxO87{s_loFy8S^eH}XEhRDW$hv{mIF2L8< zIvn06k7Llssl`e7?ua3#rQ~1nmCbwkPZQ2qsI&p{EmKk!UhQ?l2pEZC%N(mWYWXhe zz+>VLK_Dxfs$#u7gS89rYq)O-MR@pu7L-v1$x|Z_i`eC2g9#oa#Bkx1dnfS~`D3y; z7b8m&7=~0{LcKcR=-YaLZNR^4p^014juE@;^U8Ve^0_mnhJ|0~i)jAQRZY5cHq(z4 zGEF-BAd>HQ^#^Noe(ZzcFcickLnq}!*DEtaF2dg`S1Jx0O4hzLER-QH-uQuJx-`!l z4j4_Wh1KmWMe?ip*zLV-`i#cnIywcnF~)2`0qMHwlJAV@!YGj?l`Q*=kuf6ierw0b z&!wS-!^unKY9S#l{b+MY?$k>P=(!sIzQA>`L;*dGeqi2Ok7~9sTJ@tm6A<4A%SLnM zsj4JX@rkh3C(ak_h#q92ev9-M5xXXF5o32t-*%CH`ydJ#Pw_XUEA65_S|htVBfmT+ zyFCBw*+{$E9WEp5XK7W^!9Q&X2VpRmmJo$RMHnt4@vZ7|G{Qzjg$AkSUB>}I)VWK} zh^(JVuT}t+0es%xvm+3I&9hif&3hz1P#iHW3gP{=p_xCG(ypyw-%blWIa5Wx zWkzR@d3&tEl|Eh1npWectvnAucwDp9Qi4Vv2z!Hby|8!)Qs}9mxKjPHX4KkD(!bQ; zKhFR04dS!IcKzE=e>tHT=M(UBRp+qgvFz^BWUd-#E>SHoV>)!9GyU=#gnEj+eaj$yJIb%t(_mG1z1=p1(yQ4!1zwF_=YvVA|M$rlQkIt=7CLYLd)?tI zVB#&!F2)-VTc$3F)0rnxs7drHxYgtF9#bKi}N1c!X3(n**CvDdU z*d$1S*J*&vpKEmJ)5cqNJIMWH(Fu@UCh@_?4VXS&S^RWN>wHH~`ak4O|Ihq^hDrZ9 z=Zt|(zl8%sX6C@c1%NmA)1xWtPZgoB&f2E4OU@U%!@oy315V%&OkU#t0Q!uNj2JLW7T`RFr zo~;u)Tj4%g5t6j;5W>mk+gj86lST5U>c+%Er=$Sxe)2ZoQ&x5LJzdB&tK$G{o<_aP znk7>G>GXVa0a#iBrd~j7RoC+nSJkndh<}oyL-}D6$qQpwJP_fOupxh&>sn6~S7W3V zi7?#?9Y7pq=lKN3SJDovbnf$>4+mq zCPat{Q0!8q6glijNPN_+8k?ZQa7UQVEqasSy5Ia$`M|{N`c@;mlOFflylW0Cjq%9E z%JlQMtZLoUaM67CF1X|IMb@TL*#?1)i1B_ZE2j>t}s#FUguFalU5|hcK18A{a)`;!7=5* zrdMDvdU-H}^%na+~yh`zJM0o*~Z&@!lrk$w@vEoD#QBDte?w9ub{ zf*>|={8n%Iuh%94V`RV~Mbs}2hhQ}qLTkm@iL%FNQiZZvt|E$1n2+_a0PRLnjR1G+ z!;jJg{OSuuD72BFFr( zAWgfrwsz~1e}-A46i_XWe6;BQLG)H+lVSUxMDkF&+h?dwCmpEa)A&UT$Tg zwzjq?-tGqg9`m1C)Q=x%p^`m$X+j=sGjG}~Zy-S0v_y5DT2{sc{p__FO7NvZTVkDt zDLJZBBJH(ahw%I1vD2Ubow-1Ns4mG~pQ-NDZ*~o&{`;Ltg){7(iAO1>CXO&Io5~h- z5SFA|lrFuR4ICU7$(F(0v+1Eyz}nh5zh{l{gRw>BeUK zxSz*CaIeCT7v{w!#Uf3`$dW24sh$1{%P=6^y1yqA-oW8))bFHI9T9z>rS*MA(Znyb zG|>J@tBbo|j^=sOf$ukw@KfiiJNEx^Zw3xV7=pBE&EW*cQw2gjq?MnGA8++pndm|R0EAqx;O!mWm*9otxJ~m*Uz9X7Ytz1H}_Pw?^ zF}rN?-kC0g2;jk*fF=_yPo?7XFc6f|FPlNx53*XCM7DOOkDD3JFcCT{c?Ev zkezp&z>94|%>ZP|RM)fdk?xJ1+^Os7&$(fM<8Icp>kQ!#>aM`=vcSRZeA zE?*LlMZ*LKk4^Q%*I->U6*{8HWAYA>-33i$jK+3sD<9S3mKr_-*iDK@CgNEQ zQ!%fmk=bjL*lWuUNCHxuozK77GFBsb6Osw&$_3b^G@BWPrsK0LnTQ1q9nWUAP>t-+ z8}vl^AvfJzM0Cv(=(IMRfhc>WLz4IMY2m{HJ1B8aDuNVcGK>^uI2(1oPIgLjp;vw< z7n37>K%{}7&#i3{#qx)~(VyHu4gU@OQDE=ry)Ndgxp;W)>=XsM2(|wPk58XW;hj8yE>_%+|EV@^14~E7rATBF}3pxkKHker0FX~`9 z;j1T1yd`47%k#{)P|df9E*kReFc3M~BIT@kt+%|b(TH_<)QB^tnTSgfYhy%9^QZk` z>{S&FbW{nUk@Zy5VCkb2U7x+q8@`vNEPbntCU&Tg`?YWcQ`sPnVe{u(ZapOK#ViA>+%T;yRa zhvUaRO@JnNr5>lYa<9m_1PQt58l=`=57_Ay}M+79UT#_DWBd$sQ16w65 z8!F;w;T0|)si8Y7`&UZP*gZ_$!l;<)OkFlJ=p^j56PD-?hwp_E*CKWfcLIdU9iBOG{W5g?ws^jkE!xdG0 zE;x#;I~QAex`nPUhpwmd1jKNPCvT+e)e%2`*%q+T?Y2eJ{;|_J@s+Zg2TDv+UG4(v z?_5Uf5<%ZdVAYLNI2^2fX+L(s+P7ZB=N3T<|J z+nR%j-}6OMM$SBIPZ47w9}W`1KnNk7%0FMw_)M-a8PhEF*AAHJL;`ZQ`BEtv%_!^) z@ALd$rfuXW^vitIjk$E{2{H277S-XjMx|wY?UIBks z{0jJ&3hZD!;5?UD8AMckbIw3yu?Bx`uif`UU=4^59Iuq3HhF~osr`Bx7r@amnne7c{`lK}!3A9#6W0dLKOg>5ymnu=8u|mb zE~_39PFUWQwJ9ErXbI5-Nn@j;qM%SH9`hG7GqZS`!RvuRE-#bEpe)D#sX$%&C-xR53vj!GOkoAi^KO^CI0ndACRQ_~qgxcOv*>7**T>mGcT&EKlk-{q7iEEvJD%^Nh}w`5E=h)SK5Lhva(n5++$jD&iv z&Ckw)1Kd`hU+;uiyJ`yINT;Z1vyZBh59W-Jh8B66G>RF`f`4ToT zn?vD!a#ADvnL_7?+X913&#~tll6L)Ug$x;kyro)|`gs!}%-(s*x?Uml3v8%-%0h)x zhGqwyd9}_Vr_CT=(1!8UlcKHzo~7>*-=VL#LSYldCx_~mj$#T(h3cA# zE!K+P3rhI~l_frwBUd57zOgXLRZ0DlmnxFn^@gvIfM@cYAGW~vf4DP%eNBH|?;u!n zK=W$CaCJa#@j!dJqi;vFB6<{o_dU3q?Hc!qZ5k>xx9qLkH@crZZz?e~HXZVi9=dDw z81V7vTNC=QW3=)lb8-zx-1{A6y_ySHrVVilD`=B}V~Ci6(=-x?7_F3i%U|Ghw`0wv zhdv;XxL7gEMuY-B1U&%;oHSD;{1E*|GvN_k~mva4U$J1Qz; z#{QP18smUIHXlSk4I!(TAhTvb<#hmV`|PD^v5i2KG^4*2G54ZGU@E{9UgQZ&jZ2F^ z=`V+)7)N3yLz8PY3XPZ5T#t^+@p(EPuDH)doNgCSvkOqyc(W#|P-lssLAjW9;qA5+ z{Vuno-$69SN|@R+gUkaq(}6>cU%&xMx<+=b0Ne|GOIC_*78}-y|A9Li1mg^3pH$*N ze@(|+^|)HgV?n~A6;6-+^--iuCf-Ht$w|jI?PU2i*thNkY&R|q0Yv)-r=62O1VRyU36 z=8)-7Rke7rt>{b6mR{KBNerp}-fYH*&B2O$J(BE#r1%bHx11x87|+rv!I1IOg5<>W z&a2ql1K*}&JisKdowZ+mE}~fYAg!i0Op>P^E{4 z8Ws$FfWh3T-811(ZwGp{m*K$G(aOT|0niG+aeu^#)Mjzt8~qth;jdjV#qp9?qyLG+ z@A5OhBKt+}pGOt^|GpyQ#~O}OWCZrk7t|SpCi7p%l<;)DD%i1~`v2w;_N(HQ8Ufo9 zEtrX^gI;S|WsW=V4&Q#EaF?g7#=P-0@c0eYLaT~}dowfNv?kUeOQdU6oZ5@?z(8>9 z0%izUpQ8*K^IBGgy^F1F6Ytdb@6=717;F9|o@=KC%i-rR+ zED?)GI`*=!MDE8Y>`*jWwd6lm(I5Fv^y->B_LgSmpFWf6&RXaRJG!do^b!6hdgkqL zsCO+epSsaOd)ZKb6mJkTp1)pkeQ`IS_+6q~t+@@WSGgbwT5eD?nYOMxkC->Ay!%2V zy!T+i5>48uC%Ex8M88MBAXaFArmPidVcpF!Zw8Z7N%_qt0+)czv}x(I2Kit zc*8(KTrLpT{-G>dz}t)sY+$OB4O`)7NZ@_`J6;Ofn@uwNn?M zsou!qd1Q5YjHzIdi~OR->RSp#hOE`B8(ad#*j^#ikM(4g(3&0910u^vi}GvVQg(If zeQso|njo9}1aculwXTN(=+hZ`7* zvdd*qpeaH?<&+Jdcf}5GLG?G2?oLW$*e|mdpRY8J2aoCwo=*Ea(|Ni?Vmehscv&CZ zo0Lv#%KaJy9az13{~>fy&Di9eH-2-?m~O2+6lrvIQ1X78g7>?Xwx6k;+4CX2E#6r6 zyeo+IV!G5&Op#h&;HT5rmZStHXvDB+!bYN@XqSKIl#^P2MQG?QNvo6c+inahEEZcL zwOzrv=h|P5Ox{1vrpO2G@kJ6K9vhmRKqa8$HF5-O193oz;C}!I zLPoKH-CW(*7ND9lW&a<_?+0YWfUD!Cvo0H}*3VVF(hSt?^*U@}h5B3IhX`s-ms@lHVLXR?qKM zw47EoN!{>6vf!yKhQ6DcHv1?K>{oXt2Fys##aJ(~pnp^uq|N0kRgCENMf#iob}?a* zj=hpD5f6$|k^BL^g0vsohZfJLtUB#g=H&3MxjhbUxqwE-m+-yEWUN^Vrfg<>0nxut9{tM$E3RC14yNxiW?%5fyfSXv4SdFn(BtWYJrW%FzE9fb*Ika}oj!G-1{Kj-iWbIppzi)$0 zG#H+*2G69r86W$7X7)Eq_S59ry=>PK57zHHACH{7cEJNb^T`+a!8X>j6_Ohb{Uh;G z<2~(jeWTsG$H~PmIgiIKP7cVIeB^*#N8>k-V?EL9#Ij#{_x-|nuA)4f>!6b*(AJW` z>Xr9m4X0-<&vcFG9s?q_YWyQhq0rX!S>org#HVXQAiXe54|Q-G!1$x=Wo%+K;t#__ zOmy`+SuksXJ>+wwngdnbPVi?lD2QJVcQwatDsd`~TKV6J9Q^D;$qDg`vP z=TeAo#z8bKww!a}Wkb)Nm>KLig9mUwi!na3jd^l@@hZEH2M)>!~zBvM8 zz>BGM3`4gjKypxY+!R>aV60eau-_OslxLqYtXEL@Es|K|*0tJC^uZmF_6;Pi+l}y> zDxB94!GaEfHrsEn(4s70r&jEy%??wni*!n^D1W+zVRQg159NcUaV*9sO$7n#pW@*o zApRf2(s0yV)>2l(JAguwy)t{_p=m7dmhahUFI%Xl?0jTDNU1ys9fDKo!>QfHwHl9S zuP>z0^zGlsT;AhqPj&53zTd$l@^9rn>8l|au6j|6-&Cp4JTFd_&|o9HJ3n2)#peC% z4d$qTR&AQBDhS;npyFhjM9B#MQ_3*f=OikD1c6{`Qu$7@XfcjWnl9;x+1cJxCq!_* z(0V8T_Iq+K%9;BhXK&92aAhrogoMqfjVo!d+H@(EdQDtFk4^Ajk1aVkFnMvoF~c_? zRb%oO0a(|Eno9zU^*iY8io-_0+e@9rM7F4(;0y)!NHY4&0`z|+?oo;;P?OTNS)i9W z0Z)U$O%?~n4Qi4@Dw~*(DnT@X`wX)%fLSqUkE&FCox#9qPipremW)7XI6Si@ZydB) zbuw7`d{8>QSQ>LVj{b&6p^M{$C!Gb4Eo(TdL+GB2!Ot5qeq#ST5|&ReGjm}DK)Eop zJU&|WCsK0ow-#rwL`6}Oel;_GWw@)}hkj2zZ#(=_#gW64o<_o@Nj?N(;v){ME zh5XwkS3{DBd#?(FYWUFrICaW$x6Z%18wvF;^XC3fm25Lg^fqT=s!y|>ZX;*2qqO&Y zCGk2;eWZm@8F*2p$nk08QevrmNi!zJ{O(?WpYGp1w3dRO=-B67I)C$663=;QcbI_R zu<7Y8m)I!B-XP)JAh%Yt=xauf3BKR=hsrg3ILtQ1spW4}MomTLY?LxA`j6Otc}1m= zXPzqYU^j5(hscM0k&$q0px#?|iNst3t+ui5Eb=dY;W`2<|_W!um^s zyTcoCC%M6;-|0bO^?t4ai>fkN@c~26lafs6-d28qEbuhf{HzHfOwCHgJM&;clxMY; zZMv3iIZ+n9SD(zrkjTc1bGcS$g$! zqmz#SZe9OhrOv-bbMJ~Pzj=63K%T4U%|u;>tnHd@UAa(kx6;$D>e#Z}V4sm56=5yx zo|P)BowV7ISM8*y;ZG= z@N_L$3SCUecroMqE zh$Xo8`y#riF?F56IkTdNl_Sz@7LLsyF*0!Bf_s3DeS(&jHX5Jx>!Ybji?{J( z>;JJj#|?Bsh#mU9OOrak!tJ{U@aHO;RZKjT34ueM;mNl=# zeYQE9QF^+W_Ypo9Td-?uTX0Vr==@bIxaMHwc((W~&bkXe%{5{>1$TUj8K{7!&x$7X zJIo^}E_Y5%U;otDR~!~i;xbmdKj12V$sJ+a1+#ir>rilzJhFs+|8y=?aie8%t&`%d z$R~ioE@0Ksw0SUti0YtQm7#D^T=I`(Zn%YM*Os&`;k!lolm>`NEuR_&?_bIHT(x`| zykFn0ue6a+;-vLIK(~O%B=bn088Y!p~>dzL}8r6HSn$o z^Y*JB=UeB2cEZ!F@nnr~X|Z#Acf!EF=r)Q3w9!73`~Xe=P))Wsw%U#I2@D@UFZ>HRdp85ybkWmW-ncs>9*Q3 z(Pc&sHS@E)tF+u5(hVsHQ%iQPB2FzqLJA%lQBE&Pu_$T2s<8@HK3;yS2drewj_v=R zBP*`xzaV5Rpv_eiSLg{`%tgZ*Rg#wJeW*Ybb-gGDS4jZc{i|piiScM2*TBn`85Nf zNWhrgXUrDsjC?{OI-3h&m0z%3*YG`pxR~vfjU_%SLkGlOFM&(RCXYHIs=|*41;9XT zwsSQhX7QNxQ@cpt{yS8?qs-n7ov|CJh7=XlXhoBfDW(;qewaC=Sj6JazLY~1orx)*}8 zYs!ErA;4dv0ki#~eS!A2PM$m^!1AYs$I=mnoa-Pp`f5)HJ z&R;49{Nonf>sEv}Oh4bPaKRe06s*gBiOF8g1LU3gXPk^{bHvr3_M5&ZuM4ZLV}S$=F@* zFPI=@({*n$sgmcHhMkyYQrD2j2&3B^6b6mYVZqRj)1;>A+mR+4M`#zsf34N=VEN#G z@$FT7qvmQo>S}!<%(pu1rF!7G4d?ZRQ`vmA4il1EfztZ*Ok`?(?eFVwLMJRu;INdj z(P|gp-U`XxxdPnnKwz~n)?JYNJw~in=ISG60^W^=%xW?xwD{j{59fP9-EHK_8h)mh zx;U2M!n1;{!t0-i-O7PDYqGB*HS23m|G*w-0(?f6%&2b9Mz`y*tua@_8?ReM>)Mkm z*FCwxN^{aFyX$(S^iJ$JK1mE^OSR@BU)X=4M9L3?+~=KY+m`qvlh~c~d)LcRA~B-S z$v$4?c!;Yx14*+A;FNuVE^Z<;@lfJ+_J1zqYd#9#tc;*alJ3#T*cjI$Gs_`u*-x%z z>}(~MwyfWN&@&LkjG^5C@!8_#D}UsKUh0f_Ep5*%PUP!NVaYBuo_?XBH{wbOMp0*l z=G~?tkD=G^gy~-WWrcT@yr!7TB(R0Cb3QaBRK#EGphunU%Ad+=ov}jJz&oe=}h+ zOC`1|Gz6KF{RZ$}=x5!7$A_-X+=C|fb;DJUIS%#rG*W5DtmNGY4|C+*(r4`E%^Q#K zcx}?hemUu>8|?j${Ld#GLzovGtZwQHNc>up-?l-5t{1FmyLacS?7O zXn?Lqs*QkL$SBQ$~$&#Z;qJ{T8goqpU6sUlLdmE-V^fs%fh=Ik0 zZ#t3q6B9x082Me%&&&Ikr2TIR*a*V51EJPA@ zkD38NHpMj+WEud2SIAHhFUhK%q=816>RRXf*I4qiqEK1xhN*q85VM>KTOCTpDw(+ zafJi#=A9K|^5vCv$8}O-Txlez^do~!7}cqA<3`_ao=Kvre8J3z-5*m!3PDo0g&5uy zKh7RJy?hUBcu@1-W0JM%Sjs)+DD4wP-=~4i6$rqQ)H58B_W! zW%F%|3LNtD@BTV+?nIRg10?L0TgcN0SS>Vm{q-4% z?}cG$Y(0>v(?5_X1*1%pNb43$O&>->Ny!e#CX}OC(k?R~?!3ZPcJ~P%Ao1@`RZ)xz zYKUtL#I$Z#T_!FE%JYfux9?{(kIMsWYm`|nB#%)-J>mKUA2Ol!6PFA7af=6!L@4GO zD@%z7@2I7KtW-Ga>-%>+Kk|R$M;MHX3iq7mTbVbTvQAGFYC3U7(aaDqXkK9=jlc0)+N&gWzOKOts{U6Hp-{j?KS#Y*0EN8(~ zz-mgX_LdYlP%%|iw1CjpsKI=g5S|Ev-2A4WuvJ+MD-L`Ms3hcWUMS=Dp>1aX1tl-i`u z_NA*&gH{)&bP6JiI$eH@1d59-B|KZOS{?4z7&}r33KroRHpXd(tw5o3?HRPwS(S5+OQ!ARaqg&sQaGwc!z~ zz^7K!k%{3+IA5oLhtpgwxx;Zdm6IWa#=J7B(?ke$s`~t7yXDzhfzNoiWSdCK6v9R? zE@TdKFec9`?Rxlg&k%J7yDgK7ve_^?@}RC;<)TlBSXP)GOahL`tA`a93O&`OI$bKB ze1^OP#=cwgcBQ)M_87_q)2+(V34@2%z(uCt4j>#4klVv1)aMQi(%Ub90&Zq?k68b zdTTMlxB7?9Op$09-Fgv2#sf-EERpS#dWY_UTQ`yv@W>uKv0Jk@pW zG1XwOmd1yglYv37V4Tu#q2mLn^D2}V1wIINezXB*U82b=Wzk7%1&yg(VvCz0Po@@! zUjcj>wT2LxIGf9!ZVl7E62yp~<8|E@di2O>V``>AXL0y4i5TXgTqI zuAROfdFac&2JuXx<@q`7&bt?vFFx9H1~zAH>ZP32k08zO!yR7`5+X~AT`la!FMc6w zr`8mm-^heI|GebehtIx5IKJrO`EfvOa>Gj|*nZn!pm|5M^@N*QF6q;-t%hwCz`%P1 z+C!8LN5NyrkW5Y^P-V_wSx6G|J!>&}UNI;h2t9v+?Iy9cw~xMUy+Q>@dN&}4kbI0! zVk4`%zDID~d1n0IDZ{ZY^j9Mc*aFV2fV47VKBw4Q(L0rYn!deJj0y1U>&X?s3~uw2 zL5Zg>K$Po>pNFDu(egk3PgDrD?Dq47%Mm9B$LY18@kX=NMupo=7383_u-R6o8g>{wuy?H zBZ(fO@E=M-`xQ;OKSo?o7{+b~NC_6;J{>we|1~-3SbKTlTEpYJPA&DFP?W7o&*!r< zk|~`lami%pOmfS{V~v@0)b_1zez)M4pnbqz<(Q@*=oK2B#aeY8Vk>Ojyix*_K3e!j zbiU*!v)L(S5^f)Cp9|kSSLmiwm6t}O8$RTqf9qpzQ!#mb*gT#%5%&QCAr5D%b zFQR}+6u&XX4K!ZxM#W(|<``5w8CqyncYqj3;?Cjt$;fIaKj;vYC6%{lHk=e$94q>k z?cE3Wu^x^04+(!jqzIs!XVb5hJ0Tcx9q4gF7;!7N5tCip$G`R+n6F!dqc;)dW}1$3 zRmQWG@264HAdp3>{cX#S738?l`{3Wm=6|?WjeT5M5$9T93Z)D#o+jE&-E2_^@Q7^3 z->$hDToj8(?I&;$I**60F??BuX98tkJpAEuFYlI+1>xe!C=4z2H_I}qR3X5=9A0E7nf-;%=>Ge+K@4v(kB2yVz$(1UYshD* zK+ay>Wj$&EDL&I%C}dx*a@W5?Z(Mg;T%O3zMD-*vA5+@nwr8tDg5ykOL}}I?3aH2y zoyE7%E{W3sV9b;FUzN&wFiS1{OxdrnG!$SFKQbYc zA3UcaQBJiG7U;%BLXroxg++;_IJw$)Poz_IhxfL!U#;qoou%_t8zbx+BQm$ufRB^7 zkyoMUux^fke&f1H4j^$rnV)U{1y)%C@5k}t4Ly({5TQJw`4*$K7ss`)XOrAkZujjQ zJr_U)zU`eJ<3$n8z=*&ERFK?j^seeLq*f1wE5srMFib8n(~N0Gm6Q8#ss`OnnNH(F zRj0?^mTC|}>kkQ~7;xA7FFpXgpY?i|@A>B#>-P`!0)m1>P-U-?e~&XijKnlR*b|_G zce*ipG=Uj)YrlAR4#B~}ArCm~(^M6qO_JLV9Q*iLZ_xi_ivPT)j|&Az6wm{Hw&l8d z30bY^BEa2yvR)q)%NZmUk}zS6fOPRKQ~Y~1Z2W#q{7^W3ydq5nY5B(4w9C(SlVnFC z`^Ud|<_&Re*&@C0Rj<;Oa+irtma*^OlM60(`_}l65in3#*-O(0+M0cBq?pdaKO7~G zxu1?Fh2)2xkoSIVJnKmNy^QOIhR*iI5=EUQilPm%3Gv;88{lh~k8qf_1~&|#WruRDJcf)m{(#EafW}RHoAK+?)3$~Y2>t}da}@^KG6t;UBLS_Si$WA& zF{;Lh%MIk2zV2r@gAfmxnjQ`T-weGqQYZtCKOl=J*N_I_1%8At`{C4=5x~%`U#8Nt z4GMl!THwfx+B7YeOdiwAF}0CM`e_si5YHNX!6S7TA$MjhL#OvejI-9c!6s0p$g)_jVrkc=&MSvg^VjArhLOP)L*ZPo`UOOZ13)2IGR+>`EL_#>eB9l3&V-Q&~&7buZ`S zx2w7?%=x43(Q>)Psjl*wU`Y~K`TlcD?46$+JtPHJ5GL$dW|||ZS+ol{e>dHN=es@e z9j9CR@OK3V@$=RR^L69`*SWo3frX;lcXW7YNN}9tflz3u+6`cXVD(37L#{@xIHdfB#{YIsmUfgW6D#CQ! zgS}(7+mBo7;meeN{kOOv=%(C-UoI%*$0POC;{=e!fe(e+yiX^z1e(Hvo{vp-x~EtL zfGNesuj}|zx_p;+ zZy}P?Zwc*b$n zA3A2O)cvagQ&w%c!cMdOQJb=Txw-&phCJNEDG`r&gvx_j++3k6;ld5F?<>+D!|)+v9|V>dj^QnE+C{LN$(fc_a;)$E8y*)Ig&vD?uS| zSA0E!V#NBbpWl$)-u(=i3nf@9Q^oOH70MO<{^y+sneBjBMwAo@Ut%TSLmrKF7~aW~!X|M+6v zO)sK|dUPnC{y=I8xK?Ou<_D+&hX!JM=3Kzp*|=)T?2g{C|2|@cj_U;Ut`;tMvHyFj z7MvXuBG8IKkz62#16OO3{B^_3=1`;ctCN^18z_s%H zDo*&4r`B`RlwbAmx9u+J;Y_uD3&7KA?ChjQRb^eaJoLcjkUKOYxihjpCew|=J*FI$ zH64Q@eH7Es04a=e3STs?bSKhW-PgjmML@IO3|Y$2{jIvbY!P_%JmElYFse&$DDSeb z&dG}YFUJeU%2j`ac$#!vR>&ol5G2Py=>x`$%f3s>C zH*X!E>o=*}IBAk`R6hnI+Qd7jn{!e>(cSjZsiKEHT^zJ!8+dAKLAFzjatQvO$h-fT z69aFLlE*_VD|o~6;$-lm>M1z#DUU@W0qF}yo>w8ktE5^!_D;1+a5m;4?lyPxCtsqZ zf@@3nfl9euu(lB=4j_l6Uf-BvVc}8)xVx^~Wm*K_iR|(B5X@Wt^zwFey0;Mk^1tn0 z0O`2p?yoP;Fu@e-tyz0u#V)?S|Hot|(_x-Hudr1+8#ks>NxNx8P@0qA1WtfqZ0LvK z_(wseWYP+$ZV)AI6r9%}S>wQ8Yp&cbej9Qt`=<0R*|DoEXx@%d9**}idHBwL9U zYQgdhay1gt`YcCBvN}8g8e5lL&=a9QB99NIuVd6L_@Q-q7VB&3xA|XFqCzLvD=sl{ z)RGCCPAn9p#=abTkP*923m~iXtbtYV!pOR(dAvu##)g;=Y3uDK&u3gOoctn7x_L3bCJaS%Kfw4#u9i<*|MwYJ^cMo(_$(2Kz6C174JhXzSc``orw4~-1evv1hulK$ArX3XvXLr$lX)7Yn-ClV4ff|n2W3|S4ac-fmE*X4 zZ?aSN=Nc;aA4V79(X_hiAwS$AWnW)l=>|E-B3uqnScb!3>FC%iNx)iemHF6~KQmSv zV>PcwLGMidl2)$Sy}I9gw^;_6)R2ode{S0Mr8@?W+Te#zK}PBIyu`7XaL;a+=GPFD zONdgSlji5bw_33e#3(Qyh$olug&Y)Qgl>agwuZ`~lv*Lx8i|%7tXcqhPl$}86s`&h z7y@7Ll!(`3TG>JzxNu8}8#{%vhli-nU+$hh@3`=D6DfD~7JSsRe(#qw>|br<_K#5$ zvOf9UJY&nfjw0st?|069&Kt90*o3Ol2W{~bayk%rQW}@ z6JS-y3G7y||NbWU%W~2B9{x9Pn4ltc-i9IhXy!D^eKLk48)6f_jK6~932jf=tnCz; z&D!z4$n8c8XYe`XlHG>`mA=px8A_ACW+|~S7xZ7_YA%Kx`Q}fJpMFocjViE{Tz*vr&Tl=ibbc{T;zfN#aw%(r@|2$ z=IFgUKK7D{al#Pgl^U=Hj?j;(S{EEj4LH}@RH{1XPFs*OxKgqfG>A9;n zwX^@y+jM3^Z*nGfT-mExnL9IKGq@FboDU=vv_$I9NkGKmh)gJk$>PQ`3t*ty`HoG- zc?2?RM_nvR)JZNpFiP{!xvRIjz3L7$ao*D)?pBE{nNxy!B|FY=fMXstmghio<8h*n z1d9S_0iMDzQrbzFwVNbEu@GqkfktMw8oOjuucQ!wkmI6EpTAqM%dfI}j zQ4iX-=_FgIOP|o8n^GT1md{KW^)U_kFf?q+8jiwfC}nProx+HyN1ais)@3zcB&&n+ zdNOT6?TH{`@Ac~?_2`+~{Ve7hnAyvPkih`k3Q9h`0-TDm^99RC`$G#>3+YR?8%R%t zunEqy%3iSbkL&xM4xsutxW3Z1u)|4q1jL4uxVIyhQ zUx|pDDIRs9O<GEDF zh5PrOa_Q6?KLg^{AO0i5g#W~=Lx$<1O$`m_=fmU)x3yMgZwLMP#{YAoj$r_*`HiSs z^Z!KN_tLKy)MRka$nDOlzMP$fwlXpz(Pk7zzL5@R*a2~*#!!-2e`X*hi>FbC#TdS) zVboHCLCt5zGgMCw1UXOiY%UKJldgmq(LyG-XUYss6tT`An!w>CI4yrIu*}x#@jFiC zep-2}E-pB}NNoxJLHL)(UuPCV=(@;!7gRTbp1eo8CagYrpb^OuD}s(v4I7pAr)3P5 zB~mO)dS8s?G!=pT;0%pGa$(|RF~6=zhr-!Q^{rR$8i=dcl2jSSb#AKJ--G7}=c88JN6~Sn0tU&c|9g ze!KKpjP&-c;M=9(!eg~*U;e*L#Fu0-ZKM4v6x{TN^h0^{MK>1eD}W%Wca`(joR-KT z1%RUm!FQWocQ2#JKfc^v`(sM!u3xP4@hVo+TFD4N)OWG>Cff7 zm-r8#hrsck=~6<`<$<;(j-WP2kWz#SdiE#Qu4y*!Di7}IT#Scp<6V%1=4!g|5ZG9* zhJSu3k=T|sx+NptGgaodf@GmW6Z_TQN&4%V}&C(duXy^dbl)Ea*25jd?A*s-GV zI9~9jKV+MnHTOEzuU%){#-KZOAC$L_=rSZDZ3$=gVq&=i%_{vcQMO31w31g;sdNJx zWL&~p(ehXt5w_}5j4Eobbpam0H@sJ!$mJw;UK+DMvxisAqEYR2?tDnNjg}vae!cW@ zMPV+ULaW3MhkZ9)?NUYF4J2?_`yjsZftfC?f?9`Jp9IW_^>czg=fCs#s0s>gp$qP1| z0R!0f^WJ#>g;8Qw79n)a%kCdO1^oht^1kcoVr*bw;M^E`{!b{E!Qs8?3f_Pw+|oVv zhP^mP3=T@Qu%zx`Q?3)6|1Z}3&`q~K1_q)j_kt9=#AMm*hK8xpt_z-@4sIQ^FljTs zV`50ad9bH~gGbdcvETM>!P1VT;0U+ps;UQ&WU=8HCf)DOF7|f6|I0>v;>R)F&N$ng zbPfrh6HQHmq};ou*^Wp*yD1inlqQ`jL+av^@|AZ)O8d&Ff(C}p>x}y1Lkph1nXV<^ z5$)O|=huG6apgx!2S%S0mA2mn29_;&^9TDk0#FJfca~&|_L+IO>EiX>Q+HOi{`#cg zs7c3JmG{h6}NhtYm40Fv;Za|6zkW@xZogvt3PM7$elymy(48mH{)P1 zA@(xxQt+w6q}7mHm)S6x6}-SNwZ&;|SezJ@IY{GXNH|kd7w8q z!QJJV8Yh$hq&HX;s?8&n*vwy}-OH#7!so+$a1U*H=TVkss}wPdaTH^64#Cn@iXzKL z&LkJy=Vy095asr>wkdRH-T@u)HPQTT2%gFNp$U+9yxZ=6i}V)KDCQUzI>5k-iSS;9 zym5Tqw{9;+6L^UTk#mr5>gJyB2)=j!4iwEb27FldkjZg>0xss3qNJgIGqr{`#$Vq) zOgF7}~J&{J8PBC!= zn-XZU*qL!AXG$y5AV{G^CPFMEC8eKTbFO#ODHo`^Vy#|> z9}4{gU&C0)(xkDMbp;XJ1fkj6l#Z%HTC{D=Ip@iM{Wp!_FvdZybf+TrdU7G8e4LTh zq3Xagd!Xc)uWb1PUoO|$Y|zDw%Glig6EWE=ec73foh1@-Yyt#^V4uq%Ct)A}2L7Gn zUS?{1bYdA(X{Dt(yeRVlWz~KLvCOdv`BTmA(}tUjy_?Lj8@yQ1q)Df!nk+r@dolj~ zI|uiWYhMaYYAK07moV49FYpiDKW`TH*hk(yyAdPH5XT7fgr7dt=BKYgNnh`cs_)#k zc=Tg2ybzeIsMI&HX(_d_uhf-fl@(Ge)WVUp_)Qd%=D2597E5%=R-o7XbacNSri1bT zCHl}~PFDDyP{`O`OH@~tyJYiwN|&GE+%KRTM3WnrE6 zJf4bEU_zB|f7CaO;6Q4JeKj0YjtH)~X z8}5@ma&~oTTcQsZl_H(m+h`Xv8YCHPQ%j@}mcAq!K!2kak1ekejju7HE9TDppni{Y zAo^3_aUXP3)qMKLAunAkgC{(GpvF^=`;@7>T4BRk>lbS3O4W_d@v2VTz+fy|bJg1W zxPc1z7r+g!*B2mDE`Pvzj8HX;P?gb9%}FA|{(Z(u;Zi?i)-{uBGCQMHr^ddft9boN zZ=p7LE{-<45a!y1!8wTafdXvOlk9?{nCj zi_Tlj&d9c^?{HU~q}lt~1+x!;=#?uN)8dzMIZQ{+GG}vv<3;bNVxY#mK*_tn&~LJ> z=3vkhNSYyq);%5v7dUmRHW204?oVha;Ot(-4}~EzNpVodBY(jt4N{e2k^KQnrL|pz zt5EUdeT~-m+Kr*iFtp{K$mIHkC+Lpr!FvDUPa{}Wgxr1{o!1gjk%e9_Ln3n3ESxGR z+4Kq?<3w>8W@M&2f9Ya)RMoTMGt3N0nrJq(mQAmGUC`;B>w>=Ia3#xceL|zt!ij_l zWzZ`7UV>~`HE5+E`ChY#NwQ{8ib%31E#?~8HCx7Qsd z@&H;319O0Vh>i7Hg>XtEdVhlQ@`CfSZq#!#v2{8DiBC3tEE6}dYb#E*OEGX$e@ViK zyKzFlPL1f`i<`k%;SJB`mHur^1Qq!t)ciPovy#quu(>UuAI*%%`u!i&CecmYA3Q?? zt1O-*1tVV;B#*quh}k1KB#~lWKluLdl!5z=81G8Zb-;1M!sH(w)??%ho;5pp_e{;) z_iB0vO^1(Y3ON!-E%pdi*G@4*0Lc`qP%aN=9g+P7!!ww-UB5gj9tfKztWXg%>7Lh2 z+u|(eRAE}MQ}QO5o6@%S$kkgn?`S@W8x9IOE6*XHtxujPPd>0yZ%CKiEy*l|=H!CM z?qMluIFOV!xMED1DJ@LsrVHN-nJ&)aU#NVwqH9dm0IhE`BU{x|;I;B$6K7 zQ*{|Q6zndGaIy!A2X=Vi4Ie1Jp_W*hDLmF24CMaH5c3NIIK|_I5~NS|ung`qi-ort zP=w+GM6Ns&>*Z%NpJT6o0$R|x>&dvZ7#087Ok%i$I*{4HTkGhiV<*RO(~&2FFs7W5C@8EEM;;5tGM$h8utMQ40n)1 z?7niP#wJLX*bN3qmEYfXQRKKRpPAafld!T~G#zj+p2+Hpf7&WDbatFnn;Q~;LTy^{Y+5YvQ_qUtnO1Id@7*^W%6;_vNv#_NsAH$QKng_=@JYyrD_l&6`hNEKbNEJl$Ax^f`Gr{oe5VuwT@!>GtS*kXKOhR zOmkCt)Be@zzO{s8Dfu&n`Wf;&ymo8j1Zp`FC!EU2(*W5_q`9`wEqG9?h)|tAD<)SU zL_llL_s7wLrLvAsH^13%>GP-S_7pLpOP$HioKwGmEcU#h=4i2uWZI;PHv>{iCvvmAk zGuc;PvWS+07Cp5BCS@k;;@>L5ssr%+xTXBcAAHZ&d3s+KghV%fZoGxBLMlFntr4cC?9L*??o$fJF%=c52R~Y(q5`jL}eK6ko zYkWy?TrqDSwVZaqOrbIBi{iJOCiLPViv{26E0vUvUJWxoh9ew0q9HJhqIB<&-F&-} zUp>!+LE35U=$RguVVQ@4mHh_Nb2|=WIdgi8cAb;h{ZGcZIaxQSF5Te;7aL|NBn2%c zZ1Y^)O*dB715CHNTwIN|)P7T#a5bq|Gi(w(i?46Mz6ypFnhLYc-%xA0wfp2XX;v?YRNOX^0|56-MK_DN z5a&L_qsD#trDV@Sib`6z3ckMz%5>q0_}>SQ8HUTm&o`#Oe9wxC)CCc!Zk}T?*0?^O zE~qWtm+$ch=>iWep{tCvf>x4Q zc4xWog@ixV1VLz17<_fN&HHgJxajlvf~t5O4t!|5y~6Weg$K^{ zLAK)ol#fH5y3x(E?a1en(}Q?8nu(c`hu#VbA$*Yul7_h3h3p8yDM!=v3`!I$TL_bi3&kp8%B|Opoa6Zh5Z^ZmL9fuh!h&naD<>%_DJJZX z|E%ks7JVUb=j&eodC~8C&2RoLJ1o^b|6uz(^ZPdu)HCqtpTT@A6WWPEN=~Y=52}8>f)0m&&o|oxLqFuekd|)618;gQWb$Nda$<`O4?OhE^+9;` zE$0a+)QI+FGFi>ruC39d%l*=4c+q7AWovS+@6^KN$=cy&N@L-tus|1P2JI$0hHMxo zBiQa_?8jG2n7yCs`6PHhv7$(l7}>R=6x2^cxR-Ms6UzB|@^+r5R_jJrrgV&*`B>dE z(*-9KPljT9kc~`AC(Tqmv#-cv)*WY7(CiILXU*o>*Iap2%8Z>ABJ3?hars#sfPR?? zBVM)Uey5-O{=$2I&zra+w97Zzdhd5roIiCQM;${@XN2?k)#DJ6U{^I#cTKgZRA`fM z!O1ANVX^&~*wXA$$ODq7cu8TI-(d*`Y1Q%-SXI>f?Cj-aUDJL(m3I7?dSW?2)BNlY z`jL2CE79pXZ7lGI_ueqVY-MKag|m0Z_UA{kZ=~oe=y3vL!$z{hB(jG}Z3*$6^a!Dx zjS_51C{_v*y?tOABddmQ({?yS%(!ukGch(%#`BuqvtX6$c@fNSCnzg7amzO^Ydtny zuL~|yD3HkySeqceIBzGCV}_`fj5oBSxY0vI2P(}&&l)&unb>HpGgI#eN5#sILFK=9 zjn#_gka#)J(YZG#P+Iok^}hv99LpMT%4RPX)En@rW$?`F@y|Dn7DjYE_Gd9c&CIpFq3X?Hv1{#kkdq*(dw`qyHHElL2z@SNs zEJ4M{lJwoYA+@@QhKNReqSu~>w{`dJwyIS^?&BWL$&i75ypez11KpHC>s5|UZnctU z)?6Qpe&eE<7FUX+r&Bf1U_dIv&d#m#mdGI;SPhKn$S5@C%omEiEEEbjN?SRb=f2h~ zeC=rb)A#<8Yw^i|Y{n(ye7{saWgas@DvY15&lM>GS=|ph?VvRJLs}kpd`SbFwolVs zk-!r!GiH>hJRp{ynXP`ig(B2SO-I3>Q9vrv;L{tD0@d|*P zi`Mr<-;VgGfI{m}ELTeh7z?{TVpCpA@8i6BexIqU&uD59`Nridsi;#CMQdTfb~{=q zXoZ99VKh=Gh}xAvatxWWeI(tIsgTfuzzfKf=(8pbN%SrK!EI*i#%G~*lQ9;S4d8XZ zuHWgtWX1dUfcM(GJlzB1?PzNodF%E<_AmVf80}_^(q6O$d(SRsN|Z4%s`XCG|6*+e zH0#Il|FAZg^B1YE)U2~d@Q3to#C1!IxY(!%)C39W5f?pwLB2G(Uj|~3azhK{v6rb7 z`jX`9Kjyc5_j}@So=S3geYvbH4UV7Kn*1Q4M&9vZUY#hLJAV$w!=j=)Q}lB+#tAFh ztrL>xr|2ff?8D#hPkbm>&-Jga&U-Y$7*LH_8oT}mOu95G?d=g`0%mZ+Dwg|xA z%|*2sWJ_pbF%cs`oV3plmvtYD^=y9!7`)cUbbP?buQPP7bCV9*;{@BaueH_GQ#JQW zLb*%ZKU6v?M$eU;)T=EsFN}Ym)&k2Pn_3=feLqvnh(hOg1@PBJtM#fhmAQ+T(bB+( zy#p3L&))Nt*H!nRzBM3}s9nD=gj=$TmJ$_Df&d68rkck#OcG~GW)v`ykYmJ6vmlWF z&dw5MCK`_4}w0ElU;q+%>5$iO%<`KZb>_fVz>5@Q{$wQ+Kj8d2LQa8E#43>v?$be zYvAW#{^2e68@J1sCs!y?x|y`9lGS`*zP4e$?j9;7Ej!j%Ze7$Cz8gF7eqZ;aerHN$ z?}HKuRzWY4k_s2cA`EswilBRzf{gKBC)4>MTp0~NZpW9fI=#kE^7qW%1@}+dx0Q}6 z(=2@SS{VWt_nI#cHt*1;z&YO}}$DV5IY!KFf{DDmpQ>v>s1$ccO*infI2qJIyJ8AwM-LDZqXq(yX z=^D`dqRue*GEfqP{GY}A(?*X7u$Y_d0y&L9J6z=LvxV8cbL6c|mg{z4s0~p3_F@`! zJ8J=uX<0Puxy8sT(9Vlj!;RNRfG ziQZ*I&t zy;@qE*o1Ff-h7=C{LB8ZHuR;FYLf-y7kc;!;N=1j8G(!ALje`2Y|$h&R$>e|ES*b0 zfgV-kv-)U}6I~rjoO`+dfeNwegJs7TTtg2MM1jK?u!Y&@UA1{v3j2Z!NH?2IM~f68 zInfCNdh2`H-{v~FVKar{8}n>@=}$8eBMlG^TESY;U?j&;QpfQ!lQ;bybJtve;~)_w z8YjmcRNMZN{ui)h6o2?w{6Qcz?mquFmG=pq^Uhd{rqJuz5SP%mSto=1!jO8M@9y(b zMc6x`*cPbN?AUP6u^gxpN3WD>9Mm9Rw_MdN7=ck*EOaE9MNUy%g%H4Shx1d8gC@f? zHFD6`J{E*@pf0Z~n%Ht@tKwIDJu}zq({(;rYgA_NSnVWYV|etfhM>ZDpQD@~XovF5 z^i-+-M{njd1*T>900Lc&19lklKg}g*?C1I#UHiBXva5v^0t-$`$JI(gMoH@*+p7lr z{ELr*gZ#J3&{}sKtS&WE4$2hx^|h4xsR-VSS77%2AYkpsqC`Srl#^3vL(Z(%RnV3y zg2}3utoPYPAL-St_vSmj$^HHJ#h|60ZtV6)V{o=j|3p?#wv8m($M0dh`{Bfwl4t@- zQHRnBSHuCfH?~GC^{n1ay#6J04u@L%-w(3#r@oPz;G%hVy`1|W11-mT?@Y4FcJ1*r zXpxwav^WGLaW%FR?W;t!q7>&#wBA1KA;rsA`{LwhRB=%4-0$Zbhf`&=htngszE! z+$y9`e0=L`=9fVl777YDRb7d^FYPp0&fBiZd*xTwNmAX1t@v6C z5jH7*V`R@>B2o9BL1h>7&fdEs?Mk7Os=991Zb(dGj#GqdP7Zp@%}7S7$VREu4RN29_O@q-#G%{C|AeHL+G8 z9#f9`$=5?tphDg{@13ElJAnoXe4zvaOGh75-V*Y@upi5j%H^ng+ERXP*NAc8#>eA< z0t`(TBWcP7sQ2Mqs8Qj}f2Y!;SXLmS(@qOep$K`L{*Y5X6fgG#t zw7=VZYuq*4YWaU@VA_9(ZE=1RD%JOCT5|fng_c*ciG6-({Miy&GRxvc&SKG4v!$07 z@O9zaPrRd+$ZLA zYE|I;znmxqISL^d+Z+ zS>=SQ?26dKD|FFn9xc-|ROi`LJ9??6bgKJ31;}y8Bgam<_x|@>#DZhQ%AYulr$+~G zCx5k`_gUr2xwUvSc9clra_V}heK3SW;Gd6|&85M_X!GJD@!YF3dr}m~fFX!=5MKv~I_%ZbIaTG^Y z7z@f~`97WB$yFp~c^!aij|Z^oDIpPgr!X z4EePtTz9cAUYa*~%a+{ksR7_7mw!m$ULe||YS+sS@f^12&wee9*zd28 zGr6#Y5!ec)umQ|q2u-}jSKPF7ZGMwDy3G*gxdw;)4rTpj#|iuh@KpD!hwk;eiQz>L z;>9c7HJ{0@qN&tHfd3}s_V{a^w^jsgJe(xp@luQ!_(n2L%v08`B-kpy!%+s$*>hm+^U9nsAK+&C>$=W)v!uND(^u$O#fj;(v`x^UF9pmf7lza)1o6tUW0yhY z1GzyXGFS4LHWevDJS=akUq*x;y0C`BFsBz3q^cjM{b~LRV86I2+hRUZg3GHV%5x5l zQ{AlTjv5$9|G|xV^X|BY-hW>^-Cx5_uFsB0A$7HdQccyVmGB=BTk&Lsq6rE0F>_#v z?BCIER(i32;MPGVBE}Q^ovS>&l)U3GQfl(vegb2z{?p_;g(eP%CXTVC+#Tl$z^oSA zvI}p3{OsvFQ2s3d?aDW}x7742<&r^x*spI^C6_$T$30~my=u+S@+=HHr+JpyQS0i& zaAaR9M%DgEOWYl*iZ_x{oO-ArY*few{(f!nAskTF4MWQcL_?y{sZj>tjb;pxXk~xt zkD}?LmV2v!Oey(hM-mp_ye;xG$V?q(G^ZBFD|&)&Dc5l0^0RgM-d6k$U!H(# z?cCQ{?X0aDCD*rG`KNXIr-JRX_d`qu3*SC#>i4QU^VuG0d#_g!*DWUGDhW8xpy_FJ zEuwbX9SQ72yQlL!FL>uxcJ%h0ogvh(He8$S)ecHJUa-Vbs8gpfP-L#7v$I5~i5~un zr3Z2pq`yS!Qg9+CGf_g&aV!)64^wXy)@B!W4N}}%+#Oon-Jy7KiUu$4?hq*MP>NFs zZpGc*-QC?vC>ja`Chz~v%=aI05T1ivxt_K6z3l#CJNn09A)S1~HL}8M#&k<*py`*K z{Vk9$GdRn0FYAQP^MckhGrv9i4BWam9C9{`CH?-N$LZ}VngTHY?e;}oMWeNwQ!D)f zrs~X=R|a#SN06G3+KD|L_sN6X_=V!L3rlfRKFWU%2@=Zt_z~T(s5cy+CJ$8{n1V-( zp3e9s>U+4G%#Z5N&RtxEe|Fi^7p4{CgW@A1A`TA^eS&BjY6np;2jGJxJH5ZX{1+*e z2$d`RcR=+V!#QJMdcJVM#UqLR2vWWg9T9;wKw5c8SakbcNg3io7h z)eJFF)mNOX3-Md&3eCdozz$6ph5? z&=?DI<4XK_jgPJSAsDSx@MBSs0U}{xEuwr;l=62eQO-S$=8PZH#YHPcy}ECQ?L`QH zv!L=v-wlJAX8oXfyY|un5BnP5DUt=xlgWuEWfI`9R>!BDX(3HJ=i=&_3@3pa4}G2X zNfz?Ew$IKyr%&zy@MZ5hnNcxhGAA6*Sc(MCWR-)kDTH$ zZk#F7Z>(Q#@ZGx6PkziJWt?VR@H}n^Y`%_BdA%3hCZ;SC-wE>A{nOpoE%t8ZH45S~ z2;}@Di0%3(Q4kVtcvD(ps9WI_Ud&@!pNYshG@_GFo_|aJo=~;cq?;#5K=w>qrAe9tHKc1@(7}xLH}Z z`un%q+ZQ$PZq|LP#6#qrb~I;}{uPWFpA%Gwb zmKiN+IWYg%j=J=n%!k;fdPy1ND8`U@MA9PJ&yr>Kym>Mf^)#B~Mf*t_7M`n>!*QfA zE#=IoFG>(0_8#+R0=~?=L(z8tMmb|yH}mx0dj_Y))WhxDXDG)qx$oX${)ljb*rNX< z7z7pY1m|qDc_%<;LxK-l9(p`04jxs+lE~HqUv#mBWy4Vb0)}+Z|iQy8fA9|djxOh)u%3`kL)AVtYBD3CRX?HP&)mQ z7dZH0f)=x007I>5FQd1iQ*Rl1w zsHqn2KZ%e3`mVprhFy4%ayK>>_Q-3R%ZwrtQg{$YKXkIAD3h4VSya2>ufH1o@ErMk zZVlEFuta<@4uHJCTf!95NIVtdfS72#8U_e!^P6*zrV4tj->9=La@-m%cW`!Bt+kaM zjXd@zOPjcK`E>|xP2P5Y@p$#Q5TX(;mtjx~DZLG?VhOFXms0d_%=64KnMQWrkkh>& zw7%e1TOp%Z>QHPJ1%_%PX)WM>r=cXR>ZRr;1ugxRWPnVBsSy14Vl z?_r53ITw)EMj|`;t&}0JOpx5XkD2l}sybfT0p}17ojjuyCgO!}C0_J&|Cc#~D8qaD z9(U8-ABN}*rdgr%A4p{r2&1A+6~X#(9yk2uovQd{G8oCDzhwB+3QKo54N}`4hjU*? z6y;Y8tUItic%@-_5yao&C#+#?0>fVeg^Wioj7M~8%zv$D?0VwoUS!$tT)O|czwg2s zH+J4}ayBWiF-gl0ew{ZpiB5?Q-ug-pEKB)ruKSngIYyN*X56XK+qyR}U)-Z+$8|dL zi9!V06@Ji#bRbrPBHa9_zvBB#*Z-G}!tJ+$PbP}Bwrw>u{;Y zt*OHIR(+tNiR!3F`$cBh9nE_>A@hCdRD?!D9Xrl18H*Hqsk-1#ercygoO;sI1Ovlx z^m18$Y4c?q4ug}-mEk#Kdo8{(1usi66E2V@5u?q&E9(yh7^rzs_~t=-DIA?QSL@zNK+-J_1nwxaR@|xEAid-u_*B3DBEeEe3~C@uNLbgO|A7w6LurKI-UT%YXj2W4ZPUCY?=KcM*wcLu z4FYFk_}?=vVg5SIjEpb-IAH7TPq20zpXbc9%Eqf9gGut`K6f^Ej>lQ|PX%E~K*k7C zOlz98$mz!Gh976=CsZvj;FAiY2eE;w3whO}$1L~ZU%H5DRhYb$e+7;wxV2P499rmp z`6l3dIxFq4x*Aw@#OI3l|`*_Yl9<&-`TTeizf&SyVA38p^Yl&NtX>S$-qs~rI}_) zTb-exak-Jp3W^;!g`LWpDem@>vnb;-O_L{$s+l|n+|(f`>Dzf@c~Fph{-HW$2ipe$ zTdjXt=k+eMOdYJAyLKMe^?>V~A?vZ74`wyM(iP+V)Ry;b_kxE$6rYl>*SwLQ6+>x0 z2~!ID`Fr`a$HFWS;VO0Nz(5ezl-tUQ@P*s3U4wi@L$)@c`kYlwrCCjiQ85riAva4VdbA^Ee>r& z4%Q1RpoxL!B$Xma^c0<4*W0@1x1!XT`c~5U2nV?+gjus%KIl{n8GytLLBd~~J?;*_ zp1=?Tj|q%d4o=SHpMbJZk$x=z5Cz!e@yo~b&@-raZJIDW&q{ihlrr-*2HBji)r&g2 zYYaqQ=x}6pNX99A( z?id{7JTszr;JL5rkCAAQ_O{b7|L+{^|2^Oz=I`_$PIO>{OSe8vhYf7EIXN!>@nOUk z3rSYgYAzsFKLM6yrYp(B*JH_QMCGu%Y4D4q6W0XZ89(SsOp;u%Q;0)vTtW@0gtt4n zSE*s&av?@?A_nFgBRI9sc8GtrRXH^|@i4A+l&y8Ntaa4R^K9yIjPG%*YPqz$%2b(N zW($q45fXtw_^8B;>;F7hAmH(Q`f$3>W%*eS=PNm<%k1|gN@_L)t&9bD;t5w$kmf=O z>awWx;UaBKT?}$UJgt@tIq*`z%AO}TyUYI{kjf$OZpOJ&-ThIdG(l`9;3acmgvwd$ z$!4QYeumt?=>D&CRV1aT`=7dm7SLJ1^FJQv9%`W4TDNsj8?-L;pLewnJviy#x`o&8 z)XblZgR6oBi3Ps6EZ79{i^R6r^WnT@B<4IJa-c$45MgvQiXY{u;-aDlVNru=*Y`su zYD`&Tp2WQGI2N{uHgUH73)!Z_o$p-qqj+VW#46VmR}quyB>CiL6q={nuh`wc?A->v z*epoSB-J&@KX-TuueX3MeYehhwmi2ZBNSyYx)Lb5yUlXV)A4J7x(ab=j}*lxW*lha zRZiYUHjx)+>~7u!#;57ey4-gQT{6;&i_RRWv1&&tkK`i28~&2;gNKmlZO7p0DNq_; zwdTKVP2XU8rjzz##cP4%QQ17cv!IbktBwn|ocoEKCLwdawQ4&i062@)Sp}A~UC_pLf~wqMEpcW`b(9=HedOjET1e#Mo3?2kgkPaY-N z7ivJAsy{-(l!G(>&KX~equ7A@n}k2&n-vYeqNLr2vOK1U(t8~R_r|`;%pp5ES)E^u z3f>EF#C%eI#+-D@bCn5J_cZVA5NBc9E;=<`5}2gW+$I%Hi{CFoJ7|ewc{F^-6g{2Y z$9X_Nl`^3FGE_F_DyZeMib4N_#H!z6cQYN#l-AuKJ)Y#>p18AfyDV+A^LCsmKxGeo zZ6VtZIK%SqfkHhYH9_B|c?3wD_DZKpYI3IJ=xgiv$>9egc z73#&AQO6{e6gLi6DNvy_evwSI0KHBjMF7btot_6u=AKrN$8V8UOFrZnin_U7pGJRT zUkkJHnWzsqTH7A)VMOY}{_pa@Jf{XM^PntC`*rlh%md}i+&*udtGAS9ScqiN`DdUs z5!A^ICD{;lQmn@o8~L5s@Rcxjjy8jphRTlS<~X0Qk9z#)BI`|MYNXDGRUa!SQN!lb z%p1I*zyCz{0-zclG%$h)Ck~fDMb>D9~VT-xsj+F>z9RMqab3gehAn1R*ITZx4w=h6_#xukzTJ zZRSDPh%fuw9#yPSlNIuWGvrdx`M_|e`AdZ{c& zf|5Z&bkZE1#EQ}Qx%hx)@SRW$@PRh>k2D6uAEdr&T|+keVbUA1(wi3*-`8RCw{rEO z#ItYwV8s zGdKKx`q;5FsOR{SBGFQGXp2Y0jX#4F%ZeVyQbGM5usj?o z^e^scHrN%k)H>~a@XIa^tXv~EapN((u6iu>T%ve6)ata$r0kDHu zTTELN`5oBoRc7^Ux-?Q?J`MYTQUcSgfYWa6GvmStY- zKsav42avrz`KO*N3#UV=j~aE5>D%zdc)DNnUb`=kV^eKv0xNx@_z(H-JZjew3h%gt zgb07x2)v<4<`-$@9O3)XB)UCNb2=C7$ZringWNnx-QC{Cn>K z5H{bFs?(d9adjcKiBQLr6q`K9Vu>P|E=PM&M~=j%PSM5IZFty{fHoT2|L0GpW^Cd z;-40zTf=u{NeEzMe+vKANCJWFb=o)nr)oIYf0(0ma%q^>$-~#?GL}8HlVI|S>UZWf zfHzhL(W52$&cr{^1kbk>qeZJBjX$ujH)=~S#yVmvWGpWz){h(&hx!Z63blkL51SKp z)dnvPeqKtmD5}62gi@Vru8Pe|s}+4SoIp>Rg{Fj%W>rj=N5d^G#ih5sh)W&c`wK}5 z&n5*zJk%JUv5zR7oku$vcNn&MB`Wg#xR-ML)P;~x2<>_%&k>Qk9C8}@;0$~HgCcu+ z1XF%sUGAJ+ZIGNMdm~X?ZLp2h`EPcMm~v!#|d~9u46EehxdV3U??C@cC!V$)7|HcKP+0dbD+*f%-l^lK3)X2Rku4> z8zm?a7TzSb+AnPJ*@DltOdPXn=$AB;nR7bq=Ck=S3;EWpE8LLnb+K~&ifbO4#&b_D zH$2|FudJPMOXxF8CqK_ML{B5%!xt4rQ>^Q0stMwaEYvp-(__ZpIH1UVQ1I>>tV5Qv zM=Yyz)qf|4V5cui1bsyK0#Kq1IPYt76;uqSi$iet6s&EVn4ceSI2*C z8@j`42wKPuJST!T$zn8r3iMD{`rvZ3NBUY+KOl>W_4!mS2^w+Ax?8=^f3M(Iu^N{Y z&2@q8h@B5GW!$dBv(1sTN22Zus`~CB${jU-cX0M`8{ax1p|GUuF7IAfo*TI$7Nvq; zXxwf1yk4G}Ffpx%SEqXx04vXgkp(O-sVuJ`EQ*>L*slW`|3bx_V;z^He63^IG0tOn z#8lrFwCY?X+C@d#=%GBQd^O0BvUg8wlG z5;0>Ic6N5?kjXB?|HC9Gx2CTUnazO2_9z*U!a2UQ>dweAY$61>{`V;n&E~Dv=g)iS z44YVON||Ws_J67bAUkfFxN4ZFXV@tPsR_iSQi@vaI0SIg#B^r7#Djks^*`3Z$Ak>q zh{U&cnkN<-rk3jGHyTA}8_f!ARdyE@cgkm{^W!IgDs91Oo!83&OhhTC)&Vx# zMOg0}kHb?=g{aOQjgFj*F71p&);o_K4koG-;H^)bysr${jyeR6wsKh~_m$_avxPwB zTvg^O%n=DllJU{+Xmf1x(YWRt32Tv2l`&P`_11ouCB}m-9b%;l#5_3$YFBqc%yh^YGHjxQ87HBEyTB^6~ej*zn={5$2cfct<{t zuU|Rn{rhu(^%0=)k$dX!dse70msX1n|AlP>B@N!OGUg&TC*hcOwQ$HiB8iXLeqCmy z+&i+$PMR^R!$Y7^P1lvvSHr(jo=4!mZtv&zM9h0I)y399W4h0i5)fMa+V}OXmp$lB z)x@N3hla=pnc~wy!q+zLMqS~+bLo+1B?e?KlxM|P#RyfMIK|9LtoP$nZqgBr9jhB` z!1C1v%|BILld^mpFCS8m81i`t>Mft?tqV@QQdguL( zS3x~n&dXP$k_LWYBmWj4{kT%;p0~*NTyv|b9DI(irV`V_zhscU_j`)GN|ma5oU(k1 zs@#7KXl3o}xmVMjTQ7bT5m~)L-=K**h12kP(r`OP#s+7UnS=&m>bEjAvdj7GaHU;H z6R{mv1zlwl%6v;5t$i~mGK-eMx0GI0C0r}f=i2+->Y<>=qXe;y^i6-B+P)|uovJ9a z??N#rm|;BJalw|1s4B$2sEcgmCZytdX}Gw@E&CO+5a>fD$+?u0lSdQ2PRwa!yJp#R z(ntS$ir?h(2GDkY`}ObdW`_K6MUkke+I?^KsmOu#=AaQzI@B-ZA9IB(x^onEToP?n zQnl-aF&i>Brt zNY6MEX8~#E1zN`!I+xbWEzb%qCq&)2oQGWi=jyW6E!TW%nrHqZx&AJ}!*V7$qa#sz z;raTz@r1$6H&!gxKY@&Mv7M^B_aTze@N90C;hYTzvtr&KiHMy3T!J$)h_N6%w>Rcf zS$uo1*gER1TB|M2CjTEwq?^Jv`-$hyXTxf%d3kw*USC{xBw-?in822YCJZp%`d}@A zdjpXJVyfuqCLouZ{8-gil*j!77~{a9+G<@964Fng)Jo-?&TCwA0rgJY3cMiK;Qdx4 zMnw^f49^6wj5rH#fo3ihM>;0Xi_ag!qt?V}!m)j);H+6!)6%=pKD^Pmxlt>++G^M7 z>X~O7Uh^d_Kly>jN7%Vjd-~*xin-YHYjia#Na#e`s8m--t%k%Nz_Zx7ywEhY)Z|`y z(d%;ZtLJs^_mwz^F~bU56Mx304lt0V^?ljY;s@kHh8|G`JA>Lj;(G)u3kqCZ9)5`4 zhfE|J{QN%jqj>zPW5!t9h#^6vJX(vjj}-AZA+}kPDg>CCz5~2W1zi1hAY1c-eaT7! z8yP`>d+E^+WOKfsQj1|Wf&hRW*YdX_`T5x~M>l{5oT%D@2djO8VlC_}d(W@d=1Y0M z-hT4?HJ}kZcpdubY(`A@`AH}RjxN}? zg+N{$*!48jjM^*29UHyQY^HwBtq*}Bink*yx1aF6r?_>exYZo9I@l)w=_2~c0$)4U z*IL(r?PZ0XyP?3?G_g#NLQlC)U;R!d?j((=h+)LZRcbowENZ%s!fBdZhL{6W@E>va zGKpo~^giK+B5@JIIlv7g{D6zQc2K6xay=1MdFc1OqSD{|`{lFq!QM?G`ik|ozx!91 zb6DZ9fQ=EkjtkZ99o-fmx+H>(mJQ3-7;G^bVPo(Q)|Kp=(T`JNAvR&QaZ_Om6Y>4( zDys@+3A@UgLQwpIUs9Nwf_BW#*~27sJ`E{nqHOY}*81v=1hz7H2gEVvwjXMv@=2Bz zq$%DnVaBCy$b=I258qTO7A+CV5uwDPMWwMz;4HX~S|GFw-d1+tc^dFsK@TJCFBzKe za|~C!X}w#S2ei}As{yCguz{d$)F_iyWH!do+ZR+C(Dp};h39oSQq_sZuP+9)qr|(Q znZu)>Cho>PWR!YVsZ8}^v8FBy>al!QaJ+tv$Jz#AQ;aC*u>D3VkgCVx10XIBH}Fq8 z(4>6#6rH=tR{c>Sa};8O!8N5Kleiglum_bzfIh&w`duWrIe0dm1O_<&Unt09WZsPB z&NjRj_xP`=yITqr_;mJPmG>qQaPHswN_SdX8X+oTzgQ551WamhDZbfsp9%N(g;He; zxkntdx+EsUW(tN)`_uoODX3^S@pNEUiSdt{GtEk*Jv+}0xzNAhj^nj`8`=(0HaXHW zg}m1gtfO}mT=e+o=@Wg#l*6dXagu68XEi-A@#rDzavKcqJz)naH*|*H?3QaE#8-r< zupB+jmbh<3AnT{0s7?F}An_AEgsqB(r=cPhZCKxEUkB=sld3%ndp{$4pHvJ~)x6JH zJG*=fR6FQv!77sQ5P7%GWThQOaE4YG+$eWQ`~d-zgb7vQ+mSNN2y#q4Fp=+Tk#(2< zl~vt2SQukbUn)?uSD(^esPfWPCeF2SK~;XC0K0yv1axX}Ce|9n4VCK($9JHUEZ5Q}?wNzcS4kd)a-E5gJHI zCUk}?*p<4#xrh&6(Ea@4DB*XNnwABdF~wc(dqg&Y7J0zLh3=V+9_Px^)uo03YmNsI zDYDRYCHSsSecM|H4_ya9e}n{gzWikFy9RL&ZV;H(TX(MwC|kVJf$<_19=yH(9hh#S z8a<smpoVj{VQH(mZ>@8lPA%Hd**pQF)m-yAL05IT^yEa@#;VNT z={H0xtK!?4-sOJXdU$nvIOu5z^kfOn!dC-El+t)t@?SX|p<54FU1jdWx>b&MoCO;% z&xk1Z@z%~{{8iwiGUGA%*GZJZ{zz@B>4UDdT4s_4}=tMg4RQ}BdT(iYJ^B10`3BhpKian|0{238@&+M&Ci!DET(xe-dQ;s9Pps64%vPEXZQJ=3o`0)i?5^Ebq0yo9V`%d9=8?6X$ zY@VX9;>mni5=L;b@93HIoLhE8GCfQ}3j3+^>8HSh@~HVsWxpzQ=JyA#ZOPooMapcJ1l)KYKUf zbVobx+TEuW0A@&A)uan(l3XO|l6eBg7pTEv6{(nAvob3Np3`ZH5}!(iM*vA&I-v9F zX>vBT%eUM4{F{IvdyN?egO-!+z+_^Q8P#0#IRoFW7jL5MFZdZSEt60Kv@xHhsA`() zbGVGPEhNYtpmUx7#`xd1zyT;)WuA+zhJgVY*pr06+K1|CUWo&kdayP(m%n5;0Gu;H z(lzp2nhAVpsBv?s!TdiMP;a;N+l8a%4dLuy*WTn=zjfrx=e-F$FrUZ&YLbb#V4(^a zv4DgNM%gHy4BNSShF%2$-=HlYKLxvsdwgvf6JM0fOM>GWnI$*xuOuAMgEtf8*MG>L zR8H!)vpiQReeIlE(B5?b95@+*?MJC3n*2I#J!^d}^Mu~|Bl2I5Hzi|JdJd!w(w@~$ zp7hh7*KeSXEYPZoE*cW%Qpc(q_rlxB;lAT|YlD{Or$j{L9s3$Fz1Fj~3p|g;bA7U} z26@O?W?V`%5yRhy;IQEY5m7DBOtI~y;y%OCV{pdIGD<3@&^@bDl5oyWe8x|sNJDdz zlT>Eqr%9mCMWdGsyK|eOpT)^L$kTbqmvk4mduJzGlpaVES!805U;8D^L>GaTAf%EcCOO()ZubS1~)+ z=LI)Amy``}K8=vB?&eVTk-FZ$bxGi#bxv(Eix-R!LS9do9q`;M?pNn@rA9J(i~ctLqZG*D|Zy61&F|yW4V!p#8kj zeH(a~ukfBM!p`_Qds}R?^J+$7dLzsG89{)g8qa#DP!EH`-jdg~*TOtlvLT~{%UmuF z2M!^2ku@6rMVV1~L<(`Q)YU>bkroNt@tqx6MXa@_Yf0IK>ESKJ#h+hX=k<1}E4Ai9 zr|PnLv|*)^#Aem5hUsN2NtQ!=eCnulde55v@7%0lj``w`<$5a$)Ay{L86<22WkNDv zZdD%g)gSWb2T_?c@P>IYlzBP1W+$ofdg&Qegv`PQF?|WIu`y85SP!RIxw!Szq}fdTFppnBbKovjgG;)7 z1ndAup9Z!#Jb_j!R`Afe_rH#gV#_;7gt&)W~neg60M$3Hzi zxg`Dw>6cJyO`rQ;CLx`AbK$?^Uy*K8tw|~C`q~t*Mg_~0f!fAooFJs`aD%cKiU^@;Gs3vwc6q{@G>-5#m`}DT? zkVgZHvQMJ{=4P(*U(=4d(+q1;7JRoCSh`AAeDs$UU6pzo9IJZV3oC#F3K#U&*7+UY zH7!>e8`s85R7yG}K?lP2jgs<%dl`ZSM8P-y zi(cu(t)l=)N3IlR8r}60Gv-^l;3?frhzjTl4UO{mzrfrvqJd z%WbQy!auUZI`Zz9*aL;J|F>QZL2-3_4+n^clUI(h7_)cll*@{>uf_YU1Bp}~0Li)i zXj`p&S;es-m60Hp@Zc~uBM+V>qW)nIg`P*NynIR z*S)no5gSmklPl1VHX3^^lfNKTE>(kQAj^~Z7gs(_5ogM;BlOvi@NLY#U!zs^HAdAj z?ehRJR`HmeU*t*Ed4H5p4Y;W<|C|4KeEI{6Z&uyfYKe7LvJ64J9Z^h(EYyp7fFBHOAcM@Af@ccnjC4XsXEfi0-SFMdy|C0 zO&~~qKCL~QT4P9FOO#wlBmCTyidoO|3&I=JL;~Vx_;xFy2t1YObq>7!d_vwfvYhTX^iRS?mhrR1f2iM6?wzEpK!VZ{^Jf)$Ny6sVO+uRWy=mF7zgG&uttM$ zxBsv6z%9)G80I`M)ChY{@`5$5QC8Me1iZG%*C-uJZD{_Mp2POVm~$6TF`pv0@0W~} zWJRtc$D@O9dM{&;Sz+RX4+Xy!w@o{E^VMAQSE2Gl!?B=?KKv(7S@mxq7>~#b$uS0t z<($}#K^iM>nmhkE0vLu?+P8PQ`*!O3mYTPHtr|*OTq-MkTFqM~0yo#5w{rvU?E=TE zZue=C{Om+^K4F`am$3aHpuwi5gY(T*Z)>Tl8bF1SIS`BiAnG1rBeegy|EC9;9GL$6mBO65*!}aq?*(%4zv#KPYN~c8a zKgI>4$l?5A)e}BvlcCGAu>f&%&@c@+$gf^d-$0WL6&m&N%+Nfepg$r}(zw42e5+!*MVUjQ77X75vJ)>Q+K+sO&i6Xbe zBMBz9WlrNOcO*}?c}Kgb!KGR0u;>kHe}$Uc&t&^q4_+7IjBJ4V1jGVKw0Mp>?lhc5h^^dOO5@jD(tRq%qiZ-Ht8<)ka@m#)@*T(<3ZdF$H}{(gxs;pg+NloK{R6 zVfLK{9}iT4SyWra<7V56Lfng0TsczaV8A9xT6wCSnLru?C1+L`)3N{1p@EY0mHov zN&%h#aofX+uUiC6)i)_$hR&+SZ?r4|n;&jDfTyKw?lTj!+1F1;ukGa4mQOa8F%9(Q zrxtcsRB4!LHK>j0X>Wo-XLP3Qop$V0IUh+`;gO0XF*#Nu^?in!pVdPfMTG!&_CB8( zVp1BHWmE+?NJ?0N?SBrhpxr?WKdU*Wx`EIA(CdmkmoYB)<4F_fx$d5E&fe$ie4@~V zgalYB!^PxDcd*^_;{OK(f-f>MVH=kpVA^BH%XMPVivZJxZfiXnMDhQ^g!wT4oBuFD zDD2r6h6!9;b8CUZZ1?`R)dxbjc4@KN^GN|-7#hQq6J*inTQA1bmX63oPNO8N zqZpm7C($&jl%}2VlGp4%>Vb68vh{ZxX}R>G#$t#u;B`>hu%jvgrYf|Y255LAJp5+( zNu+Z;d9LH}W9>^qke9syHf#SKaxziz^HqU)BjNSBg|#*o&e|Oq#P()}ygpEvLnGy$ zb{wjApbv9*SK>S?5e3TU6}3!?i1_>$K4(+ZIUXjQm_mTHNIMy0rCP<^ds+^lb|OG* zgjB|%9;adS{5$*4j1q}}#A1t${&l=(x#yr$62BGTLzKU1jgn4*NEfi<1^DT;JIB~p ze$L%8pJoNXUH+LPYHs!+FPB;4%%anIMdq#AbtFO3IlqQMi)vl-FjeFle0O*0vEki% z8Q$I9zbmPc*VDVwHMG?}49KPUneVpgNs??3u9(T2|5dKy>*4rpM5UxT*vB2zz&xa# zdR#Gfvvk~Q?t!YTjm1Zk_4AUZu-}38-B(^j8`&Zi>_%6gi14llEszB|(NBlW4`>cMu#xFmfCwv64X2 zejd5_B6C_ODW+Cbd^mN4G;>=xcMOf&h8;l?Azk6IO;@k_jl&3Of~Kn!D?^#D1|E@h zcJk=ptsMx&y@OkK$21{QJk#{*yPyuO=we8Rq@R39Ilg5E{ zBt(*$TFkK@xBv@04ArvwGqvQHbo#iq$}4={Gm&i7o&Vp-K|;Fqq~yp&ql9!&l{QsoK+!JZ(o#iYl!G`%DtJEIP_^Z2#*X8$p%~J=LHF3FX z`_4vJA`R0kZF38CwndHy3eOeQ+Z5AaBf6Yo_7NPl9TA2%|Bd07XK=T1`rr(H#;8x9 z&4EhD-ElB!16Nr5u|}h$7V(f&k(^#=G%^m~K5xc~YX~Lo`(9>L^LKH9%y>!p^_iOJ0Nh4?{*NM)Z( zGFL?}mwgvYmJ-w6%6*F7y7soEyBtL#+GE5LzUo@f!J_XV5Jm8-k<&=RUUf)cbwPue z|L}C+&UWC-i9~>q`JUs-npP!PUq=Mt)ZE83rNuoQA9)@)H#QxWydABNUDQGm`wjCm zX|Otc0jfQ99D6WIgRJB!v0Ttp{Mh1zFBWvAh98nG#LfS=#Qtw>!_|9hlk(D@#TAqN zOBSztd_O;LAL(kI-BrHr9{?73(`2*Io_s)BjY*M}PDHvG8qU zCRs|R2a4`sBarwRY3vJBwy&??`rX-v@qKfRy8xRvpM}r`&&WBqA#B!j9KGQ?Y_;bx z;&UtAM>^;i{m_}%L*Jtdv5L|nxDdVNlXKv)%noWN7A z8X;U#obnsBG=2RygQM((7TwJXXpX{ATODT*$sy02x5tT7)f* z9g^2Dx$7_w7H0?lj{E+J?ZY`A!qUXEDm$5AolQ|dbVWe)?csi1vXWt{(pJ-iVN*Gj zA?+a`o6oSNs;2!=;Hq*p!*@pvV=`J$&XkuQ)QpNbWn-InP=)HoO3nI_H>o8h6`Wu! zL&~DH@tyfYS*0XfhejFBA%(5<(1f~qCg%KV?RtndZOoAdrWq|-)V?}Ca>F4WF&#A@ zRwJzv0I-fe=_bbQT&sskSkY&W5g9+3>BW)2XX4Oo_&AK6aF@TyrxlyYwwC7m;?xu&tX!(>U_BA<$dVj_m(lYd4s zGeg>-<7$Om)(<&x{|^2Gfc?bg)S_UnEx&c!sos?A3-FFzST0z5=7>&O^?UH`Vw zz~fLS+(!ICXi71YDFcWYzh6uiPch{iv6cPI;Uc^iN$*a!a?;o$keHaLG*j`P5J7f+ zey+u=-6|1CfX}W0TjS8WVneJRa5`kZ^jllk1nAj{vRlz>*W~5t8E&7=Ap-7sdv)32 zf{Bw{En8l{cCN4>>rVlG{{91EqUh*vr2SnP|5uvoOTSn+0X}{;Vn6g_$07*@#d@9j z?fptDd<~r*oih!(Co!>9KBv7*oQK)q?Dy};A$o`fLiqNdxM+b>6zU zICL0=2(V-Ej->+oN{)h$4gYg7mEIOBJ28;{NDfjB<*z~It+{Kd{F`fh=4?P=6C^a- zE$(6k%yX2k1>VZ6u;9Tq?anxmU#;@a-740e5O|N!d!MjuZ^NspB~|#JtSwU*k8sWG zV%fiGH9p@vcl~X>Y;OhV;EoLd5M^!^K0&s_>^}VVDX?7CLoTD4!|CfqL#90y3f@OT z|3)E03TFnujyJ{ZV@?><6KqN?V^jF_otRQ4T5x#0;sCKpTWdq6YSdQh(ymD-I2tcn zn_5(k5VI;H6-t`#IH+){Cr8!CvjFaR-`V{-mi%UPO8SVU`-X))lk&k6d?WmTOzsag z`UFa#098|LZ`0Nkj#_dMUv7IfyFZ(*OxJbOK`BOpAQNmCX%ZeAq6o9bmT@^TwiGC~ zlw2}|g??x6|7LsGauLS9fM`T8IOni!U?C;Tdn^1pA!CaJB)Snft_b_&N)07r!@m#L zam^73)34rLq0WgyAL7>_trW$o{?~fm`6I@E&e`3(2%jiE^}UJ(K7-XC9{rS{U;lhc z{!}3D50jEdojGnGggzb3PwgfUhyw&d_yoIzY)h%lc&RxD!wi)(8mI2~k1!Fb|2p{c z^N3CmL=JKmpliOYCtcED?38)&d+Ki48b$TU-_Bi>PiTAH@a(jF>YncqiA+}&mb6$p zMz+5V&}K0ru;1jX@gwyTRwdMi4HyYaI|1iTDR3pLt0ZHD%%JpY4d*?W3M3d%jmOQ9 zgNN4#V-ii*ju-Nw7E|QrxO&M1v)56oKfpv4O`J(941?^@tGIQ^F3J~ z>G8z~ZCr_R@c2qj=RzOF$dv$RhD;vs%d^Lb*p#8|%DEiFM~F?wIY&;xR&R%6<0CgT z=f+?Michl2K^oRquY6xonY}(-;TVD(3W$myq(&dGfj1W{B8*Ct_IsD%fI5g{@wqXa zhkh+X==xaD_*3n|&moc>KRBXl;BL#6rDs5=Z>^tZ5FjuJFzjOaq%M1|V7acR{gmNR zsgTO84?6!n>7r}p^Lu{FE%kK1?q+B@<`Z~)On)x!cI-|&H`Y*6KWDFE>~6*C*}lF= zxtq9b#%-CEjNL%cuz6KGmCJ|LzR@nUBL7gn7~@KH5hH@>ODDTAC%~6(Sf9}->ZKGZ zh1~3iKOpkyXdITkipusdxEI(lWno{pXz`9uOpJW~<|}l4GrKEfZao7UK8FWe>m)-v)x?=Ts!(4d11|Qg5o&E0LzVAO7t#64c_H;a$8HoY9lxQ09Z0}vG9bd^fuvf<7wMO~{>oi^qhFPi?eDSJS zUP~R19?w$y(o%zu2?Ckek^yJVfKNj!^)T6U*_*!6;574nck}z;2Nk@nnwP|sX*RqZ zSjj~TVh3z>dGlj?vyX5~t4On7hnL_{V1s`Mf-}-|;4Gl!3R+n1YumETDX=b8Vn!b# z&!w}?=td8JWL0$YAx;4$ni3<$lq;67fm#}tA69rhg^5KmaxvcvgV9n#$W z0K40CS$8Dr26eixET2%d<9%DeFSHWoJUf&!G99esU*s0hX~!W?IIi7P*7lPHx5dN} zXL9np4WjC`-P?TaWZEu3J}!4P8>GYf0UW}xmsqWGC8^#=m|H^n}QkB|j-Lj#|UgRUq|oTFQ;OIu1;H#%V2>fHv$OV{C6X81NU&^&L)TgVI9u64mEv*|m34E6Epm7h-xYv@k2<+bO z*%ll@U~ZY&z20d$z#L@MUE$Z(!oQ>2GjGzvtll!On<}B-Jq~CZT4}VdwVSC)v~pHx zJ0M#*JIFvS%C(bSKlH(8;6^0&QAV?aBv}V54STg1bX-cY<4Rm*DOm+}+(hIKc*YXVBmh z+}&LQgS&I)`_Goz#ns$UH8(xoy`J^1>~-%OF6l$Q;n6std93ufkz0D;u*S84=b;$* z(v*=#Rx87#LCR^h5`KjqpFYs7oX@`8=h*#7Pt{>tCHux==G?4C8m?>%&?Xw_k_0qu zXtindyf7NpO4+R&?L;?nkGLxYn;$JOx~9Xe+U<0;f9Y-0I2Dk5Sh>osZRln^&#{bT zWBQv!+RZscWpIvfPUd#0|D;;L9OYU0WD6lDJ%yr%y`7qi+W)xA)8O$aNpL_MO4j+` z*Wp8#@K-gkv?NEMo3^eGbgA~(V3Yc&takhR{^w0KjvxHfddO za1w7@p8vnWsVB@y$qc+YJ`Qja9%8oMa5(O`BS6Sabn z86FejBat)|dojx(O93TMC^;_8{mwz8QP9Sq1DmEQ_DuKqL zk-&?fBVpWiS6qQo5dcGwy}_})9yl;Yi6u+P`WE+e0^JNF0_d z%x@Mt|HuTnt9UiPI+~>Hn?Nd&s}<|`#zT~DIfs(_&&E5FD%#d=2+rLfSuO82)V^PA z`G0i#2Y+;J0Sqn@a{Dg2f-g3^YfY8F<09Ma4$rDq$EuFcdzIk_pdd^_LSgU}PY%>& z3lE843(Mg%ZA$g`7*&N5bq;S$%Y8c33Noy}Sj|DrkaIT@EF+RhQX%wF_YkKLbL=jg zwsB-uSazuj>IhzcAnLk6Qa<}J(w|SD^H77!Kd@V*?nJ~VtDuv>0pX@?o;H3~ZG!I| z>x&)pi(MS7^^Si3MCvb50xvI+gcg?7&ho0!n!k>m0zLJ5y9IX=T}49;y{O5uXZnrv zQbU6FxjbJ9EN^dShlsS)YFn+_JIIIC@(8U)NSWl&xDS*#YpxfL5>hSOt8P2ptMs`T z_0t%K8J50RwYGsgJw7USq8|1>Ev`hZ($qr}e5v{Cf@UH(jSoE~ z@*Ro>HtbM=7lTv88>H98iSjM1Q7kupQD@RdloZDj@Vui?x_oo_J|9?nQMJjd{+RWi zLNuKB^;asBn?~nBh2N}N^Ne2epkCT()x@2;1z**iw5_VNjjGha&lfBE2bski;2|U2 z*iv52*4D!{=+oOuXQ(yBQ6>tfkL!jM8NLBGTR#pt3elNh*0eqfdm4nH8XFQk%tvy< zG-#JZL(X{Ut>6?_Gt}d-NY?+J)1n0M-ON#8+=zoh(;A}WP&|)qINLYYWN17lv;am)SDXN_~@KXj+w&A#TNhr_^A92{{_EfqDYcir@n zZoGfgZTHth7zx}N9fLhl?7dz_F(vQ=Q?d~6S zY5hCG_u|VHL0qgvWunVuF}9A%;Pt4_^I#T&iUQO(eFSgjUjnvdN|tk$jmIxY-9#I& zhcn&neeVUqUAw_uDjb}g^_Qkjbb|GHdv7yP3r_T-HpQtV{CF;!VFqbAIY?QtO)A2v z9;s>x3~Q_3j~(-FJ(Ft_f{Gab1pu$eRV0r~NgsUu)6WHc*Xi_1 zRok;dAYXdc>pbzYU+)(@B``y;4x0=~RQg4jbsOb4)K%e86ox2N)@1mMT;DVXFrje= z>fwVhHFkU;6wOMMYf|W!M0#IPUWzB_fB0~srs{j%MtBWyw0^wAZvv8d%2&6c{d;np ziOFex&CW+wM00N}6S;Ar1JbX|w7e=A502Rn=K$Pen^GO?c%HT+o47YkYyjN3`70$! zZ$h2fE!Z_R@wimz*GHJC3z{jMnRnk2*XO}Z{TF#5R~pBUH-{rT(cWa7`bNEndCOSH zrLL4s$-z~cJ-f2^R>??IfvZ8!Uc$bTa^nik7ELsGp8Ol^W>u6?5sv_db}QwFqrxa` z$r5ymFD3}^KSY>?I1x-O>jXrpW9LH<2vtNG+7wv~lD-){6m_T*n+vnP*N!+kK4gvy z(jw9P0hV>lLkdnO9TBIRe>GPqvx6PW64$v!ko#gWn%ztQRaM7s*o?=k11RiF&3Pyem zTv{|d-BP87E?1BkB77owK60?xtlKav-I2{-v78leBudjMghR2Qa2xgHYAq@dHeWoa z4%3q8b-7=&ZVQD@!vW1?jC6NVCDYSGB#Y%;psJ90UET%Lqx)C+hXaYpnN@(nm-&Qz zZ_5R=zcnfxskh>6a!ZoJsrBZV4{rT{p_Zo2E8(_0qwv^(Ni>wTIo9#1ij-++g@|Y@ z-rqGoSJOC>{0_@MG%L(OstfE)z6Lj55;%3@(0uCUk`nq(bW|}f1#}b#e)h^ag(~*4 z!Fg{dXhN@dXxNfNFF)FU?_k|sU%jb4TyB$s(tO%?-)H3FmqBSVR1G8%xTQ6qFHTS-IpOk$2Ycqob1+z8xCP%QqcOyz^wu%h}wNL`X0ha z*Go9{n2>5|J~J}X5(5haw}LbA_O?NE`+OYuvr_jvpo2y?Ld?M5EgpV`yFczVNl5H@}RA(xQB z42NKY)LE>^!ixMW(TlsVTU(rS>U4YM(Z-^Qmd1$6C1kc}9C zMsvk#;7)nqPG-FNR=M}xwD&Qu$kOO-KT-oF?U(Sw%uo#>7W!c`!-=X)vnC2PO%*l0 zZaeOgm?&3}JY2Lps>&>G?j23n8LWD7N?maHvMtp(cp57k=wd5Gg%Ait1``oP(I4nk5J~qC zoe8l&B*M`dRKG+sn0LZrl+uiIm&v$Y>8dxlLH&7~CABi*OZC|L@az3#w0n!Uo}Q)G z8n156#B(g6S;2Ck=|oAbOS*1m)t{WfeXzsVvK?8(^=OXVAdc&ZcjMUu_-q*bYCrwn zPU-Sqv*CV*S}mi+#H)+(k0nbGnDMntOGawJ7UNP;~ z>q_DIR?KERMPn~Dm#p}EPR141dX$2Rx41c|>4LKQl?}9|BWF01E`4{Ha`|EO4=HogrDtY!0z8+fno7Sji@Qp^4OAV~zOjS5v zvkVqOu!IRd@)(3e+7Dyp;C@+6c)Cb+RS|Mc`eb#)Bw1z&a<&d`jhpWxBxS+S6?`W& zT>=`Eb6;#RM(=G#7uTB@N!M!`#yj`VYs+3goSZh+*sZH>vtx#RJ(RLsOXcx71Uo{G z4BEJutX89SJWQ5ggi~FGYjoPUK#p(9{8es^Jn?6f#y-F{x3;hBdxqH7qj4kX^sFx$ zcXR_95el?t_$jo2T)s>pXNs?vRs8R-$vJsj`HH%wnnScj>`dRnH6XSBhDX^LGs88* zVm64idue|eGY1nXaI_k5cB@gvmp&UdCl+`yztG;l^7=kh%hUY-=0sMS41Lh2!O+1= z_)ZlIFIHVJaW~RoM>p% zrqoMHb&Ofyjlt)I>b2V<%L9P)mD}``;``YE^+5tV4LFCgh*KPHzyvLa5S2fZhbtf zGhH=*!4skQxrf$MV^{lr0)B9<+dWnpiaGR9j%-!7*`*6L?wA2j%rsuLSnY>fkoveR z{cI&wR#a0^*m^D4o0$rdDSr7&Xgn&_YzCw`fwiB*t#JbKkxMpfG8xulu&kEqJnk46 zZ*jB=Y(K5gUY=iGIy~eEate8Q`L((FR15yIq1>=9h@QG~qZQl|dnQ0o@}JQmc2ZXZ zNNE;#Bq0^vaV2lT_OA!|f}*cW^Xtg}InvPZT}4j?_52)K7upvLQ|LY;=6%@h@Wxki z+zP5rs_r1cu8}rpk4Kz?K(AI6mBshXhG6|7u`GIekI1TsKuuv&p<6X$bpTHV87zS( zBcqALeFCFv#pV0jvQ1wna8;xvRGaHYiYDL91`@`-@-*4copAdqxQgqv486J7R0p`` zW&_X+ZBDf5_D?PGYDupw;=8m0kEab$FDeI!n!(=Cv{fH_xAs?z3r|=M%9FA8jvdCV zkqp{J2^`|T528~)&GKy-0NYNN&eKV!$mIn+Xba!h`N}W!@~&XJC#lDBxW$*dZhkyH zKe>cYL!Uh-4AK-kwJ0$J7^Go}mrNFwI-U?KLneXR=K`-PLiW{pRHuXiqcCnc7{t>H zYPvZr|F_1UvW#=l`Oe~a_{xNWl@2vEjRKx|!7{bGISN^i(kQ?-(qajxiMkO+0f_qb zMYJDxTPQXR<5nNxzQKsfeQ_A4wBv1C49cX`vGk1xY7V6SWp!YaU%4%vCcVmKX7kT^ z&XXpk=iK_ZjZ}5q{w7f(ml~}l`a^CZMnl)QnZ%$Db`SlK4_ih;l&yNNP*g=`$lxoP zw7TkV1D_izW#$lUF>U6BkbZ25-!X_hWYInz7o(a5b2v7m$??5&9o*IaejRW5ChPo> zfYbNQFUQe*%cF?PUnK!pzO;XCp;|hFm~7t`I6iuWFq#C$bEbOjJ1Abp(Ec)gomAG; zZ=+kaCF50c{c1f8e~&NjkAx#ubU|x9N;OfbK@ zU?7|3$`)G5dq}}<_NsuDndw($qR<+^^x*EVbhlwms-i}85fkmxx-M{1tt4r4=Fm-S z1%B#pZxCUDXb%euii-A6tLhT|%RrVGPNxeM+I7q0h~NYSOBeI!q%_-95Hlm1S}vVP zI6z=YJV5dm49x$1VZPRP!AAT$iGd6&ZfHp2JO4}iX(tp-0E31F9UeNQkL*YC9gL}< z_~&K4&;6;k5;O_GFP)CoIB&O!|1^yK z-B%pybs#i+cX#hfc@~D_j!`(3h>iwMZe%xf!3yTQ9I@TzXrsBhzgD+})LEE!JB6UC zn1Y0^gSwDmNJlh*cu;z*%~8<3tt!B#*kfpxz=;&`B|ucZ8-pm&_UsKQ@WuX`g)fhO z?0qUlSh3yxCnQC;6hH+79ku{W^Q*4SQcXbL8JCbkM4nWii~JQa8V z9#svMbT3P1_=O5G5l@A&i4Q{3oI#{qol2zgN!@?0^nePb6E@wbhs)jn1=!kidn9yx z#e#okVEU4gQacsY%E9Rxm1Q^7*<4(gaAXt8&3+*@v`FOP9PlrvisQ_|Qa~ZK1K_l@ zmyi`}Dm({xiH*U=ImEux0)O$*dx-ivYGv?i&58h+Cy z;9~SQUjxTVuA*Li(<%8r{0p*WJ~hC6syIpkjHP3SR(4W=?GF`uuhY(mg`5S=n8_>@*o!CPBJ8f|)iZm-&kAqE)#)7*ZoN8w8+Nrd( zz<~+D_htDslKH71j2G)T#FB$$r3fkAiqk6ff&;TFa{_-~{t*?dIEwdY$-jG-(As<#|ET68e{p&%Wkvjb+mn|(J z+ofs@37PI*L5vG9cQXv`a*ooay+Ads38caf+}TRfcmxD7_iuYTJK%=}|D0AMzwAj88eihp4Km7S8kYwT`n~#TvcsJp~S!vF92h3*3xQHjwPdiB)SYQqwL$`C0$x zpd53wm25~z0FpndNe#3(Z*OlM9<2hlTzNL53hUuj5kyP=fdc5z@5O^9aPVqK3GT)B zleui0Ds3-&46dq*_{(0=;9fycmJjn#zQ2Pv?|2m_o&|UN`gx}im{3u04LQXt`easT zqVQ#rNyuR3qIX?vs$)Es!ZzGZ!0%m&Hpc-YJk)_kNkwVZE5vwHR5gV}aMsJBV=A(7 zBxJ*~)}GWSm0849y*cj1t>Jf`yWIm{F4-nNU1lD~M;kJveSZ1fh>jMlMRElEx$bCX zaG0y!gg0u%_#-6QELNsJDqE#p0M;uy%}SiAg)t3+MH}Ka8W_VgWafFgx<$G=MbX;z zN+X|R6i~OP?;H@#ngOZ%eW-vquUpIIB3AP<^JfZ_0S9GG@NDU7lF!7f#GmZYgg>iE zmA%?ryR}7agFuwxI=~Ij+`d9nPtwg&3V>kFK-}AA^<(*MM#`o$?TTxEDT zsp@K!!KKn-NsVp4f|BM}qFUYKeN*f5$Jy&+zRjvEnKcvHkFYdT?8I^m1V|(Zr!GwG zGH4j|Z{f!H@Xlh&FpPNVK|#tWNOv|WF`D_|%jLy#cmGaF{gqz*j;CB(ih{3=CZ7y* zJxIa-TtzK%BpTPIGwFeA27GmVzPBw~tTvrji{CQZXYIIC!NjAeD&1wt&!DL)Jvyc< z6!2#zgka^pVs~s6_zHFWm?-XhQsC!9mq9W%Fny@}?rW)Zd7Jq-eYXfZ)uI>BaryU8 zp5G<6H+!2HwQj`7l2xCCOlH0+JXvBFQ3+yVl{zMTYDm6z31%h4gQ+CE?{gG#@K;%< zLJ`c2Lv8%Bq*`@agAN>nmVMeao(=%sQ=#dL-GI8|NgP++={V}Wxylje+W~4Le>@w0 zbV|gI5yv&w>R^R_CoL(@SL3CSpl0KtbYa`^;d2>b=Ii0v zd6eI#cUz7vs;8$`JSHA2Of7FiEMv=jZE34(YEziHdRRW+86#@vUT*^dH&6aCj@#(1 z3LOGCZ8SK#e-I3SUkr<2G$|SnHw04a7G$-osK(vq4*6DHy8OIj{8nLCxw3JV?LsrQ zQZOI)*Gq<^xjnEyc3P(iCoW0xUewxeZ$oRRV}?IEE$^nEFQ#vXmCga&m#Dh8?PQvI zG#0DTdU=%9)22sdp+{vsb-nKTja*J)-lR~AY@5TB=|6_PA z^}u2v?&sIJ42r+6?DD$`_;(oWZ_?b-V(5F`+(7bc92#1*f#ilU1B+I1@a5yJ11eSL+0DA~(5eU`CGe^-qV4dSvSi`VdpC!O;O`lu6%2x4MGaoIow+A$0&$LkTDEhp zbYD5Jj#1&@vLKIRDn7d8KS^HR99mFNF0u*fvqd zDD|K*K9}m~ciVvrA5C&C;{EEM`tC`JOy}hVLzP8^xhh39SuUw;%$g;sMPY74`J3j| zGLNEbdXmpJKO6;*90oU-+FD})UVxh(bU;ApUDqD}w)wN`s-xi#Tl#my3)gJG3x?Y# z@sGz1yujBXE*|Q-HkF<;srMVg)u{*^9!GuG;U)LSO?p44j=Ll{btSL{Ct` zAJT6Bv*W-I_o-(ru9SCar{mbR+wh&0-5nVYA>Ci`OVAMv4ZGkl8DY%Qw~lwirRu=E zZRgd=C9pd5q$lG@d1#;Sk5b%`b)!DttvjTp1nlr8T&;t5#5#2B6V%_+w`okuss1}O z-f+!s+8IJnhu{M$9XhqpHyFIryXe#UybV7qBi(A$0pcbzX~k$2RSufPON7FclYLR9 zu2dJpOD`%AoIW2LgZkxno$=tj z%-vtGV(Jnlhou+Rb6?#1$(UJ4o7I&saID*L zF2L!*4s>@iAV`|Jbl_+En(ighNR(JDmWn`-IG%)5o}9`+4?&O4g)G8AO(u%ouFDiE zf&oEAaTYEP3H@VF-bA_hOSpc6mXkrF=`)S-s;49N#kTXUFN+Y5U!&iV{^UevGQF)~ zuU&riNgOqknIcl=T;_#*>H5gs!Hi)0L4|%H?Jw5D{dE0_bV1wktOk5YZpzhcxksF^ z*Bg9aZfix_$qu@4#kEExP(TEyu-v+*YFEd|7PyVU0-8_#j+1aiaHtxr=JGQ7S>HN( zz3eg$=S|x4wt;l`xO}t)cvX8>+lC78`}NeZj(O2eeo-55d$CF6d0Fbdy@uy`In;J( zXXyDEkASvC6L8?L>iP)Pj%GXVx#?AWAiO1fr~b$fLm60`#n}v9Jqaw$s!%IQp^Yt7 z!kN0mhMjn|P|#O38Z#7Gb%B?oaaaq__S`u-cDeoZ>!wn?c^QEfKTsU|Yx-&@ZQZ;SydDahff)7M*84a^p6c zB>1HGIiwnwYw!O!GXJ=l3LTSviV;naEY5&{*g#x>ieW`g{uRfU+{j@NO+i8>$&hh7 z6>-b=I6cGgUruJ`-up~o5)>Tj;EO-laapxp`~jg7^MN}M-{+n8nem9}V>v~|gnKW2 zp_rH$u>$34>37*d3*g_Eqi+tHHM-5*{V(^RG}#jHUsta^7(_hI^=zyX;ujMY?TcmU zegenAOv<>pxWSjBTYVbH;NDUNaAwr_87D$!{Pl0weR4ikm}UIIHt^@%w!NE zQ)n_{+$OZ{&G=7I=Wicyzwd};fYP1a*;Z@#~4A_o^@H|M(Hq7J7Qk^qqE>zci6A$8MU8ZdAxLLb#HB-yX1CB;E7RW8$ z&`qx!#tZsklG4|-G`FS3G@H0MaYv?@Hj1~Hxj$hhV`R_cp zL~eV19AEZiew3lPDCA-1@o2SAb19-WL+9G3mZjXdq1WP0tVZ#J5Tuz7%S&;^bZqIQ z?MLRn^K6YV99UayOp-g=u56Rsw9W!7#^whBgy*%Tf{XHlXy$%r=@w(_{V@-&qwdZ| z#JhFWw3XM_H~D2|OU`6F)l}~aqLxLD`3`ID@Y!CQ^Pl~eq#8sc+X0`g*!6dOX;zu2 zx_NPk&51H7(d1+Zkc044YDF&4=@o4Yji%U|R>~y=N#e?w`Lbc6!BV-!QZTa54DDXe zEre#z5pY|=hHNeu@z&ed)|ZTG7vf)x_<3hZvTTqPn%S`?$pe3mtYf=4XSwoB`(_>M zqmWA*tjwZ&a4SK=C#ekuF>_HE7kQIK%S_OG1EGoLnnl)Nw-gp5Xz{1*o7 zd`vMDoMP2-Dl)2B8*KVf*&3;s{%A6EIC0RTOb7{1aeg%4qHdSVbIr>lKgj|K)2GVg ztM6v1O;591{~ym59${l1(<8S^%dhiYDJK@|$Vck9^SM~3qvk#?Cjf!D964d#Gu zEDx;G&En?76ULuYnktr+p)Cxbqc=%CMD?9{x5vvKZ!a@Nbqk+6|N71isJAb^ZEfke z8CmZ78~l9~Gt(uR5XQ6@=JKUrC>94SSKls_FL74tsr)o!*X`JBcW3XS-sit+Y6G1n7eK>dQel8s~GyN)A?I>vn7D-9j z|6N*TL}@G8|CbM?s!4UV_^`u!-dv6iS{-F(8P#K;mFZQXFj@B^#Sp=gRV)kL8&*z= zIB=~Z?3SXKuf^Q;8u28)_=}f`_{oV@NLvyWcOEKS{_|sIr+a4Sqnw(6fY~ep@uZI; zMvaXzWU=>)-wp(ux@^2A>(rmbYz}epTB)H`5&lzNUP8QOVdW7G;p5ueNA+pn$|C_B zawXnM-pBG9=|^UQ5r@AB97-HnGp@H@fX1spUUg0MI8T5sNQl9B;by$2ld^< zI?|n%jT6Rpv~`@Su|3n1rb1JJAs(7imAOkZXNO){M{oAG9}o4dTn7#)zWhgT1}1@U z;K`dH6SGFX|%egJCe;t(+b@K*?l=uP36zN;RcFy7@=Afl3Z=%9AFax%Ur zF}0p9fl3(f#=M%7bG=NJslPx(V$vw-H)tFBgZ+~(Y>M|MLwf|jGGdU4fav++v;Yu%Ae=S51SW>e~OmX%TSi@=95 zXSwjQj)lKzAK&m_nLX^^PY(pOyo?jR{-$Q zdB18)o+aQ{&{pjI8Rf+5>Ups_i^J5g?+u+RiTx<`q*NzuDC48`q}eq7an6j}L^27d zCA0#*DDVpq&smP!VYF{zRX?)JOiptqhotGO-`-lEd^XnbSW=Y8N03UVC=`bP&+L-CT7FJUOz>OOQvoIu)ia^6dP?!U zEKg}A$=hzL(8oU>s%Zb`!=6}^>z>^KG-3S)Q_1S>R&Z5~j67i4<-vm0N$QhH&&b&E za$5CMkRc-)g^>G=biD6L0RzPEYOOqi;@HYkbvBsblSmn)k^aabM4xiZcL%6;9EoF|KpPQOa(jsb4mCO0agaHtjPDE z$E~L+&z4fE@vm>n$^_TqfAjTw&1b+i@+mfeuK&3Jp@JY-vHdJPl`CFSn22vx44Jea z*5>NNv@wqna}4z-g6q>6GboN#F@>fitlWbGa)!zZ%7q1!meAMr=xbEd&Ril# z%C~A=cq8#%13^Je0;km7TK0a8m=4*HpOSsEViPJdZ87wo^oI;(`e~_ESuRU+wJ(Wf zyl3`#x9(wG#N?i@IjyzZEm9k@g$O2)5le+s#z2FDWMY`FIJD754E;ezF>8&Z8=rG7@ur+9MY&AD zjzSkv48nL`JNL}zq85JNo3`IiXbnwqgrf=IJ?CXVvCF$}!NSVL=Kl`R+?bV(Yo1SF zUUQBjIw6Z#ub5@_IYVD>q>%no-{;}i7D~b&ht@p#z++qlW}XsmmXZKjz5n_ug6EtzHLVLl;BPX^67pP-(fS%A}b> zXy%t=nks2iFNlZF8Un0>n+L!y41A=Obh0H@=ZHdBw5x zhl{=ff~bA}@Dp=b&T0M}F-6V>iXRaT<&6Kn6$7h`x#x(kh~5pIEvhvKrU#UR(y*`R zUKjsLdXpvkgM4yYd~92>9eyfP%B_a|s)^oKP8XToq~$m7Cf*G8bk!gm*=?o-BKPn6->42f~%#mMpAdp32iZSh^?vt08+q#q>9Cdxx-K z?5_p~u;<_la21yEZ*<7FF^UAroV|Y7avJM$bokHhHDI&Yo5g%fJnM0%RG^wRlJig* zS92Bm(KR!5QAB&rS&q;5<2N;>v25OnV)nTrS{A#r2dOQy_4rS{kn!B?aOVIk0tdNI zeql}<|Hj;z&mPh-hJ|eLRaY7%i#DNAvgyTkUxL%ejSR$EmFOiKkrDz((Gq0M7_fbY zL*fehNBUCWlj}AwwTLC`zY{zCAv3?wqXe=18PVCFDOC6%m7cw*d;8}jFjM`Sgw#p8yZ=i#VQG&DTzRMO#NC(Qz7dk6 z!<%-|Fq4WEIDsF#J=5L)GLeX=J&C$M`;Rv?8+<8nm*n92`Y~=K zu9^<7PT&3vPjuc6HW7lf1Eq1L#O~ zoV}K2ZB;^gJfBrQ+gPC6jF@G{)@LCYb@{gw;wOO zMmUG`1zpr}J)8$DMgDLej^FFXCz)gg;o7_!fBw`#1KxEo70o*u(i@6K6fw{U?O*X z&FSGdJ4f{8JNC=H-H-e}Jik8XrPn?)8|^W)F6K?JntmS0JIgjK`KtxJ7ItiX;z`fi zh-25GcN3+!Ec&`RvNICi67IsL=$*K@j(Vpu&wU9-Ab!5q!t_PbZ|@#34I-2!efcnm zQ8%uTYEg%)mWxPnON7^;?v3%n#Wf)cLxYVlq`taC={o5-e$KTT9En~lSAmySq|C5gGL*T+6aMwBF6^MV+ zt>|l5qF;xekfF2tlf|Tn1y(wMjo&TKp8lAzFX^}pjvwl$!F-Vd@FxmK(f!AV?DUE3 zbmrRiEfaVgF6`aY<}w{Rog8JwBhi1CnnS?6)yB|ekOk%_NDc)g(_pD@qKvtu(=B~ z`F*}yNX7(^gP9&btI{l$K)mJYe5E3$rLt;cLWZU_(4Y?=7LIoF+1e@WrX z0q-l(;?|!v9nl|WdfaSxy{E5&b&ok9eM{_U_@OFds8{)bGf|Pf9}N^sL{?@D4FjF zW`;%E&tF}DH$fHWQl%OLZO4XuT&@5DV8TEbaRry-5c&`BiiFZLc*!pLm3$zC)JAcj z1|O^&R*Go9warZ}pr$1IkCIy6VB{7o25WLGV|3;b^rRL*#nmbx4vrT#OgNjj^_K!2 zqgIJb@P?C7v9Hj0Azao}plY}0@g%`BV!IUzBAp64WSR?fqKyd~zi@Mq43gBlax?*M zVLgOo(RD$x0-=U=2T7}5$AAgV()5TVgu;A7z~s%fn~Jm_Q0 zOv=Q>^j|NX0LFm^Ut0VIIsg(q-!~Scw6OEj~%{B$~ z|DJ=%$yU>T|BnKqSW>F~9|~yE=O!t#b-Nxxoqh=_X0Yc`ybl^&$t;H=1y~glD9B4; zHCN(f&zgthd)|MvClL7u(Q-jBV&#yqyt2%iFlg%%aW8qbbTMy`CbMIu!_xOtU14sU4h+U8#5K%3C&LdUD>U zbV^W%))s#DuKT(AS^7rOE8A?-_v_*S8fLeJWrAy|b$lhgmFz3a_xHbp8V%0h5C_VTR+J^G5R<8xb(4{7Ur;+c6WC3>!|;M%u0<)ECaZ# zc;8~v%K|^d&$-`i)c;^7)htx(xf_Sz%FF~I5JYv=MC*bf)RcvCnHt=xMNDjrfgCDU zI>|5IjRY&@PzwwN`fakms(r6M4zfajyX++knhQUDy5`%_pl<_cfR!9G4 z0wYxnDIuE%0fE_MGYf04ikO@awREb^s4{w=T|kYtfnk)|e&1Rrz;XBdC9mCVqtkJy zkq~$UqcC)mg3m$A;iSf`r8loBqI(1H>Y$!vQ{dgk#j=Ndl>DGSm#!csFETWg23^vw zJcB$3__6YRcL{l8%%c%LJE_T4lg4_In&~;UIjHf7;{kFApag_#u-GdFue=Y&7NrL>qe+K*TmpXp#=ajVYEXT|FQS*ytLu9*=Yh`#5sMK<=jz1yoWKbDZ~2SmUkl zGK3MdowY5U$!OF0?mlBQd<6^`^1sIFBdA9z3uC=BNrAHUV?Ic zMvs=D0rT$S0p{0%(`cJOOQmYKv`EX~{o!B3KBD@l|D7)fpWowL3iS^MqdKvv+GtlL z;M&ETnX3V?{~xmxp)FkdKg^Dl)Rf@soj8xZD1>>ZEJhvdVE#7@4QkPta?-KP0*E$L z-79gJ9MVdR)80k;o#eAJM{a+8$%HP6xF*y0L$l8>K0!cky| z!l_Cu<2$R!eB|=OSNY9IuB+INs(KMx9IHWnX!=-T-g!^%X+NTY<)Xos;9^r8M#Nky z6^k!Tzb}C2q2)a4MU1neJUU@Kd!|gzrYV+?mRwYlV>xzlS((47aaK^@;4XcU;e@<2Vzf8O^U5 ztRAv=7Ayb{k^gkhtE%P&N9#tzk7AALAn>aR&y{9Y{_I3`q5I9sorX*^`HQ#~i z@pzJd(UYMme-Je5LydDKs{a~OZH_@9sDRb|RjIkrsq?#GAK>{bgI8)BVIqK&oqF8D>?mmmMHGZQ~PA5f=^lP%0uLlML|3)!; zN=7^xEXT}pxXdxrk%!VHb%9raZ(})fdWi69LQ5%xoX60UtJSQU={fmav{vIoXRBt8 z%;wVt_N#GR{AQD=dX3_^lvwUK_OP1YEu$OlaUFQ}x_Eyx!2vCP>UYf&HL_%!j#K-( zI%R|DW%%Fop+|00eof|T9+dXK26J4l3o_&WqVMI*Oe9k0vY%7dBnCRhrp&Zuzd_#%rNqTSz zpgXrmeAes#;2v5gy5X6QZw>20Y04HH)n%4lSRZ}*gXbvSR7iJjZMqRp&dlY2bZ&2< z&lFcBc*aZgKvc3R4KJ-VGn>%CC*ujX>oB}o6V{r0)v^aj4+3Up3XI7UWs_JV#gUY} zIlfGhzTSn-WxWdrJavp#%*ZNxugj+ukS(qGqx>{?_Engou zKPRSJ{XaYY@H-}L`HATs~X-hDp~F zE!%)y7rXm)W&t-HHz}WWA6?52niuc8Xg%S@5R7g+H)ihojZzSy*RtkB@JM9Mcw&Fy zv%)BT*hn;yJqV~^gv4-WQPqx75%q{t&I~x0yRE`HJwMp@Z{#`pbe$)43!gn|j}0^Z zO(}h|PWjSm0qYAu@(@DAgqFnyK-(%v9d^f2+~%!qVW< z0M}t0S$7^P{Vs@$as6L)_8Y9?(FDHQOB_84 z&}p~otj@TUb0bI5+1sC2oWpqSFN4)s??k4ktLBW|V4(-=SM4lf1TL;d+R=A=D%*?Y zcjbxHDF$7!JJXAe9uBE%K+|dpKfQ0lXDp{M|Hv_6!0VjaFT0HL!)4JJFBHf^R%l3B z>|_u|G%Dt)ND@i1{*`!2A&+hm4lS+RWjAAU6)?bWWBj$?Gxe9yi`DbZ;r~h)#i}Py zPhQo_*VN#C^5@?qO6mU*4fFAI4Nc7P+!Vn+vAPl0)s?@{-_Q(0Q{-?g@v<6?wN@;a&PWw8ZhS z@ptr|949suxeBSV^WjtdQpq45)eG3MMG>IOGRFnqkij&5E^{rGDH_QB;J_NOqa9sk z$Y7w&ma@Z{pS9L!O@+{7dD@%cXf-#NYr=|`OJ52tV~)~%w_$9?ih7`e;h-Fq*9K3T zi%auweQpDROECdyZGU9dXoJt%sz^Iw`%MvmZ*3V540ya|i+5WLtq;bWhxswZW!SHt zE08Ne9S{8hiII399Y@Y>c}&4SG}SK?-ZX`^%glLP6ah0mn!^PQ$fwDN{G(jl_YyQW zR>j(M>6Hjitf?uP_CdWwlWkRLPR%e<+0N2%=l;RhT|{o3bB{Wp>_{L7V8uhurBnA0 zbn>@gbH;o440bIjB+jTr;8$J_b%=^^n=b8|AF8e&MevYVq8b9oROP!8UAjSH&U8&a zAe0}%Z+)e?{Fj^U`xkJe3)?QS%5{3-y^LR|t2Au9niYXk@zr4k=IHgv(CLL&E?k{N z;hJRCy*LDBhbV;m-R>R#%7mA2%r^CMK6~f?Kf2yJsLe3S7cTCE;!bg=6eooO3GPr_ zibJ53qQ#-O6_*0Vf)p$6UaU9-cP;Ktfdb##-TUp`y?5?knapG|lQ-u%&rgoTI<$Ro z$<=8}zW!2|@GiVM#UcqNWfd=WvqfV&T;bzreV&>paLiatqDloUGkJg)OrrYIKuiA28 z=w$~rpk`|R>DUR&WurWh+FDLgY;9dIYJh})?a{HJaG@Pi0TWUZj#P+bw0zPaDoGq>ATp+6FkBi{acxOJFFz#l zvWSU7B`{y9VU@-AK<}63)4HVp^5NdYrf0b`&>kHt(m^KC5cVPB@nKVve%vJZhAgq0 zd05Z0U!UTWj@*72P?d{kW}-B=I4mRAOeWnzWvk1$WpBvW!SXl0n394rO{UPh3VffZ?=kEs>fMYZW z9$HnD3T}RgDBA175_X;`>|oqH_=4W;XM9LHI1HK8}dnwzW70`*4CVX0PolvVu6$huT zh&)6ZlS%V^Pq+ap16EPWQ4~RNavVSNdVgDJ6Gs&$AqeWk*8HwQZK+Z!XSR>ApQFh< zLaSLCu7@w>WKhQI7G!Br!leIl%8qbq-y<6Qu25y_0QJWlHya@aa_O^aXH39T(XX?z z=!*UpQhet|2}?%*V%WbLg{g5|n8*-0#UH}V#x$9Wohe&B9p03lKzaQKpu6(p@|Wtl zs&$_jTlcfL58U7U@NR#UtAA?nAJieKq)FZ9Nfk-sw;1gF;2F#p+r7HZ6aOG0EfZ+b-Y1#irDqv`9#YOy!0;tv?$iEs&|MYBu2g;9u{vq@42+_ z2eP1e6D)O2lVpk}i@qOB-kb+f6Y>-Iea1l3+Yra2{ko2rMVl|WHLskIA%*4HlkTE9HhvRVo^>$ZX7 zOHV#F6)Ae+IH?jQa)La`1WIDt940*C551InxuMLIS}D1@0C#%x=fV5U-!+j4 zi&nZjH3b!w=&RQNcW}BmIW_s?DIpNrXvBjmWiAf+8XFu83_NJpi@hW1oUgI5@D*-1 zV66lUroLBNni3u`ybT7GyF>VA9Xb|&0 znS6+FaGgR}OkctT`3)#2%W@daf0d+;+kLtZq}wmbqrU2yLlfP1k`v#vH*IwJy)-6j zvgp$TYBPklV(M%V|P&WcHMw z>{xl4d zE`J+UXBV%-jesM!8N>x*PgQFh%un1qsB-YHJdD3j3u47rz1A5ui2B~*@?F(ZHCq5x zMKqr~TYKv4%T&+iQdo>7x?nSnnzY9^aA9;QL?47O`Lc+4J zX?-~U%um9*$3kPp=qNns?R?p-O~_n+P`R19z&`Z&dC{gj`UYAdbm(dYKVR)iBO+#B zaX%o_QYr*ynRy$}kyiYX?dyrw>4!4P{qlrO8IU2po;x6r`S6*dsWLsVUbdsIlV6>z z_Jib)8j?vhT&P+%)G!B0s1xeRG^jf_jXsifZJm@yWdag4RO^FE$zUwX#ySKBDK39N z$nL|i<_v>IM`#aHJ|FZw!n5bE+pbp()ynieCk2&~lFac73@}|3_0jqy`B_TeR%T?F zS>ZBTkz3+N>yma`PE=Nt3w;YPtDi5b-mtKor%oAKcO}eJMQ(srV_`cWEuKA|(z6EK z?@C!sDuWfcE$47C)vgFn2~m+$nFC&`45_md7I5%7HsO{rCjAhSQ?nx5EeHwC@DUox zNi&unX*=MQo}4r}@j01?`*O2BHa)_|i2A@P+3u20d9&u&mpT~tIzi{qB8-QGIXrAn zm>QaONZ}6LBg-1iItSh+22%NnXlu;rBr+5U0^YpP@K^i0&hE)3n;Rb7rEK6syc8(w z2%%ljrIwSqKWWGr-ZgsH1Sf0-YPVuN?pLewman3Jp|%6fF6hMSWPZ^K}nM z>UOwbrCi$-i#s=G;Ao4{-(*kttLIP7WAD4QRli2FE8Udp3(>t+WW~fWf*(4v?_J3M zbm&Ra4vuTa46n-<2WXJ%3HGdJG_z$JH0#~8eO$E=I*I7*`j3-A>wWkN?r;3>WN5v8 zOBB@`{0J#jX8@5Hxi7nYMW}#F7n)oO5!__&bV{{3lCd8OMY`_*DJ|ruIn0%gNJl5UlNBrw6cVyK{&Sz% zU}TsuSNzi~VwRT_Vy&*dG=W#~uRPSrn*tvV9dsI~8Fr`7}^4CyI;Lsk|ly=`C z2Csf?nU|{fKb(=^G}N_@YL9#NGGPDOaMF zqf_#>1Y3`z%{p9)2*LljUDZV54{m+#Pu*xpHize|<_|hnCJBWj35W?Gf?ha(#Df&9 z5lsEc8Vp9M71%S@M)l?ead(deGV6gp&$y+x9&KgCX!o@!`ab!r|GZawbMt3{;fh+d zx$^VGHb12fr&%~EsKAn%c?%OuPVuU?>u0eUq+0c@k>pE5yB`ALUTZ&B#>ZVK)c&qc z&(5-5ssRDJ6Ao}SgV;XCOLuSxa09IEpPcN7sP4YW6d%bDy?D4;C2KxF9yiQNtpLd_V)H5{v?{Yg70TAmlyV!{@)isB+b-N)aoktQpBd?g7J7qiHu^f8X)d)&1K|v?o@zFnlA=D3eNC%-gZqWW2a%X{%b*;%1 zm?ksh_~e7N_7pug=tItFN_Y!1c#6_d;Ljl*lFdPQ-PIqs3eqf9 zw&CibPe=P%!Ts@Hj!dPo@#hzKu0q3H1xer-QwnCh=%1;<)@NeGP0}?)aRT2$e+sY$ zsL+F^_wqAT2U(VK&d&0%im<=pic!f}Z9w&Ox}5m5oKuoB2}Ajk5}2oD6Gm7Y2~&)F zY;nR}GY>a>j(%)~;HZGeo}3qNe$b4tUe`6h&Jv%||9wz;eUp6aJyiLMs6|Rco1V!a z!jr8lFf~QT7YVcl+VNytp}Ro*`KvClnkbj+kpg@6O8tve6-v@gfa(UE7FY6YG&PLD znQ~s2;+Ws8pZqX@NVI#+&07r*Di!UTlDVrVIDs~bzJlwKn9L^9Al50V5-~>gbch+N z!$4~g&4CY%k0&kv`N>V!v-zjO&wy!o`*f#+h|ttz(}WFAZ~1hilVFVW5CPWKJj}CS zlSDw*x7{Yhdkp)D_5IC52Wtr}&+uELTwP0I#DbB*h@CSzUs%DZ=AZ^&Oq_GRvKl8| z#$eIgoqa*(kjwMZ3;#>VRXk&utejj(yRfQlU0og4!z72|lxXho@bJpYJ9x(=dlHa< z1K;^ST;aTIRo~Ug6gU1fZ`Ix14RJS?zkPT#bZC;~QdO9dtvwZLujiaIlZ({ZsekV>gv}cI(ooG#ii3k7w!0=TARQJflRHnCq1G zYoXL0Xr@UI^maZ+BT4%Zn(`mRP*j=7u)g_me>&pJfkyDVDq}rQ%84 z48^e$TSeJ;QDU@Ct6V-dj?C};za0|?AfpKCbadtb`=rrdVp@e?&GXnUBdZ4@TAVl( zH7QWjq!Q8u7|ldQhuFytR44h+(XTlOu*A_oBdqg2g4)W2z4-!aPPXmNGs{C;?>WCd zZ54hGxW3Br7s9(~_xREFYf}69p&*)<4!V~Tk;ijgQD>1EQ^H6jwgvLO%t9*KNO#%w zs%b~--F6X>%mMF9GMR0i4eayyrhef_au^zV=0HGH ziPeKmd8rKhathTO&Mok{{?D4N${YULs8Fx>fy*R53_K?w;F-5V52;kJN4fC~I2hYX zC01)DZpX2o3J*q3w4c@-jLUseG?Mk@-$y4!j$793T0@BRxtOOl_-L>c9dfkMbqaQp zWHU?h!le8H~&R)0rMrdUCF_p zs?O>@I(?)5%HX7leh~u&Lc)}^eyD8^lmcr59+`{W)o-=w@>T3neSOuh=GSb;z{OtF zidq&HjSu=5ZZ!U1Ht?zW)SYI}o#s@%(6*k${tuep5AgE4muqk_^sOou)SQUcrSMYr z$jF^6lb=wFa=ceIPyyRWw(`j{M3I}XtEsU-L|AAU+gq|T#F`}3G>0CxNKh{XY6+il?rI3Pk0%SuWKxzUlkt^hJ}#4X&1*#m&Y)HS1s6%Kox zD_?q0Z>>Q8Z>`E04;b*Y(7lzz#dOe`Igxq!+>t?7h8(`z*cDC4KDq0onqs2FAgtbZ z8mH_(!RL`(l}nAbwN$idF?g;)lRCHKo@=(=cF4=qrx|gzffu^WMnxoWS`iN7zV(fD zxQ`jI_?&qwB_KIIh9oOyVz-?&v}MqkF8L0B%J5XNMhYlFmhFC9bcOgjSu0+9Y?=hm6`6>Th+H!qm& z92`iS>n4>MK$+hOn>&k>`=K(KR#?6RQH0EPib}hNQ(9m9`2QY)RE8?p*5oM)l6mtp zn3yWr*ayZyR@ImG^;yZg`e_=496b5wxT2m*hbJo^|6aT-6?^#BiZSc?##6{+cV7L; z1^&3xjUo}bFcSlw>7zc#yLNj{r8n~{!|j!gFR>1^r;dK#?u+3YfG_q%up(!&o(S3c z!FuMCT4^Yi692*n1Uoq+S%SM-jTTX9BX`1N?O_u|Wp99#J+H1=PSYy=b`I;p)=Xs+ zjupig4A;=*%;LoMEaHD9As{7T0sVHisqPoYwhi}C3Jj|cUFJ5MMdiR2hll?PWFCWd zmw%7L`4RL+pf_|OH!fNTl?IRTp{@^q#*6wJGPDB!wV#;BpR#h0@a)6x`ay-62uG$& zPU4GdOgG@0Ds1!gay&fENv)-S5wYv^8+H6|xsClu~#-KICO=&!TM?J7jZHE0jZ zBlS2SnO5!B$(;^wD=Q{e4c8B3iNt>M)+S)&Vd9vtvf;;at7TE77d1){Pciw!U?1H) z{I446YeBcr`wcN-O0nnkG&^t+K{5@Ej2J5E6wSX}xcw?(2XbP>Zx(H^uh*n9c)7^5 zt#VqBeTLQ5_&{an-#C4ZN!|dw(mblD&j>$7KTTj-MK9 zbDt@|%4d9-d#oGqZN)!H1sqd`MEk{9lADzNROFw{>DWfkd$-jOsep9;k?O#h8fWY0}9W31&Aict6bTui=mZk-( zFuMl5<%|!D1P>A3C6F;IP9>!v8j9GEla;)+KP`i1=~q70KkT=rx_u+T{@Mwac;oocUpRIT5~l;7L)K1R zau{a5%4WLdFnkgSoQzDlZm!HjN`H%asOP+8jWqGXC#^$b`EdBVoL8u5T++gr20)@r+3K{ zpR1FnD5>z!Fb^<0p9@ml?u*7Q8C+T)3}#78frmegme-ZXu*QDUp?&|7>X;2rzl33V zdVO@zR1Ph}NR&3%urn5^S4?0|H<~7C4k=xLqn5#T1(p$c&M(`k0dJfBqgVOGlop)j zNEJJ*QXgneyMOX(Jw8XIi_AXaBrTO^AG&zXmf%Qr={0}Tv)6@u&f6@wnP4EIW> zd%qYPlE+s4DV?*;IQ%2S$&S44o+B&QCMbjsv1+_kr?sY#Q`xNiu4ceSR;;#^;TcZ6 zx{`LkCe8k@XSLYPawEx|SsbSZNRH0@s5xR-y&)@E>@aqmR3#Lc2dok!WlVEh@GI@= zMQTUG#MLKUGvk-P`=l|p;$CmtkzyQb&9-Au|UeGCEFhf<6SmExgrtG zVhb&5?j3T^a;YZER}UliAHAN&PC_6^@nL=e+oPGBmnS6vCrvMJIR?W!=8F>m;2v{# zf6q)9fxvrR+Pt0qKKQuV^se`%AHD1SSS3&6THwXf1*f8;ww4y=gSf~=^UaEP$nn8t z;{VWG^dTna|1_6jx$~%r@7Q0v^e_QC^0k`w@$sL+0+x zr`q3a7ey^lG59bCe&`D|un6KV42BoFewH;>X_XQ1WW*op-4uY8g7gkL3AHfkHSF=X zT%7XQUXx9Xp;D;`MatT{<<&R8GCV436GAWLu|xUT&V!88>Uqi$?L@vG{6c#jeK>j8e8(r?PSlDYTkmHw6c)s4g7#h346Yda42R|H`C{!@Ra*gE|?(^7;dOok9tCZAO%TE+2)CLx~f4XM6#X+mEp`YXcT=Z&no zAYh~9g|gBskP5ksN@`-R(W>SbxmWU6`Z~AVrNFK$V!lx*eNM2fMn`Y(q=T8B11ls0 z##YjYn+eaUL?Tsvj`mvbXDKUoy10g(!`FdTEKYXLwDa7c#Erty79MMKx^4c(VR?`xR*L&}arTltIb7nxrRwaKxV z(x|5nWN z!~Hb?$q_?FV`{xSEacIzP#Qorns>`5ehsFb$Z+pZI#-7^W1-zUzsK$ZP%wgzAJS&a z8dfL)ebS+<=t6hs>r1jbCC5-fU{B@KUBwe&bH$|E{{Tk5SiaN$1&oq(1SlT|7F!&e zoY>Rp<7-I-YYqaRzgc6HT;KI54KP^eejz;6t_;&%ea@cv4N3I8>AZK zVFzC%GI+&^4W;SNe#9;u$Q`*^tl{Ma@)ApF_NW~^snCMo$hg87tHbMay0#H1b8E8p zn0TUwK{2Gbl1_TZUr*J=IFjUYk5hKlsa9w|NeN-oGC&q6YwOU;J_In1w0GkLAf1L9 zqFmRHfrMyZ&+WTE%}HEdJY*U+G>AX`6py<2LctXA059Bx6IiKk!cCcph?Q)rT!n@{ z53Z-$$Bc|uiE@FUiMQ;*C5awIxWo!dQm-&0_#9=BaYD`l`CX(9crvrpMmkQu|4p%> z`;*;}E%xUIubZxc=D_dRWytQMTEVQ5*G3-6jOVE@PE#>Z(C!1?%E=A6?7)r1F<&+rlEA?S$Vr%YJzzViKJKCc7wsvaD zueJhSF8AbBLe|*0NCw(S>@IbA4sOS1MHQu8lN!fId{UQKo-omLQiIT+Fbd_tBd(kD zPF7^Pe9GRPz->w0;*Qe59k`T>Hs&3)G*HBsETe34bB~S6)?{ms?dntVEjKR>+1AFd zJSPo3p4tlZ&c&LfGliy{pEdz>g@wb5@0}YeE@?c*XWBny?(GlFd!pZV`9Q2*BvZ%t zCg4!mAwdgWYJ(>Sub4LB)bNQ(4wO7DUYH{G0B4FbCv8Uu@azdAFnvujnhmPVLl7NB zxzd?I&AYi#3kwUu|0~e#z^o4{COM)m+djZ#T0ob1h883R_x9hGAqf5hvds;iI6ZY# zP>4X71oCqgYb{JpGP=J1KTyv-;a$o9Mm=R!<8yR=H$#RWn;~)vu3rMD$eE|4&4Gv&*F7V{L7wBmEZvY#Hq6gx_W+phxoaW zn&&V)EwFDN36BK-4rpMF#RK|;vL>)8YlTr0n^+gRfWQuR1*Rlv=>CQUhK2s9qfcG^ zCO;_ix`!GsuYMubO+16a&XA5)j|^PR<6&w1?1$a~%Nc@q8(t2`L}D@mk(w@fCoP`DD1QqxoV_j!~n*ULs%p2 z=?_CYLD{l!JF8+nLo_N8m-WnoDJF4~@dG9xsIk$4#j2UJL0v`zCU z7)dZ(TrhwZhK!faiDH}_;9<#AqAD-%o5>;|QH%WJOKk=9N{w^AMRG+c?xNO13f~v$ zPQt2%YJzRJlL2-;lpKt#T<(wVW) zT)s&sbA-<>bec~9IFy@k{qQjBgE{I<)lp}{Ao_0;?|h~=ZoeHIQ|`XM#<#zuEx(}k zV;89u+Ag7^vi~eL)y%VPPwpZdn_INVHU{pTm?n4fjo=sX90G{i)<(=dlt>4Gkd~)} z%q4}vHU0>t|5}RvM|AUKEMm|xp%^o==rf@hGodk(w3H&%qyCmhOth4p-Oy$3=SeZ% zicHwcIgZXSDoE~`Q?8jDU?btREI?o>Y$k=j;p1>2zu@pu8>;M5mPFDs z9evQwU%2U!P_-WMnuK{aoXFt}y+6g&p>*`%QK$<6RxhsrzXrk(fHPp#u6X?<^Eb>* z9V$ndIx}aGk@4m^DffyXRe{?FD4Jm@fm6g3rl_SOTUcuqj7d;7O;YKqFn>R zlHVzX%o?q)7qf|B%l<|m36K1w^XIj4j?d9n4)V4lbp}2=z6|ZbYkH7 z6p`E2ST6tz5Ci_K)G#lug6?I`SVJA!gK}INyWNKeqS)1L3ac9D>G|8FU(YU_`SqON zKu0WgL*l{`uu%%8;|*xwwxzGpIQv~TS5NGzVNZs540Y_$ii{-KI9S`S` zAD11BdboLElV56;$L!=ai>DP|5I%rBU|^a2aZ7ZlHRPG1O@I=lZ}am;5f{Ea6JM%F zK{xXp5xoXDM%pSoEN~D`krDrGpmMK>k6b9xyv8>3i@DXUr)#^E(f}!p&)O)t#k9=! zw!K5lW#e~U-@OhfosMpg6YsE8#O$40E`Z35_mt9+(j~J=7}wZ)pyPkl8_{aU6GOvd zbO}K;*$k8mI3?IF$|h=_l`r4O01A~_ukbxbB^L1_WIcd9HRi-5l%CzXIN(_G5Onpa zbIK@5_C2<%Zm<_ekhnGyj%8lJaf!N`N@WY!wD>Z8NZeS2MfeybKK^PHDsFu`uaZ-# zJN2;`_meIf5mFNpamK*`+s$!+n1E$5zWeNF$XNQ{%&n|z&5wN?J(5Fz&otl&3hbG6 zZxllU+$+~|RJ8b_g5gCJ`m0wLk-yuf1IG3x{_4*|g-hKgC~kwfVkdU7&|1m+J#huU z7Le^0QPUI)#4&M2@**fVLCicIU)uJ5CCto_Ejjef%@rYpd)Z6wk6YCKGs3NweQ%Z_ z{zGG}fZigg{r0WPxC|NAa%)W?Q1aFRd=BFNV1ED%IwNX6V~;0K<5VR0H)$V-jEDH{ zT%sFT!y1P0%}gQm+!aVh;Hj2L({d;hVEL~9gXQ~*yZ4h?vvBCs-(&Y<_QA<*idwL& zj2*K8vyG_W5Qo0OpoV^G*uE&a&7dP;9vVi*v}Q=BHP-M{owY>+n2c2A48;MeRT&;5V`=U z&=*kX^&bs-fKASX!&%29t2Bt!o{Q6fdf|;?pk7i9c?si@ztS3wL8Z*Q_Lr{@x}eeY z?s@K}&^39(o+?gD|KEQKTb|zQ_FCK!lTKdX?%&>i>5?p_fGEWqB&ZWe<8Q;6lBIgh z*v6pG2@52xAU15tvWYnCM?MBbFR^*Z5c?rXUPuM$7?E0|1zIk)BVw}r+0#6h;UQef zqYx$zGk|u8Dh$aRKwh0favNPE4dq>I1-`1*q%nOhv$8VK(blsc=I8wa4oeuG9^h$y z7(8qc_MNnQ96t@%IiiWBs}nn(Hx!z>eE`xwHH2V*`D)9LPC%CCY0&-@IbNuIW-A+)5AZ-BCgd!9cJ;@y$w!-i{~h(hTU~ut71asz&c-C ze+17F`!v6sbi5fH^H!8tZSmU7uIpHR*cBgQ&e+U_C<4x&n}KMVN?wF0u3%V@n1CV3 zvuH*>z6saLQUG06BdOH5KI#6BW@yF3;vj;k@9C=I75ro5TMZzQ$0Irl05tzwvvm5O zo$5b3R{8H>Y`KDgamgVLEBeVgrQ#l^If!V9Qn2INXN0j@gx~Fu3HGvq#!em({gGe) zyG|1*zl&p%#HR697qXiDz1b*+(TWo=qlP!B>b!J%C( zal$FEdM$T$ILYnsOWS^4&S?&<&Uhcb4!!^OS8iX<4D4Xh|Bmd7MPFzo4aOYH;5fOn z(68d%0WpE+kx>z>LMbfwvM4im&ls|N-Z>y<;&3&c(jAt>_lrP_ah;Rz$lP-O7y;hj zBpVNa(Q)zM2YEAYn4v)Rm=A`dsv!z;g`@MX2p~M>(!+thqi_{^R7p7N)I4>qa95bA zKZf4NE(CwmDJgKPWiv-Msm`Rk}l;%V4XJKEl30 z1+A&z)vTSI1WNnDC)rB zqNfEM+U^v)$_?9>XlQ_Fr%X>g9hC|1%);)0ssMibPow>)ZUjmHh(TyVu*yo-#QO0B z(Y>FCkI7NXH@Zeo1$54|G`T^VjX$L9%i~S3eT+wx}6*^eB{+B%PG;`xMup zI-`7iG=*&a+Pk}+N#|W3S4~cDo^Ce6EpEcy68&>t?c34N*oOeo8Z0DD9sS&9vS*_# zNrg2QJA{BJWMkthwMEo3JFbo1v<$RB!0jd|GhQQgk4g(Z=5`a7%cSbMZjP%H3iN>C z(rWoW-KNrzAr zl%jbH(M!l2z_LjXaeEtyh}7(fd&!et`dQdJS`bs?sJ)5NcOrZ##?5g`D~logYl%03 ztRAfuEYjgyQ}~n||0pamf8jh-12WRh9e!K5^?X81v4B^g8*psXo#uVS6@ z_Y?)zIoE`t{5k6Mr0U=aNtkn;U7liksp6RK*pRMCdSRBWmT_uDng-;ZbAjKZZWRA< zv!>Tm-Tp`83g18AE0o#(WBrt?tAex!F4Y#Hk1K#PgM9TvY(zQllLKvXniA)gK({)z zLXANvVwafGLdeXhXnKUX@mAog6#qOb^SC;nAUiWVn#V^+AMP|Xyu!#drHk#^H)nNi z{+z7D*jwE9yY}nj} zFN;v&U29`_m3lG()=SyhjZPr;rMbO9Kvs!#i#8L9Ur8563hCi47syrj&)L98sx3>= zu1D`4YdUFU6(bm-`TVf_+zl{kVgcPKZ*55tP* ztm6UJWiYkFzDx=NZ5;53uQW~6oHYHur*fZJ@zs7uS(sWjySM!|D_FWu5;vaMJ;9L+!r;qyMV#gedI3|KGP*tNVWCm)uc{?1PJm9@zIF!V(k zd2WMa?!t|!F2vwB>zr;U3oOiCdW(-6Vk&S(-1A*wy_r>wCYrvG!!jq%kLBE%Mgge_ z>)vLH0wZsZf-*p%ERs{GmVz`xa$)eb8qq_Bsba4OV0?MM?Qf>H{D8YC zjDA!RZ1k}O)Oxo4y?U|2>_WiS)4&!9O0Ixcs=*ta!BW8ChOVGMuBza~$NjA=#HlVV ziRYVj0T;6{L5UdTZIK2E8y`-7jS_K9N<7(Ws_@Nd!QSd!q~$ct<;5z`yjIoUs~rc= zn}XMV?bk6TtsVz0W2=9DP0k*9ZKSJw=X`gk+P-(t!zkSE&|<&N*WC&+S4B$89E$@- zPTR4Q6wmA?a~OY5_*Q7WQ?%zv$rt^C88?rGf>~++OHomsFk$$6zjC^~P3^^$KH2ga z$=y}GoW#A{gXtK!!85w$ee!R*RneKN)Ix7&)f~LzkeE?Z6Fffea{SBkGq>44T!N3s z(*ar2k6F|2_$~9WFkl(M=jEEXl0@>vVA-?3h)J9MU(p4-TM6|!!G?OqTYmg3362z> zKNAwE4O7tj_wW)|jYdi$N90~9ezc*mOd=)PfSal{Qb97D8ooE+Bq$CuS49zxJ$%Wg z_7XeW9dq{IVE&te?IR>@PtS4H=gEow<9CqMD|8t{({(cJE1ehn*f&zgrMVUXU37jI$L;{>4kd^{wEy z^LcdShR;=rQzVx+H{QFHh}p{P%*S-~_~MJDW!C_wwL&2^HHA$fHVG-B1M=hd@d652HRdJwW}= z3ge3l8`PH_sN#sxq14xom$J<#C8(xoFs^u*Uety@RCw9Hyhk_X-~vugxRMZ1FY`fx z76gv4K3}ZbN_77W(Eq$3?pN;o%7ZfeP_<8~G5k7mD8F(%oMFg8xvon+MjFFVGf_&0 z`UMzuXnCy2+UX?kT<0`_n_wUr&FgyG!#Ti*{${FkWcuC%lY3lQEi0H|m2J2NI9c#y4C1V)0?-M6e|caBW)bz!pYCU^KZe3yGgdK%8vG+0mMrV-CFtg1FM*62FxY z(C%WN&La2Xw`8|`+Bj-H&uosP7HViHvX>=Kd+76F)${#*-&%W`xZTFS%+XOcDc<+wL z;dk7sKN*5(d>$TAT23B3Zra*fj^{7VY5eWlZ^;fm&gr$eUO}5~j51x&_+RSQA#&m! z^ApUpP#KeLca-XCrZ0WC6N_1@1r3;Go->uPz(P_|y11pk17_o{viLWJerqa0mv(=X z&XZLx_ZxeWs*?gKmja8in^<@8WFgXeUQPC@|(yye$w&B7`kC!z&? zLt~Th>P9O&A?!R$J-QVNt-6ZB|kxE{-G#+N__D%Nav8iq$N&{=8{pJ#ka}0vCuz6Xe zE_(HuP)?2|&k>~Ol&6Muxf-1stQB;E=VDCAh&27x<|E3TpZX3y99`8-3v*d-?+N0# zbbbg=y`CwU~1a9YHwFhcpdt_>Ab5k9uG4RR?AJQUiuuJEN+MwyrMD z182Gc&>@r8)@Hmb6m>@^4vfu!n6TSGKYaKAalcarT4mnqkHapD3$qBDLy%!lj}MTW zN3LQxd_53{hoE`Y<{hfkXYVM)GKrfXApYihM2NYjEAFBf>Hz%+%sIjBfB)6)$uM7z z*eY@oVs+f!)gP1_Yr&>A_mZf&E#%p^;p7nj6i(43CzpZ0i&rXqK9M%do!H)fgmK(m z;YG9PgThV!`j^WH7VRCHd~I4-YNAhO;|p9FM52Gn*m!SRbEh=eAtSr_TV8}`o|J&R z007IXX8Q;eq20X54LsXRpH=dRzK1gyP}TuPVBPZ`Xm|Y;!MDJ|pH)0PW?>8Fa>5&( znNqqgKc}gcJs^ph0FsNLT_)~6)^O*feTcZWT|dhH`xmjoEF;*AsItrPR+bEK+Mq|T%u1&3vubDqexIAR1pX@HX ztp?Do0tds}{$y##YvZx|*Wy=VJ)KvA&yvTxhN$hV`DC5NX0s+P6y9TT7mUYILdxL8 zlm_2fs^<^`evWHllt&P8D*R*ekqR_R9IOjMO?O?g@ln|w8g4pQ1g)7iaVJjm;ec7UZGMbGZhaEANN?=uiVf9KRK@wsuXQwc!aeMQ7=4$Lb6IJ*xhA=Y!R)5DMm+#U836>_Sl%|A!=T`=A!j(f~MtFv`lQ-X& z8@^N5y-!+RkpP1^{#`A0kAH6=Pm$ksbd8*3fU4`}k=%CallJ@LUNRG|%iaWMD=RCk zT^A#7e{AFuGc;{3;k{!TRY zLI2gns$`b`54BAa%lG!5+7_v}`)x95W*2toe)6!f{jHp9fDs4RE+vUCU(216c2FH# zysUqA;9|~IQ|Mwa;j%0mv>>s(=W={%Xb2IqH?W7)kNnCHpB)HUGya`lv?v0iv5!z? z${HYoBnoe!yaFoid&C3)_M`tkFXm2THZ=7;O0ECG0Rl>$``XM8MxG`smwA(9WOsMSI{Z+8|O$sx<$thgG zk^|IV*@>3L^Ccz0U1}|xLkHC-(t2eP-t)Ds@G@s>+GJ5${q@Hxqg#FW@ z-l30MvC9o#-*M@WkMBM{O}9OCJnEb*-aOUcru@>50@mI6cpkTkO&54wiK*7aF}kp6 zs*W?sR58IPizq@2iYErAh}nwMhiBsSnKJWsm_lSJ@-kEucv_!WHrkeBU$-1AiH}hq z7$40W9vzjD3w`xlY1)_8P3zNKKPbDt(!#i#LG_+R_0qYBZ)!iD_Q-&ld%*ZWg7ma@ z8@@SM)?Q%YT@fL)opq2LRcX+2GSaEg@D{DWS0XOo2=ygXVbr`FUjC3VhFz?9wiSv_ zMcDHY5fV!N=eWj#69N48LyofLW4K`+_ze>HGfsLu+o`R^MBku^o<*!@9E@1Bk)bJF z!bFlE$@@+N^BnDXvJ?i%gmMERovl`E`3=Y)0xFL8I9|_a?A`x=bbVD@Ug5H22oPL@ zYjD@#4j=ALaCdjN;O-DC@FBt7A-G#`x8UyXv$D^#XKv>Ff_1aHyWZ-m>Splk0{cd% zwz=x~mMQ{LGG907YrL2X3H2CAlQPJ3nwvvra1Udz6bsv`)64gARQouN$TcPg7y`Bb z*`345wXA#Y6M|Rhk49Zj7Fk+!82^=wuFqw^uCA^K=W{eLG*tBY`T5h+(?vM1Y|0uN z)ei=8kbvE!zSm))2q!NEEiJ86+nNaHw^`4ErgblwgR(covg&HZgJ1F*{5O*zk@s6= zQt)$z(|^64P|1^No&OM8ac~E>oMlW*%-Q3?Yg?z^_Lj&mE(Mv-+<=t5u2$t4WLfw# zb>DH?hcQTfOe>MI>Fu$Kc>TF{qNdm07D+2p#7!|qe*~1zXz_8k&X(%SR5z9Y4JS7O zHoCHQ`-T>F^}I16&tcc)&6=p5-R#r{&*IP5lTYF8j5P!~#3Kto$_&Re1p~vhtRXr2 zK;1Ir9u`AO1P&Y&&md16Jf;K4knWcFNTLt}48EHcPN&0#F`8S%^a;g~f?CNWCDP6t+S@UEM!k#aps|UZwx|3U{bSGF zM*p+lr*+w!zIP*PVS1Wg54-(goY=SaBZ9TV7ArO`cEhf=9IcIeEqcT5woEQ()GjB~ z_2<+)wa!zUYZ^qJ`vZYF8<$H8=j+!;3&5q2(vh}^O!z%4YO|x=G`xl-@r+3p0BFMxLBO&U(0SM61;QNIl139RoAvQz;R1>dXehZ&E+ zUaEA^)WE~0UvfD9L1iCzJ*+6@o@-u0Hbk)KCA|_8RBoBiOC3PSA!xI2_9nzWP%Bgu z9DWP5%?$SzA&Lz1cw#V>N&;zsEx}1KdGR(Ga)9~C?DMaBf#kj@oidyV^}jfJ4qP4) zn|FhbR(8racDSFa9(ENk>2`Yw3kP|vUtlucujH3F?GzoaqPSc$0@W!!EcQ%@q|GN) zGh)Z*>k>ustsDdQ?lPJD_bY4x#d#2Z;-q^YP0{XX8p7-646Wf^C$KmD(Z<|3=LY@F zEl@KK-O#(cyZf?kO~6AY1P>qIeC!8_3)4g?7X3E|0Yr49PeFjP3axdCW(uI{HaxbG z!1(k>eABJ2$H5&(|9J1G&ZH$90S(`KktUAa|H{$fsKHGBN_nDkN^ri;8C!TOD=W8? zoaZ{NlKRR*O3OB}sfU`601PHiOC6;>Ct?`vu|2#ku{oSQ34tf( z*xcOHxE$}wPecxH9wDDa%o7;4EEJgXR9z>qC5olWhQs#VEHqSST)0}4Zt-&@oq)_N zTvQGPJek`XgGHpp*MaQeYtSW@pAEG=a)|cHxDT7%VjxUto>{B5()}_(to0BQjHAJ9 zfkBz=ka9W1R&XfoyD$5d%f#zm>4t_xES=#=c#?2s3DsJ-G9#MG;vfw(EGbiABI#e& zLj)Phc$T&C@)TZG;fQs~i@OIkhu;?7A?-H8uPd#Qsd`0%B<+N6v2Ai_ld{oJ{md9> z3PUU;A;IJ<;hxl2eqOwfEcq$9lcG~jJ1Dx2ctowVo1YkVTgZtpy;xEDUuNwiCQzV5 zwmmPym9x_Di4Ax<1U>pOxyZ2P5c)&(%c9M5Bhmtjo_@FAcr0Xe72*4LdiUFE>`qQ2 zXu7r%T`A(q>^L|CTkweI0efrOY;vrlMFL1F1~ytII`XqN=ST0($B2!6Lj2F32T-rK zRsOow27!};;F39D=}G01DfZfSbbQ>llPDA1VWM2g3tUslf^)#~JgAQ!-Q5B_qk2DW zF|o1l@9z=To|BIgB#6*jXG+kUEnfXPCy}sRJ0yV@Oh6aEIl#h+ydNr4fZHl&>yLCZyHh!(-_+ z+Tdnl(6+Js$+NhEWfR5r$;khJW*j^f!G~cFm#>-UBhE;Kf>%8sxEnmV2E`XrBn{{Z z@iCCl*8&zTbe)tL1pyO6ah*U!*mqOi**R2vs3H-=-yqvu=2b)rT5dAV^soUE%kN=5 zMLIZ{^5MO5L{tEaM_d?1_h~S+k&rM8KMMMR9zfohBqrH!o#qLmy|7hTNYb< z6TJ70?(Y)2Pbtm_DPnEK=53+pPeU5B`(t(cD>blN^t5f(G;fw-UliOJo_5Bq+uS#< z_T`!yQuW`u)UJM?$Sj@Ii)3uo2TL<*l%-%4Qdl&Ess~KhN-Gx?a6wDSaLfNJOWSo$ z&vM$U{k*M+Wh@SfEP;_VN*->H#( z7n3TA|7^8!xIL4AGpYJUOZMTSZ&pbqQK_Eb1ebU+BEUFr%TIy862Ch6`Y(F7gv}$= zvnmw*&^LG$TMgun}xq8l;QLwSdrPw&;JV~x|{qhx`X_T}O+i6+WA^$Hd zck=}oL+qFwOg}mj!7;Lw;^TB30&nurE=p}O_{?|$jBlrFpzjc4umdSLt< zC*O;17?CFKkpI}1y7T^E1ELp<$>XZdpV`?AH#v-I=&fxuY5&y+{P)H3AAP{=+~VSo zQNuF&_wL%lmJm-UkhG>&C9v@z8H#JCZiKfpPH%i7np2PMz5A5lD z-IO3})P;E7%dOzvvgkG&2(P0+JA0x4pk&q8O&mGD)D5cdix!jf0iFls<_Az^CGB>z zbFlC&!q#LFnt2Xc)OW!=+XfgbfWWI&NkX?z55KkoL306gTBz54tKu=<8KXkIUAV(tvi;2g&38 z*D5Z-rIz+Xa25TZc{(N*D^@3|ba^`C2IbIXW>pS3Q#8>~^~}nt0?YdAhQ5!9Lz4;9 zM$($}6U`z5&ns!8?&Vr{ar0G#i=Nx&ylH;5a~=*+>rc`huPJ$f3?UMYSSU8S!KHn7 zS4R7Hg_Wl=ge$3BO$?5<`diC2eJ#c{#u5?6`=rm6VwJj4p?bP}^>vF4`CW}<=NEUs z72K13UXIw?tJa_5PB&Jy98cGxFhZkcT)7c$t!VNS(C~2ON+hLAz&m)-zY2HB>59HV zOAX`V=|w_|N9cJ=Ro;C+U+eX~7@Z4yzsx($pBv;Y`{--k^PGct>PtEa&|huKFop+5 zLJBce_$Y|bYJgABYix29_oNI6 zU@5x?5-O(?kp0Xo#8v|$Wb>)+_|8sg#eziTsCygvP@BG|8bMlq*XTjZ={*gX7mhoi(?q9X8s8lj%3(wW=$Za29X^CYGq;pL;Jv5By zY4)`I%iLc8?N;3MoiljLHCU7cv8_42 ztzM6{-?u(WPtaOQk4(Wc#)`5aia`_8B1N;tsbM&vXPd#F$ zEo+{s!+L0-xrAc6ut5{V_^o`ZYB=mm;D~!|c=gU$ zDF59WkWr>F&S^#A6_YhHqA4Psw1kl&JcSFDAa60leiULg&KKjw2r9s2PV85mSMJa9 zzWTaCr27=t*oJcU_LQJeg}bO^vhu5`bBw$3NJl&7PX+W{MM!K#e{7lz0TYvxeVdKN z;I6@;k7tedit@diqRc8UR-4;*t)0~t3x|a~*PKrA0NQ-8r$ zmao16ALiiLQSyN?e`6*S<9I2nK|hy5wyQ;~mA>#}5;?CDL^k%F=1mer5~PK5XD|bD zPz)Qs8guAxpXf0cA@GE@c{l4G;{UJ`R7UN4djS&60IM(=QHD#NzO*4(9#0XA9+pU? z0=my#Y($n(!#1SY*n^K5zjY}cdjrZ1ZU^Ia0)FnB?pD~ElX;N3vIBw|&C$Lf|B z5A&F|Yi?`v-qf@+Ju4l_5%c|HHXf~BRCsYZF+{egneQW^0;ZCZ6o2h)a|>IdZI(#S zLapI*$~|A|AiX6djE(f{YTsV=1rc6H)1CMq{sxIOY0mopLJ@fQn>FDufQN^N-nwKi z^sih#rV+@^h`8U_$bc13HahhW){Fk}Z6TAk@+piR4@ODDP=GX( zzw&CQQ!ht-ic8gkTiZ^S`7h^O9{lzx{QvT2sqQ#ozX70g^obq2 zzO7L&F117K?~21{Ck~luo50S|$mLq(I3#+rDHZJkS^SE88W1yDN3TI^vV+*4EEj2FbMC{9`}OoYt9aXt`dz5wH-Fq_NH0d zmJ90h?PB}uHV(Fb2v?Pvn=w_Vrfj2P?e(2*QpCFOwaj+N`$JEFxNs2rR=wNncGy0yK4MIO8DCw(H zH>mo?rLr%isf#r&2)n0O>X9Z72n*@bIHBJ)kGMLR4~su8uq@zWb@z$1sDewbYO`6i zdHQ`EvCV&s^49KVdwcuX$EVD}q#(f@963*G0k~O~vM6D?6>;;2HzgwyQZWyb)6%LJ z^mXgs`l(JE-=?O1Oy2jD6apHWm#p&CRIL91w3*4t$w$<5gnwRIm%51n0G+kZuCFt? zNI)yv*;P))C{)nW(S@8j!n*=92Y_}G2Siak_pb79DmDK~V!6S3fjdWm)Fn=Z@xM~# zYV;@kFMtT2h8Aouuc9T~dETI_-Ql&CI4a}f$-`bUVkY%i0@&ckOy^qa`EpWrR4lWs z_D&Zp{qHgk55Ra zL^LY;HA(4Bq2e*)g?y;Dw5g`qTSny!BPGIf<`5;k9ez{fGiI~VCWZ*{u$a|Ead)=; zfa#dmQuv4RM@Uq@BaC)0$kvbz69elfRwSwOBLm=zZs`aJyHdqK!GwMe;1>o=Kd_hna6TZfmybsi`)8Zr~- znR28h9dES<*>(!aZXU^YnsCL4tC8YNs+D1KVAj`Mvg!W)G7 zwS9_7#6?vUa8yZ`<3V z3{nO)pU2VV3??rb2)UB5M5MUd>U`CH^Qco8Q1iiw6g#}5?O;NCe?RU1IU>{qhc;Zx zm3G3oEfM}S=7C5}zz0t>a9`84v;L*0N=hwI|D(1F8jjo;PKc~qRv2Yyd}R+4ryZ|Y zLW`cvVTAll%^xc9J5E9m|FxrY2GiZN#M%(?*n)a#>E7$f6E2jwA#^^r!J(l@q>RzX zgX1cirD=8ln__F569R>?=QxwPB|v1R4EE<%p$jz(wyP_Ck7=}9qrGWx=5PV__7bFM z;LKNlAW)$wTUtgY#IE#%7w~Fn*Q=UrS4aVlug2hhHG^H-#sDhjxNbt=GqAV$P;4|4{9^+_xv0csyUfig3eWSlpDl5tJFi|lO8}ET=630gEhV_3{ew%B$ze{3zKN*c}&~KD}UI~uX{kXu`u@Bw- z*KYH#-PB)%o^IWjgteZ8wY<%>tpwzyAFT!j{Pqq_HqAj_z+I1^jNc?NK0=wt^Lsq( ztVNU?6`Gq;Z>a1&7bX9&pVr*5`NRgbNAd3|CLBYFoze`m}3jh zjO;n2#_^C2>fV{5hsfyPHXHH05z)&C=iVQL9@Q^zyB_Tn;Fzp471h{ePkwg9r~7DN zU}0Kq@0{0LVd*4dkuzeij5=F8X09s7@%vSsNQ>i;hDa-tDPN*VG%k@(Rcb(NxCR^{ z-(nC}n%hDz>e3CJZSP8cEc->t$Ji*Uk{s4+T0Vs3^Q)8ZQ;A;NhluZbZQ^q#Ll_Rz z)h+Z+#iY^1KcpNRJC}LWPSUjbQiz~EmH$GSQ8?NfMU_PJ$+Kqgq+7~GlTsMcwJ3ay z`_9p#;xVg$h;oZ7BjG};aG%;`UNN}7q34uF0G5QT~T zfVcwV)Cqm8i0|gqsoP7zo-;NkQ}svvLw~whvz_-&cK`(C2OY6e-RhdKrWbd({|Q^P zX&g7pZ(ZEbkRtNx9^Sqj+I1>D_UdSG*z9p%_Y6B)R!Ec2=IJ+KEvdX@gMWA3y<4#_ zY8{i(-I2bUlu>_>IH0&{kaR69(pAOw@JiVMODWNnZa_#;q|72>&JoLh)!N>!x!>^J z-@~ueN(h4kJW!ZpD6eir84R0_4?3YZzxFQ=sEyxvG>^`fy$N$pkr(wY(D^V-1W%Dn z@)!KAg}BMg*?+%kxvy4t9jRdB2VjXZhQhXy+*SH!Fi1plS>m7gW@xbgDrtOX zp}o=8I()+?;%Rt2X{KI6OEbwePf|4Uvw%Dd2ZDYqzU+l@3`pqD`#!t`xl6Om`=E(_ z&evh2!`~I6?h7^K2`+F*pg{eN@jZ=15pQ4|W5;A@5!Y$Hhi>vwVYUBRW~M>n{&nYJ zee}5jdY&JB!S1rlZuqYq%^`#VP#E9A{G_XpZT%p^S~R)M_*REpet#jAzv-+NsdO;R z^7S>)kEiOI7d27ZdLAsR`C#6yjy8oJ=t%l0+fWIOoH%pWVfywbdb&3*R< zUw<(O8&jMGl#p6{`e|ca29Ij;)uowlv553#{1IGQhrcf`P>>-TP&!0z`QSidXQXwo zG{GczEwbb0j0?vie3z8?;1^b(D(AtC2W}%B_k<`g9+Ahv6F(_Wd>Z zIoU)Yc?TRQKw;MjccHitINt65+u&)Y4&_bugXte(=jlgS{E#K;dMq7yb#=uU3vS=Qc;o1sh&}qK z(Du{!Zb|5ikZVm+tHo`Y;7j0<_si<>*1?!y#naU&(?XTL!fgnTLSF0Q3}Yz*Upr7Y z1;vpvCFo~m*x$F?sGdE!PtFyE|rP@|YeK%kgPPnD;j zbHp_iD>fIrEv=MGtbAasTTx&t%9plR&-``D{U zrFB!^4C9@Pq>eZF-I|d`*eVeBH`B6QqIo!5>0p6CrTh0_%-va9lRMQnlRdBy$S;>>T)?ow@%)bi=Y$1Ni`iPE6R;AEH2 zj`mYwzT&5z@xQ`jIwbV{PEJa#$#vfnT{Q*V?^0pl$SUpiHh6asPtD(7P2_XVVWHGQTM}D1Y~({cJcf9NV09^R-(D6eel&lwxy; z8*i17MPQ|xwT6tnad6B);Ys~=@Xh}6n?0OGptV3R2u@N9K)szGr6wX{e@cb^Tw_Wb zvrl|T6iS72kjkAP{h0VuD!|KCOi$PGW8HUS>NuiG#QT)(Dghq{vIv!;o}!)gj&&yN z3q{Of(ArvD$Xm^G(gwfZ^BjTOvMEkjXwMQ(Pq!q|Na6JtKv12|C(mKm1wso&y-+;M z;5+H|wiCD*)+b9~o+A$aD(Tnhe>h#J>TfGwRa;jl_T=DmClO+TWgzRW*1@qSUb|P#9=w z^I9vvYUTL9JyIkhQi!>>|L?P;NFhhS?PL<}V z3u`tE#a(8iLvNjck^X*?iXWS)_0qUTHdyxh%8up@8q&gUoRcF}N{sx8B>veX=|e^Y zMD_#gRm#sM@(&e)ZKjNGS#=2(vwGOhvoXB*d5z1}4LmkLE?ON)tYEhm`(|m(H}|cd z4s)+FYs`V~+S;zOT9|cnA0V0GFD%CNFl+1v3G2?nS4sgxdUw7n5X)ZEJHRDOl;8Cs z0vJ~x627qUV4}seWJ;PzlS>KzaGCDI()gSJX&cd+A@%tK#ygc9i8t9G@a0D)lA|SR za!*K_qp$!QbjVj|RI;?uokDtAlks&gY&Rj_sNK_?91qKJ$A-RAtd_a0KsUGqxD+q9 zeJ{7s)i%@0I~@&)N||hH>ol!9NgF~ZiGAt)b@sbTwZ=b~NsRSY2<6$2jg@@X8b3{| z$~$H34r!}hD_txWn%S&%j@0aK;ln5=I+`vX*43!=Ti&i0h<#qu@z3=$8T8yV_1q$p zvuXbp7-a48QK!HZk}2Z#$YFnxWl0_1pDd6xBT7s}VM;X7bsJP$BJR-Qvsvb_S>!QW zL}wMm;t<5+PzxRxolPLmOq%>Ew8taYmD^h(9GLmzA^pcQT=g%_T~6BeBot`4t%!3f zpJGy%;_rRGz@S_e#unm9!X^~HU?#X!X=Zh@1JWp_S4tnjFQh#4kVasLpplKT3jp2$ z3g$Y;a(9)<0>pGR;Q&QUa+KR+OkrbOKX?e^^o!q=q6Xv-D`|nqa{|HTsh@IszGxAmuLFt0pMF2YQT+XK zOHttamkm=7bXYCVKH0|pA~x9eJX)zQHzA+iABIDn6$0kQ8SD9gVj3V=^OyON04pa6 z0JED4sJVzF(h~sxP=%44vg%%}AV?IY{3^^9bn~zsr}CQ3ecHO4s$I5GOf71iT4uUFj7^Ij|X_-bX2G`H z9RyRt=%)PMiAPL4*g1aQcHY6c;gfV^{5a7{?P+0XxGBb5;*;_3U6*Y_O;ai(aGKZJ zHB&~8A{VKp^olG=TT)QEY{qK=|> z$T-BKiwzlB))zYIi$1f}FJlK5(E(Uh zu4ocSXVgnE^J>`c3)`_gRw9kS2?V>1yD_cr+`H9T)&XWg8jq{ecdciI8X2WHlm{N%DN=tY*9sqWIgQrWeH9 zm1ZvCk^=yyNnIiJ1;irYA8>{uS?ld`d)Cxa>cB^yCZIXM1xip!>J~@9y->%37s3dPedyy6Sj z*MB*VMF@|w5J1+(G=-!%iJ)`g7dd2x4w2f2-B4cLfoxD{5sWu2 z4}6GAVEl_KnIhG{sX0o5ii{dW|REh4>H{Z?KWS!21r1-(f8+o1R@2A`M_1t zuzp+k5T9-#y^7(}M*;oyCMZR;NE5Rwjk0h-mCuY3j~#QHPQbVXTU1o0oxc!5H@#E) zkoVixG0okfgZ&jgXUJIYutvv)d%U1p?K04#&^fk=mY+2f1fp?k1#;3PW#;z37@_FJBv($*vNQy zSq1ub@-?Y@`cOZim|e8c3s7ZF45K;JK`UYrZy?VE^Zg2+7@8Pgs#brP-R{@-dpmsa z$U0K!#wADWEq?3Ca{IUIRZZhAIcKa|Xfy2nRBE4h4F;qWNk>NhOcb66(507`mwS6= zrDZaKz^%pBwzd++_xc0$M||`b_`91MMctEvw>tSOF0dl?7g!)v-Y+psnF22+m4M`_ z;$M;oHFfUOQE#v>b}h%zeLAsjZPmezsq}vA3d>2F3dNS!Zr&5xcUZ zkK07}H3o!n#PDiw{h9w!m-06P$q-21*8J=YA#sUmF1M=7$ub$h1b`yJO-my8rmSqL z81AF3#^vG$!17`{tBzeaE2c0PsxUA5WS#%o@W5(DVN(+O&0YLdO-UQ`Al$kNPP-J6 z$_+|EU97^@(c5@YIjRaW(yAZnC$9U#KsI1L*Ik`Ltz+-AMf!ZVH*=Q?r+sayeJyx8 z{vbUzkI}#`P9TS-;XFHo?znxdLbK4fI8$QQ`QWs=CYd4HiYCE-&>$223`O@RG`^jP zvqQx0B?AJzA!i&E(a~-9TRI>{-a{+@?pJsP&7K9qT@w_8?RF3oufTcvYQ1_Wqmbq` zf|w0jGExl=PFRdnodE|Sgs!Y5j>vYt3yDY?sS7t1Kd1BCTG>}UKdaXSU(TN%hmlFy z&>Rh86{#q0L!mt6kc2%D?Ebl+56e4^GP@fMpUAkPC)msA$s#~6~nVxI$dT5{%u%FvXje=4lz?kCE43kp>%BIpEbYl~7sC8c<3?2PP~9)Os?5FXkV6{0!Z(+fdV*EYJ@|zKs{5% zDtO${kD;tD211jV8|K76kl=r%eK-8o4-rIa(i8Waz&)7xjqGjmqC3nn?ug@2u24_K z@}e5PDGo;xUJttJN9E#%Hu_}Br&|i)JWr-K^n9SqRSCfZ_4s3iPt%$AGKVVE|e+xhXgd(+1XV$B=$krPqMw_DDAnfGjg0tx(4vcIXQqfUfT6^ zqW$RTsF?B8Gg0Wv!SQi^>$;S0+XW}~oA(|P=PI*nJ0k|sQ<31S<$Sp&#`@w6xIYT} z{c*zoS8M%Od7?P;+oL&EeFAt<*UkSAY#+m%%Tf5MwO+Fsnz3K0lc`U=E%SImjxzr7 zjTS|0@z5xlWOt_E#f8h8DD1Jf&F;_ZGw;gF-K*aU!up$6*VRb_)BzM9+nEKPdwm~a zMYHgNCiY)2i~DJx&{}g55F-@JQ?`B9Iuj)K88U`NLw8I~@XRu{Eu**biFwI!L6g{> zEm|AjDD?@}&f0f&8MT4Eul$xpW-Yi5`klq2cG=&@yifTvMYUyhCyz-*)FS>CI@v-ClSdJN!TZhgiq-Lla=8E>hvU8QW3uk?IqXB9h(#9fQn; zr|pIFaueZ*q|ba-RTNI@(s#flGRCv>She+`r)^Z?`huPw?o+RujK6*x7y0yA__#8(~cs`cI|L>H`)hoD^JJI5EFB0v{6= zSF6dqd`+Q>%?6$OsrKQCj`jJs!!xa~FC)7}Jl6s$`@WA?uKK{X<=lPUYoE6D2Zuy* z66wyb>*kV=T#!N2U;eNPFoDQ(;c8?d5*fu=clfj*awx;nr0STw)gAjUAtzAoPaWLt zaSv%w4%CEoEr)W$Z`3}?Vfv|^Gx>&X%xdBtsQu`4JC?H6?5u)6kj1F0ZL#qzVRYiUl+k?l=S60mW}A1L0aF2MK-9|JcMe?ztHZMDAMt@sKM&+Q5hJ^=~tfEroI)p8<<_3Kyv z`?d`w-K&C*yzcJqy}iA;GL4C!{Ld#*uHcKwAQ%)z{f?|sZLg=BnVh%eqv=2N|1_QB z6B9*GwTfh=)`I`*0|6fTb@{H*nxlLy%?O=HBizhCCRiTVuqX<(HCZ7k$^ysT&~2~$ zxH9CM!S7%WBiJiXtwK!_1EUFXNevfT)GzYD&bJ^L(-f~JXG-qx#?llwYbBI>l zZdcBSSFkMV9}8*{*3O351UPvq%y}!YhuO4kpZ$60TF=$}>UITv9w>#U9I88c{C79uRqPAE5I;ygMi1v5nmi7 zaWIJ8DUD;IuF1jSJu=W2up;w?sRK5W2Q2dC6v678zhJxsF*2vK(d!fa-7_6?d;P=d z@~abl+l{pM5RUh7v))Ti;)mvK4g4yxki#@M%~7!jVJGFsRkq|Lc;{5O$AE0O8bp!| z2oLHU^)I=@e@#W^O)*ih2z=iUr$FqGz)@}jMK5p6?)AjnN^%=U1oX#Ap$)T6;oTWJ z(!JV;%}C}0PZ>&3XQ+T$QlR3T1}CkMUsAXOIynaCQoK4Yl<=R(_!2$~dK4U3A}d8` zhamM<=_>@uR+nk_s+@YC4;^is@nwQVg?MXYQ~MD! zjRzQ%X7UZU_i{V<0#?4E^+g#dKDiXG>wTa2m>5M&b&!qE0UQYXH12n)!E|*L<@0O$ zFNxaUcv_EU#0OOFJPiK~S{r4k+b)uPQX425)y^85F+~m&50?{*2Dyco@+~TnJtMn4 zBeYb8zvjl&L2*+dI4V$e{W_9kA!5uD!TzBa5GZ1sE(!4+wWu9u8SL4Fwjj{YnIeUM zxPXj}nfXiFXvte+mY%7Z84<5rG;sLX6}jGSji>bd*EJa6zSEAR{oR3wLP%x1_q`vI zo(}2J!9i~8-TpTa2*jEmebo9>!*-*CpGZL-lnd;kx5JPLpExbs5JJM-+}(=`3bH2f zA6_KCppw5+Cu8_kb=)mm%TgvCS&IORrjP%_e4|H*Qpsn*Qn2@8#E6%4Tuvkv`@xW> zs49QeQ1urWx9HDH;N0*y9I#&4mr+cQr|YTh#0-sO#u|~r&Uc}SOQkY{jV~;}SvrQt zsd6t7XJ87=%9#4>Dl9|z7kszeoNw1NB`LcJ-~}`Lh-G1v#p-rSo2p2jvTz3=D|28C zxZ-2Gs7_e30Bjz(trQB#jANgER2cTmV>O*bYA$ch&pUb_H|GP7`(QyT8(Y|wjGnu_ zfQBjWQp562mYwC#LDLw|09<-0(aDaCCOI)fT-rUJ7+9r7%hJG8MMwUD-drWWUO+t@}guaM5` z8m^p0Y%U=Bd%t0GI*VnEl21KsaD$H!1JUHgC$`00apv4qFW_LM>rZV4_fk{& znAJ5k=4Si;u62<%b-RV3RVWl!*FD|_gVqCb^l|*fwDZY5t=S$Zl(nXjeBEOk{lgP# zmn{W9mh+wUkJmrv2K~Kr4E>dsCPZImxkdI^;gY+}i>?-FsCl6`=|5ju_|Ss_Ov7tr z;ZkX^rxE%QaZ^c$GE{tu`d;>_24ZJX4u29oh_cUDz}}^P@`?h9n@*}?F>3@P+au!$ zzp`+F#0miWwBIR( zL?967%3hxOq!+*8vZ{V^*%i&yp)f>6xFx2)y2pU&+L_Et_!$!fL+nKO&~V(<+so4* zF3&z&M!;8$!Mczvh-Ws+^wUjC9 zaDPvV{V5ADcQAaK9**iE8MHEf&M#I?(k}8eJ5Z5L_r4KUQx}{T0NEdvk5uI>WRW z@JUW5-E@ZfZG!>6X3qFv47*8ymPQx)0R_`YQv@O?%(O?bRJBK|E%CnYxXR}=r5#ub zzsp`JiZFFNkk09+%v1sxz{kese|o~r z`IvG2>;Cs~P3tbM!i*VIfqi_bsHW&|JSe-uxadTnkQ7lBQcvsH97wmF zICm$6+?vE6z<-{?D=5vXp!sIk*$nM*n;Hge#+9@|ed^ea5JY&6C($jXQiE%^fU96R zzJkZ5jDlyJBPX5MswzqgUw%dZNA)RD)8A3u&#{eg=GLoA1dW_e@^g>$a>CD*)n)t# z^H15!0p9P$zW(o1$TJeI{(i=zGHw2^B6Eau3hUJsR~hD`RerTMkrfp9jcRYrmMazB z+C(1K`iC&@*9B)mr$w>#G0SZ)kk3ByF%yk+;l$PX3}*I`OGn#XNZXCSnky)f(*A<{*Ei_|caKUr4|uGOYED0)>M2QxYXu>~D{%-G)s0(rZGi!+3R$@&pvO9b}6>NUd%ID!Zuw@guvmmgU__!Za3)joy%?uTMI|SP$?RB+s;`frroWU`ygJESBOmJ z{&J4_Qs96LNMxbrFes|ggsLBjO37e!N?-P_E)p0>oB^LCIvW;D zjF{i+uK+Z}KEBMb(6Pumug%nl8#ps*6EQWCuWv8PFWf2&Zo`|IK-ch^TS7h<6=}x= zuaAS*N5VTu%b5_4M|!+WVFiD#C)WJTOE}8|pXF_?PaC#dEq-G$S>>}@=dto~T71~4 z-W(=bOX_V`Ks^45GsDll{QMv}kx#akDbY46-j-s_4^Qks^8>Y%3FR&o+FNx~PfZmD z$fMFfR1&{n;Qta8DTvXvrY(Lk=`^{vpo4W-)`gM@8=3kbJf?z~4Or>=L4ywXk%Q6w z`JcMPUk9f9%GE%YL*MXNi+jY?`pQYEt;D)i&)8E?@QD$8*PSjZ2G1VhJ{A5!tNvOj z5+254$o9FH^?_RM1KKqx!ms5cn@pE^uEi&)q7f5rYmMvk=F?We*Gu>A0H$;l3N>YA zkz0-3`s2bhy*xnWk&WM?Qz^4E@emaiRl<1Zu9_I82X*!Qs+yLLSE1c>Mv70qys{$W zsx;B$_s~ncx4-Z1?yjQyEF;tAe%^8TD-(-RCw$>uP%j_A3CPIE!1Li}SmxbJRKJn# zmMzEg)6;yFzP}|0h)Y{lKkOf~D1W33e1sAJ1ycWM0qiU+eiuod8?Gw~8bi!2KgfE1 zh$LUik<<2k^Wg&Zn1@eA^Rdur#HXjQC@@M|+P^gH?{o}xMUfTZLC%N%C}S}yi(R(b zYHV$8FkD?`v$Ks@$uMplpbqz$2htHfRitl$w||BybVd2ym(BaT-1uAocL&O}%Fg}r zUY23bBG7}zVTCh>mUDNZQ zc<*R<{88gL)XH??Npo}-*}{_-+X(A7_J9jPw9ITrrFFZUVKo2C{%W!gCL&Q#{G_2k ziU=>;CI+E(Pricp-Lz9jNTkBGivzWV;!Xw9&7v&lqCU25Wk8oHi%4;h(3|v*|8sBE z>uX%xh7S^`?-7hBlR%2g{8j9O>1D1>7l;H4o<-?LED0Nt$mcE>xMUz>usOELLu2YF z|M^_tcG|Uw=V~=$)zJ1VUW=zV71=@*QVT*TPpb>WPv}|c)>227wgdj)Wd$r4#wruI z>pcy!EHt`R1(I{~y6hOLW#}5L$&qa_=BZVnY%Wb0m7}Ia)EY+t&z=jzzR%oK4 z12m~)@~cdnFofuLE-^vKU+4zk5fk}cL3_26z{2k^xd6~_xtazBnS#$ECG9Du+8bG| zieW#?xkz}+z@VghAk&6=jSk8xMBlxdbv@L#@55Q&tGwp$`M#Vh_+2hkefD~`_>87%lP2d4F6XT#;B6|`&skDKDnLUG$3W5aw+801 znz%Pc0+Cp=1lo!EgX2AQ&{J(L8mB>XLI?1uL1cTL+ukpO(v~$PA)De|$h|m(fF+v2 zj@jbCibJI{$SQ_YrOai5KvPn|MWbPe`dF`p%dFuche326Q(@|)VlGuv zu7ehx>jK+pWx<0oyyL9-5m~!y^QwhFAgg~6hdB|s`;Zma`_$^K>g@fIins$CF0A+s zHVci%aSLj0BWWMRlO6l-{a^=xfq!1JQv2d-lA3P>@VfEDDgdeASlrtBH2p%{KOy+e zH`f3l1-AXI@oZk~&g3+%%XIaaxH#a-XNC_~f5xuT;qkXHL1i35TV)I=b#)0_ zH6?4g8&%u#PM|*^O6KKTt}fxY&kYI(t!C8ihsAn_hZjRmg1%F6gjTqcSlb5gfCI`n z2A%@9Q-A#^q_MJvU(x#5$dNa3>R0ue@@7m({OYMKF%x8(IltjX<~GMO0Kepf#-s6J z0y94<_(BAtK47B19oCph+!^a?5dmtFkA;;ft+QFE7O;Pnd)s00we2o9>8tzxvc}_{ z!*n~1YMwRJ*6v%zfP8hr-*St`B!qFC;;ELu8GY5lxX}mB+10(#<%?t^k$Cf;`NM&P zJ6E4*sP4|8+Tb-W(AoR57#z)4f{Xa~771PJ6%ae!E|@x7PFv~W*yylzX}5lLkL`+Z zg~9B`^I-&~LZHj z%HQ;!6^Ya0$^GMAcAZ7YN(BKTv}$Q5!~RC+?z)%6$HpS43L;g?bF9=lI!yMy8_~&b zL6)mjQDM&2wtY<3vlool^c@( z+?y`Km5oy5xFo~OCf27g_1+pl!HKWA8L>&=`%;*tYGYvDFxjcAPYJvKzax zZQE{aThIBwUOekwH+~-=D=+rUIdjZ`%x}g!GBr*fhtwwHjK1Bkn~50a6F(L1XFd^Y zL;`eVV`bQ=aAu#Xg*|Y<|qgh1p!IuRg=?0LVr4i zE);)fA5=#Om{*rq#C?s#5()qV5X8fO_rUo?nC6jgzFj3f)78HeW~&(A)t_!nbGhgK zRv71)kNDdi`@eD6tvL_8->FXMZYv#B)=fL@KnHK4QW@xs# zP0JeCmUS===@D)dpqq#8TJwbh<4zKgWXpmC8+0~v^g)x~yq}a;f55bcV2~1P3tuUA z6l1Ci8emM~faQkae^b4}qOIXSl%1CAb-XzW;U_SEGZ-AgB88XUC$(gX3+ck+MU9^r z-*V>UW%v@Pg)0r7l7-G6a$2(1xKgFKCt|tn{WJB0A9I0qf>-4Bey8>raLvj6Z#Iyb zo10VeQOHE2H)wwWBHRIn+lJVG^?&eix$VN8d1S1T^LV}A0U!aY$?v))B_-iUf0T2Z zocF7jPS6BiVvoK#R7n-i*sj#eQDYVq7Wy^FI72QJbv5)ocAMJh&7-jy^~)BHiy1B+ z=K&(Z$oA#lSh|GkqdBlref{5h!BZE-1y8@*{Nc=J17BZRwU<&9Yv*0#3iNy=bbOB~fSaPG zAAM-(vf#q%jQpzH=ZcWV(j?gsaRQ#6yG)P-JF_rb*Rxt9|F$kjnoHOz;$O2+i z>z=^8W(n3T_%MY2CzX_3CGyJ{I!Uqj8Ad`I{E!wr^WIgBStSNz({tfVX3YBy8l%Qb zZiD5Gs`+F^80TH@i7m*>WKf&1>MU}lbl_C&fn~2Kvp459e$A(2is!zJ=FeBeBOA%Z z-$)u>8_My@G?y;)_lA4&!omZEzolg*T+{6e^3&94gFFwz< z0z*mNHO^Cg#+)c4$~WmW384%%Cy&)b$u zXdEn^_z3?A)wxKL5g&~bPKNX1|B|71Db6Qy{UxDtpuEq;=Jfo7pY0bvK%~ieFHf>a zjS%2|gihuL<_Ktv#3mjjF!w@?i#~sj{^8WlZ9_!av$}7^nnT|89-8*K?@7I`)Uqyu zwFmz&X4T9CSFv#rr8ujHA}x@iktBE8p7u69@mG7bzZ4!57Wg^b1RT+`_wU0XZ=B}!XsqHNBbxws z`4GWNs!g46@29AbCd{C(ayipq+nw7;EvS+3Mn1tNHZ;txMA*}zrH%x`Vp+r^6%|2s zO6vt{Xz>53B$(#=KHp&5`S)}OtnlT%@XD_a%{sedH&w1Q;$6mNfc+#}f4tdFt)CsH zSYTYHseJNqkz265Jzn%jcR^>w2tS67i}BPW&Ak@Po>#9^d3pl8}0N4b$$Ebv{FSulDYb%~g0cb@neSLk( z#W&9ezmWjtSYo2j`*mhj+{i8fTdFQRcv~17L%pk378gUBcUa|B4O0KNCF6Ay!5aR{(W}aRKtESK7tF1EifkUdJCC9gGN+}Tv}Ars2SP>F$ei$#&4?eVXR4|J5qi( zJJZX=f*7l|dK?M)HuS#@A%s!wDl265M+g#YobamO`TEVd;We(M=d(R45-^im8zrL_>LWdd(4-u~YOy%`U%UXOCcZ(}9LDOprn1WEnI< zETS2^0Vl!A7F*nwN0}TKrCF_yuN(Gte$6ZPGq9&EpXw<)>bQTbfLu>Pc^oyw2sovbktDPF{ah~~6_`sSSc?>4}-bO`jP z&Ip~o{3AQbSIgBo47WP5vhuFC`IOx4n+xlHG`F{xWda&rop8FO&Eyyn9Kj`Mv@vl` zm(a|Qj?)fgtH>XPlOd71Pu<5JGTaGYqE7%KoX^5yvw9=9DN&t^W;(~8G2-cAUi{x* zE)=U|0Aex{UmBL3?Jqt08rL0eb|)6}BgK(2ue`fxU1J++A(^mM>Ltd7FaZ^MSIhjf zHf4?E(X~2b!Itq{{cS5o2kWbHLgke=b_18UgM;;zw|P(dr6^&?BLa{!!X_lch0dJo z>w+dFY<}nJE(Rz*CJ)}j(nC2(Rx}=I=5fjSyeay5>e%#^Sit)IVruJCx({5UQ5%bm zV$j&|B&2#1Qr!u;@|QY<6LdD2dot6@9I$6hcDmA#A8r3`d!4nlGKnI3AgJmw3xR@Bg$Ks?MCTx^W(1K z#GcXiOkn@My!V64#JOta?AoL5)RTm7e~T%*51(IE2A{^T9^D=TmUB}0k_>sHGxj#N zIU@$^{+{WCP86MH41KG1++9TJq^;OO`%<;i`*ZntR!Qg7lTuGj4_}Oz7tt+|>Qjr1 zNE-#@OzzhiIFJtw>=F}%(kb-X)4mQK@Z|7x{kkiiMQB+K1x>E@&s>yT{4$@^P#M51 z0rT_^!v)IK>X4vBl1~nbl{d$UCZ2Oe?Z$%_I~Tu`qWA3$`gHnD7>04WpdzELQgq1) z#iyCc0Hj)BiJjFdr)8GQG-;W$}Fxa4fO*8{fHp^u5U? zR?<@qud|r?b9=n-eyHI0=LwKPS8JEIwY4QCCI$xwQ-_bIu^;Q|MU$gtl00pp`MGcS z-0R)=(Hd51Vj{vMkK{M6vnr_?Wc**-$FqfnLr%q0s&ysp=;xYX&5#dr`($|D=taCe zB9G{Dku0L{dLK*&p=DEJ$H7U)8C>^k48HHDb&l4A zDXj=skg?Bq{f%|}cBNrUrGM@+l?LPk!qp`*+5N!*W=*cWqm!^uVU)tZ{mBE!&0H7j zSuKjMvdS*AtiqZN?sW7YPdY2s)X-M_tbO?z#X9vfs0^_rzov$M+pXyfAXp{!tq)Bm z>#TN{{Ejx>zey089+eKaC17?5!X`FFWGV<982#+%3`)WsPJpBupE>Ezx4Kr>O86>W zr2U*JY<>cj_Mh7j*Udhc@sL%Nq#y}gWyXI<9{k1qWnB1=K`O-)u?Mw3BtC2^r#u=t z_Ng5Bua}QV>!paSN=ZBpcC6V4GRr~M)`QddZYA$rSIv*xM?Cibq_>tZEUIv&dD6k% zy4=Z`UAuWgt8CESjNkOar^RYvG@SN0Y;}Eh`n(icz1I+IA-O7m4Y5RvdgMRRmmFa6s{Dvw1;vsDy+OVg6yR#?Y4*pKPU^#JKL&A^NMM06 zDYf);4+e_?9$QH(ABB*K)}ZPc2X$*3Vx>G+feNx4j+g6${BTH1;#o{WR-jG@^2D9r zRqfg772m|_!{Vpl27__!2!`R2COSs*b~^K7aw-ZO(;Z_ij7SP0O$^+QPpPB~BE1lV zY89p@(vK!a3|?Ax<18Q_eUjRn`H|lnyjQs#I({Ax3JQxY4PKtvga=F9vwB?1I@pt? zP;GtdXJGHO4%8#!Zo#?Q7)~l zEaC01YZzXa#hY3cjg^ZB6qrd2B4?>AcyNc&2ApO|I-sVmz|y2m7JGd z=QV8jrXLx<143vBPCTGrqxdaZ#K6VwdD(|!QWcSxDC+FYI=7oa^K=>G=Y4Z9H8rJF zQaL1>uyGE&_k-M+TJ3U4^0f9+hmV(&3kH5ZPe)85v$L~1x1PlnH%$u<4~=7Hr4<$O zGBUZ%{lNLsApif~7^pYzt?t0Fs7AAd^p z)|35PlyVs>4{NeAkw^DMUa=BZC&jcR+cygUWf*zc!rN5Be!GM1sH6X}aa_}G=U!Ry zHq^o~n9`jQOl=aDA~S*`>_Ms;RK?67GB|>PzL&eiOO6!iV4k~b!{5EzMSyIayGj={ zB1nRdIOC447i5M+k_2x&?7qbEV&FLa_e88|@n5z1`hcCfv*zvt^p;h41xQ& zyP|{%*GoEX68a{x1%JDO())4D@y~xOL$fgrZV}=X7 zv_y_9VD8O=Q(-u|{xfE@^?hBXa^ofVXTDnc`TQQ*xgPVX+U8O0;{637A(TR)OHPDM zlFp{=g<|S49TDP(gCBvqVD0aHB0_PsdRE8iD|NzNg_9+q74}DHG`&;xN}>a86QzA( z+jv4XgSqz>iFND7^S8RCfP^LCy*S+z$^aJk4Aods@oCUF%WwazZ)Rccc*b;uZjhiR z@$onZ?1T*i!?)|t030B~r{L*De0!e|?1@Ff#8$&5`F-TH6#h05JX(@dMQ*W($%F|r zhUjENy@ZcrFp%u)Q>>vOd@+d;hxLTg2TYDC@IfLnl!r!+okj5JBO(PXFONcG?Vl4B zDsPTD;gu?d*b6k8&@fScmcn{9Ab-gsVmW>I7g*@G;(5Q_G2HQ8z=xt*F^OGaR5K&J zhK*R7Q4r6=A_;d0yW57p7nVf2ThwjI29!a+$@H*?GZ0 zn1@%m+ZzlOEBxg2%621IUR=3W9K4W}e2=Z%|LAwl7#Po+Ao^!# zcow)ke-bxE?^N6+M`E~5llFl|yZx+l;ThsDG_o4n#0%UZeNq=1J!;QWxBdL?IMU}w zs&THgWOjus{5K)g*&&%*5M6%gYr{A&3rQ#?wv+nawcX3mvQMUMkI1JX2kHtL$geEz z@`C>W39qg9sMO!Tg>J`O`N{vQ_`7#40FU^6*wFDxKSxn0j)YD3FkXzDalC9tW#LU$|IU_+ zY5>Xu>QYJf&Zy!fMiN0LLZ`)|K2Gg?RA$-bZ9@>jbBID~o?oS}#I9|!ObzoB1~ZQc zBD059s;(z~%t*q8vuGxGE+Fr0pLFN)_n47Z97s5z)m|CMv?c zJdzBy$n+sFo_A%@!{&z6Uz_UFLUZln!1Hfl-==Y?@6tAC zhT~B~gWptLy@Afb5goySE_2S9o3)c3epRW;&S9K8_ijbBx4FbGkxV%EBK!+Q4Re0G zLUnmrq(m-CBWg{``b~v+u3F`Eb&EN~GW;qLoz3|&SL^wf*<)U>1ueF3Xmx`E$F1v6 zKM&8dTek1K)0k}QL+eDLOc8hbYh3SJLjAUe*bYQ>e#D5}MTq1lvT_P@3 z*gf#k_>RaWAi_imu%SsUqa$&EkV?8F_hl0^(F(=rwhfoU$z(%D%~$^&GBo(Ap3T3S zomgD&`>hmsyzLphq{L5T4&;u3Sa5cG@V-50!Qjfk&h(wg`l3b+m>HeeL}rSl@R^c~ zCg#NY$D>TmpsT0h7RVTJ59`9_PwX>ft90rqd+vuf{Iq|%dEGx<<5_E43At53zE42& zt+p(C{+(lIl5-EK3cQaY^zK}_KCa4&GHFZi_XOIw4Z)#r5L>(NceFl*AWe)WC6l3) z;$CZLN(J5`s`Aj35fdLQki(Nd_-Rb16`Xziyq!DDlW*M5tMn6bNCXu5>&gN?=@)6L zDA)=$2FVCLsC$X2JIE+RvXdskh-2gL))>c~S6i+W(enJf`0c{><0L>PPyn>N-1prZ z9RNI_x}m!-0t8nJ{CB{(ee0kZ5@F=0!3=j^q`iik-c2^}opOou1~QI{8TMsWrKE;_ zeS&-O^&`iCJYmSSP*PGFS1kf>auVHFKvhjmiCy#!>K{ZRnxN^VdW5=o+GG9`8n3^f zPfU!#;;VB&*Cz1lZCCz>Ln+3HtmM=8|7Z&qY%gco>+QS!rHD>V#+@QFfJi!W2l5r0 z*5AT7h$P4oic^Ksa0nVXl-;k~d#PQo1OLd+a&(@{yL!sL6lt;a<#i&sd}aH4r%0G4 z&=023PX{!t{>&eRmRp5``*r=n;shVUFt#}h(H~hJLM^&CW!^s0BUNvUUT?IHc$*hH}1}FBtuwYvnvwHn2~=yOWxEG z1Ez+$_`^ejT!**`{edbVdBpy)%^sJ?5r1b~YB=6_IMy0m8m-7a4O>C&Bfr~6P`TUY zt&!JrQ90L+RroK#0}c0srA(*G&j{Lr1+J?oUo8jkPI-aE`fDAl3)8`m-4NhT{qWP; zc6MD56)g(g1s5|&G7lrAkz~&zdRgbscP0V0BMSk(m$XxrTlHP6ra@AWd01@xu*r<# zci*%nrev5cDwr8V&d)*?)WZ8DvLvJE)k#Ro`>Sn9m(L}~9p2l~+l=}iCC-Vt&iN{O z03hkbeGPj*1a~L3re~ZiesnbG{Bvi8=f_W1W7Q&=^ccpWREVgXE(@Ntzmijg_})<* z4|hIx`DQ}MMkV$VyNBJFcFfN}mA3U_J@cswzG~|^oM-#TtEfLe``8yQlfx{XMVDm6 z9ALB4qa9TqB`JwNzB(~u>XMPf(FP@rc%**oi`2C!X4|iRD(4`2|M8sT(Xw#!rNcL( zLt^Wk0r4O_P#ie&?{R2AkERFi4aYV?-2}2H!G_|ck<`T5#LHYMt_sCZ7iz>Zu~|T6 zr4ffSBE;+cOhh2JQjNOBe!j`iF|BBtwlBjyRYP(62HV1cc92cPbb6D5AJf`kJ5itg z?)^z*v>6#fpWraPL!B$wIfUb2s#%X|6iyNnJA&S&B}}a|IJW2Puz$g6vx*BwF|T2g zuSfrP;iPHtlQKI#tmC|wNe_7{Xo)SdUUgi)>Da2PqR{Tx=UD2v>qzMJ4zf-af|eBH znhY?wpxVCp@O2RJJd8}88F;y-JH*`D;mi3?1$7LIIDV6SLU5zaB{ZCzm>$2DneW=Hmn<_EnY( zfESb7`MR8I)w?~QmW)t91#iclrg`mWx+&|uIg>T4wAn1Nl#FnRP$ZAow_f%GYzE+r zaY1@qb$;pI{_rn?W8FO$c#Vw@XD#RBLqn+?_C$3s9scgr6#)TIRwJ(Kwcd|cd|r2o zdgteVH0kcfdCoUG7|RVh-lkuUGtcQt7}shej-aSqX=sN;;-!Hjr>_ITOZ24=8$S`x z#gu{l`y&dBlO~kAaWm6zjC>GCsZ%7FDRW>5U62jbjZRXl8;fS@kcx!Z7r~8x?v}mC zVKs-EWr15W3vHt&v=SZaW4@}Mz6p7~t$GV-%r{z9kFk%GzXDE(+U?#HZ%X&87rSmJ_f63Su7R=m29R=og8QYKTPwqNdIvc?A+ z4JOk`N)l1|PbQ03Vec}{$pRuB(8~aA^hoB>2>q!wU1suo?)LCDpl)C5}TTvwr44Lbi!*) z!OQEL&su(aV7l;ATxb*Bss`T43IUTnY6@*&{}mvtWTA!%iSvgf5D*>BVZlV^Z}F?` zC~8gSk^jvJu=yjCGoeh1tu3ku{CtK~X@2LZ2Eh{@!GNZ!75`IX88_-|y*2heO63nU zn(WNnk)3PI3HgIx?)(QF{<*sEMNg;ZmOeYg=7PdtP zN}$*@yX4a1mB;Sy9}`|gj_m0MWDKd^c9i{n8=lCi9~jUt`+(K5UUpE^!Ii;w#OHY9 zRCy6?J{q^A^=ae5v*DpR5wadv1R>Ac*jcbe#+QMLmr{UwpbbS#z=(2=2vaTlXG3(v zo)C#2F@W3|#?ZCVEdmI(QGC>2-`2st?F^q}vMF}Wq&@t>PM17UU>-stR>hRQ?P?EQ zz}|rKONbV35*dcD2g}Zz!RpHp_*{&%OC|$M#P6NbWvKXi%X#W~-gaM9;i5c~|MvDK zv-5sxzuI`&{nYwjV^E&py+)o3E|e`VF9$B01k@N|tG>^tstZoy$AAII7SKobM^kHn zyXNcjy@IT)qR&)&t_$~?j(wXeU^!B*U3bJHTw^xY{hxtI_Ed162(j;jQ3=w|x1bGL zS;`rk?<#t??*>5R!Rza}pFzvdQ`wdwMocB2kN-)m{+IPaEc{5JIJI=9kV>{6@K>HzLL=n$rWG1m7*n5td3mV!_0V zeCe(aiOuiY*f^@QXsy@aKen~@Im20~FeEv279CB%;uQ_~3WX}sX|#WpqicjpEHAbx z7Vti>?jbm!q>u6py;^#7$L(WQN3)uAU?DJljnjJ0yke?|}GqhS%Qcc(g_uk10ybdD{=(U@B#5 zi)v|7UW~2yP=EhN=-vTLUe&H1lBfI+HWES->I7jLpQWJjW2m>4LM8qoVugXALm*7Q zgg7Y^W&Z8PEpb!|-k-Q%%~Hv=zZt-JrIU{1q#(|RRV(JW9EOe|=c9EkXtA7$=&`@| zg8R)v`mMx-Ak=Mr+J{^!_%GpT<C++@*g@_IeiyBYz8)PJttla_OW-6N09u>rNo0C+&;dXNXgv@zk~EdN5r0 za-U_@UQy~8R^wPzO48OqQ&*|AGLz@@T{#+7O+h5Qlz*;Xfrp>AmKl3vM?}b{s9fLL zdR9A~z5bKcGfDZi|N2tKS&@$MhL@beHcFv^?($siznixeh4!ZdgVHmv7yNd!H#P1< zUG@yo?jc-G5CU$RD2O+FC!W)XHXfduC~-`yB%XdI)UhiP>nV>z-+R@lgwuM7`^P%d zY<0gg+rUB`9?NdX3KEoU@LPSI?}&DX2MVDO(?d0r1)e>^vE3Mfjg}bWt1O%zcM4wG zV-;nyEQg+_NyS9QY0Zv%q}IA4aoRbn>GR=){Ws(kA*td4&CT!SgckHCJld0fHm_RO z^I7$T{Q@}lesAsRIG2!}Ql`rjRp|mM62zo!X*_W_^1iPmu$PL|AT{zL!z_I45Cpsx#(ehQBGjH#bL&zMzv&A4?Z$gqEn)9o(g4r_fqi{7vBBRyJGUT zP4Q()DXh--yAFfRS0?kt1Y-7m*hsC9DrfFng&?-KC@zBzt{55+;XPy@>SX)7DOzd` z7?=3SD4}Q~_LSoXoSet+diI{v?OO=Z48Fm$bJs+PZ^tu^fOJ&gwqFhyT3c3V3lVn< z67-?VgSvk}u*ZrEm4;a}j8qvq(XdgrJY;4jG$T@it{`{HBIp!MJR%$NUPGTKO*P~f zCaZnc{%luYSuG=^tgP(j#<4Q=hPLT_aGy9dskRv~xO;VX_p^THo$%s66QWaHeIq)} zzy9qfZYPVE07gmxP^uyr+YO%yqCX?U+J0X-oGE;N7!YU&%vf&hQ_|DDj>{YVP!Dfk zF3PWT>}Z)|8j?+`8s_aFRKsQ1PE}gCI=MH1Nm;@bD61 z%2T?<7JGUPmil~u*FE+YrhaXRdwn6R&?8aoLOjwDS}>6C=+!53RfQ68TG^Vy7tF1?&~$N|kjBhZ!i`IIniO^HVu> zfFkJLdV)`D?guogleZ5%~{O620-m@yx|Pelx{4gMX?-Wea@k;A2Lk~9#z#r%PIxVCc) zCFR;=3(hnzhlZgjnQh;L&cjZnVgd8GIvt0}941PO#94k3N<7*#()w9vUVDS_!16V5 zKYqAN!Jzh~+*oDHc0{GO@kd&mz$fepvIJp(IKN}8O44OW6D`TC8A=@wqW;}}2Olay z`>UAhD+xlFq^4{a22_$^EIe2gx+c0um;f}bK^Z0`sS4>V&kXdeD|6WOTu9@jF?*O9b!{}$!7f*W)uhjH)F!8Tw*4c-lwvl z`GJurasJ6H5?>uHjb$y#MnR`Ood)yPpL?e4FBgZ+=od?!&)>1!wXeOC%wGl?9{AU) zI!?NdXB{o3XW3Nqg!;&7#g+J@k(6ZpbD%ImkiVG0yVO2yBrKST|AKULLvR@LvV3W` zdkas=$dsP8dhTXowx9Eheg|4P0hl0qXFu)6x?RM_^N`&OCYlFb?)L zs9Lg#hg5aKXf-)etQ@|h;t~<*x4JX0G_{hZez(=u)zx)!s@4U>qu!4IdZMVK^9+Ek z8N41>#@Xy08ynNp!tZoXRMP_Yz+a~c|AP@0G_gO^F;S*fqiOH^Y~Wz}IQf}Mo}7ZB zDw7pEg~fFE>?$SoeE>?d}$SBFba*wc|Z-gIO`Z{nPU^fQynW18o<; zXF|K~RF1ApZEVU~j%T0hmKGS*3azj(89HNsKy9l8eUR{=&Jblh?2ILcf{6nKWBAik zxMOS~HyxC`M5gnz%z9+lyW60got>VI#|gSg^=dO4m*X9-Gn=ikn{99zmxe9VlHN`K zZKK{<#CNelT8s#czKX5kJ}5!pwmvlN)H-xibuZ8VD4~{j52Ijidu~3re)j3rp!Kbz z(*AMR2MPTRGgN>g^FuDKRH|Zmf^uwRKpr_A2|7vil_OmkDyDotIzJ(&^!r=DA~)hC zQ;GoSMh3HhEEN?>iW0>C3(-+Dh(sCrHzX8FD1kh>GZr%#n!G2hA3O-+JwLyLUX&v@e6{5Oi(bIg8j@T?S|4Tyj-(2pUGDMzIaZf8Mv&aw| zETIBchr#jMPE&o@<`F8=MLp(|qNru7o;7xm_32-qfl!m@zdl~Q+Mny@1OD2=85Jh; zkk+3H9V1Qs((tYEn;RQ(U!X@XG!#x$mNgIYC#Oz`>~v&e!Rg*3)g%ZY?}~+2lAbFP zrr3{vIczATIQ6g+8<2^~^w_1!99vj9ntsB+IeT4x<9@LrV|Q0HuO#C!=*>WBUN2Dn zD)Qx)4~Wy&udJE3K4pH^s;pYD&Wz&Lh+m(|`N?KQ0C&3vz9~#AVvP#Aj;()Jls0gd zHt_NE>#2)w{vzM*4p6D@vmRah4cvGWoWV|z8>=Xn5Nvs9eoVRUO-|^nPbALhyr@*4 zmFPes7|dec_!RBxj0U0ohrKFS=L+m^KVO$jR$!y~IUH_i?yKsAW||$ImleHy%lJ+j zXQ1bPciQ(O@&?*+8ARy<<;-XZ814P~XEXM(MhN95d-y&LX_XRP|MNl4m+Bv2`c#tb zBf;a69FmcZv|~HH{Rcza`z>==fr`V|1DI`ti2AY)uPT126sAPQ{!;WTWr9LXDjLzD zP~9MEQEKvDLl7)diZvs`uQ){t;!iU+hv+_d=xs5$669516%l?h|B{wet)TqM4(T-l zAt2!tB_gRg>0%ZpkNKTdC7${f9dgVqZLA%*Qw{i#lUe8zvY0<<*YM`FFkV1DNob(d zm)+sAco)#ckjz+*qNX4T+gbx%_hzd_hZRm+{kDOZ*+Tc!a^#M1;<|RfiJr^ub4o5g zq{fBK1Q@Bv{L%q_Tofb$vHd z_`JTqCbVa7{emM}^o|A);m0VyzP-x0%uK@PjM-V$C)Ps+v*%3)M2#f0I_+}Y5TNVa zX;m4EiHpPfOaXi2L%ah2ArM=HyF>&Wh6EmG6a;*L6>{DCTJEX9{iJH(z{@BuQnK+z zH!|K`tLy*hM8R)g?ENZpTOXRfBp=VzB4V&Lzci!e8%)b!?9Y6o&iK%rg2CJ5l0^Mj zV!-?NWU-|spVOrcN|xf^rvfrR8dOfS;Ed;1y+k98IY|T4-X?nE6^tq@ze5qim5E2iNfdYC z5P4B}Y$>h2h-e7rgAU8aGwrfg*~@WbUlZhc-^**EpVsXKe_LRe;z}tCDv7|^IVGR zrK{UK!X(!ZBrg@DaKb;_qEvC?{^Qom8W!^ zR#hPnSNW^sr4y_(-TR{daf|sO|?4@Zvbdz>j@k|GC+qvTr`5tatv22g1RjN|aWk zor|N8Q3voFqn;4p0mP6so9}*CLsaj+#xcYKc4j&656%;O$(flx1B7mL`mHH%Kc21+ zCC=0|^!o0qeBYbgImS()RuJ}5A|3b>0s{ldU{@51M^c$9&5L{$#$wdzBo}C;40(Tj zdZpq|=mRsbKTjXNNpS$YN zHa26%=;m;+mo(UGm~{LKScMErk-1BAUwOpg+(JBQeeSCLYnIJd&dxl$%DPMTSZ~Bf?bvPF5+|0 zh!(|J(*1K%&MnW)%cj4mqx8m`7+)t$Y*wH3UlF0}Sx%sS3``sU-ZIzT^2ZX1Q(&NQ z?NujqoDCx6A@h+VCZzHGzMqEq<2yr^$k0Qct~B+JBIO`KDJB9q$Td10(Szb-rJ8Uj zA|L8u0xy=|)7VZ~iJE1|6P!FjHb?VAKbJT z+9c2M#K~ONYW&^f`ffJ++U4W7nD>gEEN~KHQBWz{Kd!?d&8Q2SAce0|@R(r1YZ7<< zaEgEA!~>7CvPELh3l_WIa5}lCPT=eX-&B*$1$4P*oBLLtzGZJ&RvKk4C~mUvcnEqe z+H`QuZ`FTwurs6H7IE(A0%Jqd2kVjhI;NRkxD2K+!siQ^|g z_%(?BMCZ-HuZsNBUksPFTL}~v!5&1HsC)qWrOmm&o_|qkt#Mm>cEHs<_4Kp9a|$_| zCjD*9r=ayRtf&*!+(WJt%+j8Mmtj>F1m@5Xbdnh}(LY(|VYI&GeCO z33AsurGWTaj%Q>tF8S5)cQC?O8A09}<#i8I9 zG_n-=R^Q#8^`o}{PK8&s!ZG;G= z@kj9B!iBb2pQ0Dj8NRQ!ESr(3qlJFcis-29r(5EaEd_~$Qf(pi51?d|#R%1uB*qrJ zfrt-6{(TV%uZEbL+3DlY{A}}-9MB=*>ix}!;x=h93-6WY3n|w7jc9dh z5+|Kt@eH2V`n8JBj6{pK5!SUsjSeB=G?U$kspwq8;iZObr}K}Y4! zH}%b!5+-T6BvJb3>!l3~g`7t!WzR14+-;?n9|>=C#tUHDo|7BXDjeD9T)#6M@-@0_ z&RKiuYkQXPm)n|$drZ`2cf4J+F!KLB8+d@4!esL;QD>MWaYDq7`|Ix?Z)8dU)z{?! z6@4kGoJX1$q#PR%o>ZI{F%=Yig-5X@@N{Co+g5IBuJheJ<)31uQB)!O#!MQLXHP)* z;E6v4H3)MXmjpG@h~_hFtIhEvM}UjRa1ar% zhIH@bFAd{z{rXDt`eWBq&0h7}He>5t!sCIhlQg$j)aX=k5aw_)!v44Xup2BfV-~32 z`h#G~cN~5I9f_>Q*=oe4>ctg5r%^SEbCIq{q&gC-aWZ&aXNr=>;)zn~siTq2{=;21HHZHu7iyzw zu513JDkjUdp^VUCZS%eHu6Ly`W=$wA&N9y?nrOdt>IdSkQI((N2c!esWQZX_Ns50y zIjIIS|4qG4()-h#S;I5kN(|q3OBd*Y=5WcBz|fuEGx5N@s81rZ8>WOv|oRLs>SLuyl6hIFyZ-45TEvK9E^>H1q|Y- z(GgH9_w!=lQ)p*daZci4z^DoYI4sPXj$Jt1RK7nyXRp>78_^2qctcBv){XW}{lJvZ zTQD(B=oFlyhUz0zB#5RkatQe9_(a`s zF)k;pSL^qtR>9N8ueH5(wMEd3AR9;I4yae|DCaz&yIhfh8b|Cf@!A-|chh+7Jlxwj zn`qmH<)TZe_m`9|gl=@Hz~9?)`pAIuJ2n!UUsb-Fh2VDphEjp_O}e+m$*urw7(6a9 zEx|pc2wE&3o|X)r0y&HX22COeU-?gAkSA@J@_3_p{>eSKn)lAE!}@$P^$NneA{-rO zVQ)F8-}^8*JlKRWRrxp>o9SC>wWuZn_ln`J;Au&J@0K_{5~kkFOpN~xMEG+yq^0yA z6mOathbeEHfA(X*8%-qI8kWO}z|;QN{VO_U-^}b)9vd>{4vQ{B9|<$!wGkVhP`qLg zxbx=lGXxQV6ok&=OFOIpHe(Dd;P?B@O|CSCTJ}+xu5>rg0qM}trt~1&K>2$ zVYeZ|b);hReDis#O-BZtD}Qb!S+j6>>Po{Gb;u`&l!;SA<;PKpS}EPa;Kak18o^Klpa`fPS=Gy<=A6U}oDek`U+q?CmxIT(JE ztQPi>wX{Rl-AE*U@sJRn`5>OFjv;W!GLp%)$e{p{Rr&?wQGbNH!E!8?WMd`m3)^1E zA=7S%Z1C^;{c$3#jg97|_oXw1b%x~(=drq@Lr)h)i`mTgi&2Y8a$A(``x^eUQ6VZD=XUD z-YaV%?v&kBoZHD~K>b;pMO?tstSDH1Bxy1XC#LOj)Q4kOt@;q3QyzRp%}KK-(#1?F zj6S-xDGmA4XCHmadST0)KBiJAZ_1NQ z5T6WGZWgac+-ZiRgD&P`djkWv4ONN>OZTH9;1LmxYa@`)U@a~IjsZf+f5X`u#6@FJ z$6P|Bs)(8Z3E{r)yKRd=miad#YZ)|ewGK$CfdHiMhm~->2?A8Svt|j76 z{WQqBuc+?HnxE~6HtH?`x=hDgFa6V6pOSgs-%^%;)M(4$A{EpAxon$dh}>={8B2$C z?AdlJO{a(HI3;_Vaj7(#)kNFHTjcb#y}RD3v|mZM5NJ%KF(r(pF;&BSk%(z~6>~(D zR0DF92{bxm(Sw7v6NL+tED?N5(vpQo+xUI-#|m^>_}8(xygIGR33>GhYLi91hP$KV zocU{MkIEj?(4uZfP*|neu&ZvbeW=^M5XyF}9)*2y&8esDk5xy@?pOnTLoG+(vHo;N zNTRV#a~*w)H=?|$m1kjxSf==mM7fyUkV||YdY8U3?qPO={LbW9BlZ*LKWS;Po(AYL+H@tJ(wn9`~7qTogFNc1u zETg{ew1qsEB4Jm|j)na5%ZckdcBEj?*hHj24RhQSi9fWCX`dD(Hjypqhp=Hmf=v$_ z$q7?C;M49#8L{iz>Z&GGxh$6dedj-B2dc9irH&8F&Vbm?kJf_b2Df@mHmqnOp~q!- z455A(%!4R$igO1MD&S``9KTP$f-=mav>-ZW-b2B=UfW{_R^pYEbJiWWs7E7{kAy-! z%{h-6eBgy__Qacp(-)t$lI(Tz*>4s4B{Hgz%UBnp5?S>v49(wkSba3RyqL%~y9R^c z>2KZdKxRw|bdM-CgJ{19IOfGJ0qPYhT2Y@>Bq7>%q6@F3NSvh^LDn9yAVi za&U0yHGHOV)neu5o?c&XKDvBPX6SZg!&C3ib-)O_rh3}#9)^|#Dg1*f*OoRMg2$9F zl%frw+M4)^$?BHKclPoRE2iAzK(_o@wyM%uW>K5hn4Y*!JxbeJyo??A@+jduVoT`u z2|dsIyXp+;V>l+Ewa|j~u$0WTY8!YD=xz3(zVWiq;-z-hm?@0z=H2ic?%Da=BK ztT?q4LRCX!72)TYR0t4-MwPoBfUv&UUh3X=dv9t@9OJK|)Rf{(2;t$5y22RB7@t*e z4P>rxETEAF8!=t9_e7SH-q&!?craZ~@s4P(bq)h*!?5+p`0m4$JZ^h7iI zSd9Jxpl{~S_$%)1XoNrKdI5|om)oi`yf3sSBziGP(amG=X_pwzv~aux{Z&Pmr*&TM z)nJx=p4Ul*I~#Lsc2LX{TCEHI<2Pne#ASFBUQ9W0;0lMpl&={)T^#=z@i!ud;<-aRdl!c`)$;)^7Tt~__&$aVyR_2-Y4a{|w%yotG z3p$m@*0P7Tv$t#(Uib&5;idZLMKe=dB?hw_EC1;pv;#GdBo z6D3H=Yjt!VcMV{W%u7HJd<|vzNb0;$ytnM5=fqV;+6<)eDu^BUf5_KHh9q=2Nlt-C^D+hgw>Q8x8Q++%d41nLXO08$Sj z&ROyZ+blh@He%|q;#YB>e%=@ox4{&SAGEkESe{WZA61M7RdK(UBYc^Eh~e91eJR=R z*?*GV&3^OI>1$3LDLTx1C~vL}`*TSy4(+ zIZqS@ow666Mu^%$vn(K}7}uaO<+dlGk#J(w=_xR-E81!0`O!RP&lAu|phwb-yEK@& zBc~FHV1MSS3Tc-0kMxjDwNnXM$=J16d_5d=nRXpk;p2mwKQ zh#?20OS-!o89*9_5~RDkq>*k3=@bzV>F$Q}yPtE;i)Wqx;T>!7!X;d@_xIYLeeJgx zZvTMpSX^GSfPH*Ep2*&Rb3aT8O>kI7iLYJf?gwv~cZwB~N@>bO7++C6V5ePfl#=fd z_6?I76n!@#Wv%Y9InKUm8Yi1_Fi2%<_p3hQ;HWneCwtt{(a}<9nj$$NA%U>5x>dgK zWPNZ^@v<}YEca{h7Lb*OE_(fZ$ufv?`z?9->@Z+SX||n>37u{J{GXOLfps#h<7PA6 zt{C76TEh-r@BOctR?6oC^x2IO09D?41BLwX+5g1|!Df`sl|+RzICH)ugvC4YkQq0w zGAl`eQklMEGK{GaptLvT3md`>uc$eeIR(e8IVD= za|WKQS5H(9do|0cBqKW$9v;{ysDK$7Hn0n$z43;sB`J~2_lpHV{?l=1jW zaHDUOxfC4x3K?jnjHzC}vBvN#gMX1`C?T9aYBso+27XP>%nS3J3m*>;ei3^R%IFt@ zW#sx)h@I6X1Ms6~12m;G)(=l@8S}a3;F|$#*R`*3kEFq!yKM%Twd{ zmBC&Aq13YbPUEL``|LTD;sa~h9Y^?q?aUQl;W^)Q?vee}1<$U}Yqd}5(|RAQ#|F^G ziu$XH_Uy3vxVX>9zM}g-o^(fvYKp3V^;|#L5l*xNF{DXNVxqIhm@^bf(txK$iPhhE5#gkzGs@JK?)(YQ-_#!sSF*GBuc9qL3}XfYzC-;vX&CqJSxd7*qu#ghQKfX137Q1EGZZ(;<=0iKMh4 zcrOvmf$~lgieNYhY-?ly685L%7FjnqJxB&+6v6k_F4|?pg!FEo?WlWUkS7e!scl?={zvp8)l+7~z7{LB@JI-lF6 z*7|BdNNsH~#q4{QtfuJ@e}dSA(o5#luI)RF3_X;%FEzoR`_8e;L(R!0tLSqWU(G00 zk361F$5`?7T$;x6I<%cgH+EvZ6~sdo$W&~6_tRNU;J;RQewmT(&6}_TvnN}AYC$=t z7QJ}`ANK#0U8IoeQpiVle1nJ;KE+sy_?J?k7XhuWu8zHXgG#X?y3q0Mf49S!3x3M_ z9Z}KIzwC?!By?R*FrX+Ey?}LNs0ebRHcx6_l!OxY)C#yfY6!&Y zd*1)x$bMMuDz&*v{ae${Z}XO_F)79^8P`(_OP3WG6On%-3ONA0n#_Jc%*He@G6I_= z;#zy-KaeXAt;?*6L!tKZ%a5k^lfFAMSPJi9@)%?UkQpOcR_=|Su>23Oc1=) zG!?zTO4oQ=)4*o(mfBrolIw37@)v85rm{cCK#IHfF%geX*_2RrW=U8_a;d8{H#G7%%(7vW!8ia^|P+ zrtyK}@FCZK@mFC~_Z2%a<9k|JWrlu59PO^vkE2xfS5)o3hRAlsqY`xT`c8p{YJtKg zfd0Xs zL!$uY{F4*|GhGiCt*=z>FzO~b!9;OTa6h?1YEjP=CIe++Nys~`lC6Ee$HwU+dpBVB ze??_t;4-6sRhj1vOob`>YA;%Hcu=x9y*}L*o(Oxt5HGhi>^SxUTF)z#Bv?&rS;9Et z%eG|u17LG{lX^b0jGK-+r@M$GAq;&2vWGOfYCbiKtoSlayV_-*wHH579~}G7DQg0; z&hOOBz_U5bYh_M(LiC?5j5Z1t-NEGQIfK{a8yRjUn?((L+4~FY2LcoR zMN`Wf6mG;)pSEGFdAQi>Z3Bpsb=GN#H%OQ`xFmlUK?auz$MG>|6Pwo8^+rty)Qq~A z;ZzaXub}WypBMpKGOkOHZ08upTsXvPmp^3k1{{j~dG~YhQ9I%8*aR;88a&EmK2*Yx z8Z1e!8X$%!Wy6t<=TZpo2JGTcB7D+#x^OHj!{At!b zo9jZuEW}3KSM(!+MX&YJP+O(hw*n20lS9iphHWXIPNnqc{}AfJK6yR_R^6T-h8E6k z?tRCt*#6!5H*)i}63>du<^_7|qv701a-HE4!tk)N%r^1t%gwZ`Gk0l19JAyq9*R7T zcGo?8b(andjldMAaSP`yVM|nIquY>ojzjVt#3fGnyxqDWZ(RH{ek7FE|6@X1=J)T< zh0ic18YMafRvbB+b8s(oYZ1UmRA?rP#AZDDW+Da4JnxHvW!f-cDn5MkcA&?Ir5(5Z z@ha`~#Q=(M+2*RMwZ%ci`)_vysOb#gljA}o?b5nD*H@y=bbc7~ElO{T0Z z*v-mZ7=9aIB7_cMXy-{YvNS8xr~9^Zn9)3RM|Ceux zckY71kK_2H3OW+`I!mu0UQvr>%hnnC@;@8(%xrb?3j92hCAh=b+(x~u$e(W@Y*$5! zJfy1GV(l_bHEdb%{d=;oFzNsMM!|t&k9_x%W9LtgT0n}xeZlyd(x2&kNB^l*XyplI zEKCuyZCB$fp=~;xz+#9v0XVKzT@@7-F5g?XMuH)(oo0Mr=ATLIx(j0z=!2eEOP;g; z%Le9pzJyygLLTe08lulQ&py99gVY8y2Rx@PqeFwlQ}3m{R?FMEQ$|5*fr>=jnhqaC z?c6>2e7@WGZXvAdTdms6t15TpscqhZgd%%m{(f=SE%CAK`|iGAz_SUVI$$B+VWB*% zB;RE_LW9;n9(AsGJ=q5bibj9lw3YitPNG(#EdPU)0HSJ)4+P58Fc5E9({sIirJNGm z(^lQ@wf@^517&k?Gb|9A&5uDyK$C)JZV{n)4bXvz5}b{_IDQdfku8$QQEJV*TLbc1 zhpS)15+aOif9%2YUaYIHZYL(ZrE=D)iNj*=?VvQX)6_)$nOU0uzz~AKoHBO5(r#DW8Zm3lRqYP*w>3 zJd|hxuwx|@;^r7?1#-pV68OwFdJ>%M8r&OI_1Biv%18c(GX5(kIW0SDxwzSr~5JG>p=b-YJBRTdkzG~Gp`xB6XemjZVv z>vy{O;11=ZI;RWKNXA++^{G+97EUxNcmC1=XOti#ZLIDgytMH3q4WMCl)`2@5W!?| z5XvI)j*7V8fMFC9TY#nGNL$sOQ)#Px`uc-H6D_OJ&`EloZxJvzP2P#r#j7Foif-{! z`-A3d^-KZhC#Z2N_}5OR>qFAMm@d%q0=b%K@PtRaYOumWL2_4Enrpj-MOhJ8aw6l1 zkGMIO2#ez5{N38wVIw7VTjS+WD@FpJMq#*;#t6|AYSG)ORaQ8mBJyDx1#cbdrWg9C z1yv_J%f%Rjjh*E(LwadSL2*T^CJlCnC|(waFl*}j&BgB6rmMz@&V$|DB!%1N(3t1c zP_yb6{GIoq8&6X{7vGclG_&RFe$@Be%Y_J&BYSgm;ao3EmF z(Bl+lh=g5s@KaVtM@M}tV5;TR8MV$6f*UEeJ;2ha`L{xny72D3X$_8p_4A$4BtEyCp zu&OXW^k#P&PYhQa#mD9fJ(oxS^d}4KBekGO-k2n9XP#d*{4J{tti|{c@QtxOF2+~} z?IAuUywYSr<@Mojs$DfEr%v)L3`ju@?B0!)SATnW;@ z;vgtWq?1CZsS9c%#2Abf^hq0^poA;yrJ}N&g{_oYa`&`pF0Lt5s|{Qakq;m+AV z##V^cuUdYO{kEO_7rXhH)gr$yZpRfnPrFLBsP6AK!l2OeFNHS&N#C>E&U!@rOWM!6 zk`mLY=0(~9{l9+F{BDw_DQx?kp;=R=5I-f0frmkQ4KEZ4PKY&%;2-M+=Yr6-5+p(h zd*)YBD97e$F$MegrH-Qp9mM^f{;oe1xiophj?l{yMWf0-9?udG76YRe6Y*PoTRvC< zP*Z~nbB=bbMk48$Y60Llm;|NUzX62RGs3XV5TP!d?)RM5Zh8DXo3Mw?-^%D}g1?fK z{Jkdd&vF6#rs_41!yeyU+qb=~rnhnlZ}dJu;_DL>Xyqk8ZxCi_Wo3+BPxPI9jyK74 z;Y#^~p0NEG22JRw2_g0U`1f9$CK!*l4}$~7CGb)nQ#-G>kGvm|X}9}ARQ=v!{Hw3M z#)~4$yVz$0yS$s9sairQr0)V#b8|eaSOBB~psuH%pISgLNL_-slx$c?xi@g8d;)Xx z1Zd?Q`t&%@jTN8Pdb;P>^LDGLttY9$w-U?W-EX_5JQQbw`$`Xe`AlS|nY|%7tg_#2BKzH6 zHsIZeB{V`ds3Yx|J1twynN=VEhiaFDtWhIO$d+!7@GR@w*}Bafkn%rW^S$|yp|>B(^KJXmZ;XsRqIr1 zdY10TtZzmK4?j#r<9|1D`jPR%s#D7173N`QQ<&W-dSwCKt6IEFS_B{>T6#>(7)-8P zq{MZzssb%H5ffto>BbI*0Vu8nwrRDYRXsDl)V`MW)sY03G*Wor;)P7sIXM+>VtO(r?(MP5!YKI77QIwcWot&3*y5+FXJ5SnXphnmmW&*DBQv;bpPN~ z3LU(!xdx_}!C28!B_ezaNv~`Fp|XD)AHO06p<|C<3qro*B)8S9UD}qA4gV{bWhRr6 z1ffv}NHT}JGUlyw^E`*)n?7Yco1qU{Yjg|WxsFA`oFx@<&Nm=k&bNIhoPn`%5>bTt z1z5csq;&nXaW9DqbL2kVoa>S>9xVg$zTfCP??UTj>VOH$wkZL+{Vc8A}eMIqwyhj?vCoP(`jUfa!0eO4;{B9(byzE0qF6p(pI2pXteYUwH(S%&K6)@V zfk@P6MAbJQlm(+b;ZG=_HD_RolgBqkqn1Kt9vZS;=cPzU zLbSzL;$?RbiC)r3yJ1zc-4A7po<)q0ZV{_nBU0rK_?F9TLoFIfo~_(_n%gId$h%}r zzmoJ;)XXw1MXjN&lAIeG9&xlcR?Y@x@PdA?65*K7U*&>ja#278+;5~LKf6e9RV9=v z7lNXC2){VR$H>CFw`_B)M;ct`g=<&09kX8BtngYb{Cih2P^Deslz6k4<@nOhdS|`>z=*a7$_^H)=eb?5#wlu}6479;YglLgKh5ZF6 zFsHFFri`=Ph?NLjMy}@(XHIlv^nz@`jMs=p`ALBDT=&`d@(P?AD(gBF3?G&H`Bf>N=y+He zO~gwSC=XE>u}!!Pb=%cJ=aV@YoUu*-q zBo2K+ZtfnBj!s}gsVG5#gN`b0mrVx9@c!S>*Z*2os^k$~??(~L!HurOJ(%_RCy}Ka zz^55Ay7T)#`84m|3nKpYHXXH`C83q3XtEb#tintmT;wuD4`JdbB<{!WPWMfX zRS{LDf#I3eQ|av&*YVYz%0FXU^o>7z8!PGO@>FzpFqHE)dK;(0EBURKcbgnA7aOH0 z8S}@cxU_~+OqWXRU1x3)3*WiSSWn$F&a?k)T({kH{ju{M4J!*FWDGS{{v=(!+WbuX zt2%`1>@~==N-ZRsRR#3*i-O9Q=z!=!@(8_YJPF~-3D-nyg3Mb?2{C*LA`TNoiFQzU zLkTk)iPc(*Xq&Wp(*wy^)r)}c7wwshmgBA5 zA5a5U!9Q>L=2()!MYPgUiZfxr+$T83)jdb!JYFS_CNxtlHin;7>`2e&!O@f#iAw;WN72d5jv{}knH}f}D=VYI7sKV_gsvlV@pQx53 zu2vOL%&w`Zo0F2849rk1tP{?7tZGBQ?D23?+NciKYbt7jF$t-Z@=j2>vp=t6ItiR$ zN=i|i#jDQo<}nmnGL=w zDt=4Go3F3Ps3G@Zw-egV+by@sl3mxzlT9bNsBuvQhz!ENQMim^-`FMdxIx54_!1(* zj$8t^u+8|`r^~3V>!L^ck1u52nFKEQviWS2@+$}mxaA=RerrpyYx8ZODcsPi`AwkK z*LGHRt*C78F(2!=hzyKA;`;3fByI2!pZh7V+Ox(YoL8HKG}%M8U_+ug9p|}nsB$am z=K-%cWo5gJ+TKcqsbX!0W3_DW`4_=Kvv?HP)D+60zl)UN-2hhHcMDFU` z_Fk-cqo4W=z_m%}LL+pSX<}MZIpy4DUm zQpACq@qw8Gq{o>n6f?l8=!#+*J}a;%^}(a0bb<%A93Fl z(nYX1Whe^h7N^LC83^c>r{rsl&5K0{CR7hH3WBCeFrw3@N-(2AOqg79$INsp9~eqb zyg;e)%sb(4jHpT^q|6x@)J#i4yW}L1;&S9XDnX<0-l8f!(oWZP%ItP6o(Yv{%Y}dM zMF?st0L16|gVOEKa^EC{Y=Oua*Tk$G@v4>hBg@~}D_B_8*IveebK^H}`4TNR7e|>3 zf+Emj=Zgp!o87W+zqauGUBkTqxc9b2jSLn>?_ux`K*+hfM> zdf;7WbEYbjLbqAnEaV#nJf6jylTB}P%9W;O|^t4B*7}KIpkl3rPf=!yE8SI{p(M-mN-;DK`F0% z2kvu|D!TJi4PN-Jy1h(cLeTG)>oSw1;s+I(xdiLYFz4pUoc0&~WZNt6{BlBtV z{`@OTn)ufh9WC1dXs0bHR^%a*#fcrUpN|=xZY4}Cil@dwDIJWAgiIQ=Zn85aJQ|e> z^Cq@u?*x^y(}JNh4o&5=HjSMx-U_M}DHsF90UH4Zszd9+`^M@%`M!;N|9F52vi_gd z+&5t#fCJD~+nuJrKkZd&>AMpxef*a~s5c3nG2$Fe=Zifcba6uP)%c3)yDcbk4w?m@ z;39^O&WUv@E{$d^~km%(rf0sd%e$Div385{hrfRQi=OWjpW$4^5m%SYH2gk>Nk;CP0}v&-u2906Fu}6kIb?XH%Yq8;-;$VLz^KO&fP?-U<_YXaSc!Vt3?%Da)rplp(q6wtFukL@706$F@Yrw*D+fkM{O0 zGR!zK-}IY^kTX&U{cO!8pb*{(}av-y9eYrvc`W@w96gGp=2JA zaiT&Fa*Q$HZ>i;Ezw`Bhu%Iw1Yln!lKw8LSj%jX;BlzdQ{b-pq_zs)V4avbtY z874~va$9zmnWPZ!bvEmHbJ&94uWwz&*?wsm8KajZt5RD)rq6%k-LLa|`1C)@eYjc~ z_o)4!wpT|nga3apThR(yLXeQ4-~tr$NhD6ztRG4ApCzQ~Bz32p#wedulPQ4{bM&&& zvFLv-w5%f*{51cEE65bQ`6%8^z{fUkJ*!H@Qh@Os<-@h|!27_W%qeY74Lb>54w_s6 z9mHlGny8(q-d-K$dB!4mpY8RxWNxV2bc#KJRoGHHi_Nm|--Y*oe^WRE_m8u1KR${2 zY&~C*_Pu%{`6431+s?2&SF;}c%=|K7DhWkNUOqQc;+5ILTy0%(v6;6yo<$FHemcjm zVx_I#s7h<0SNKcuit(A|w#z#_8TU_DLIO;|6I%gH9bbOmU3f3=xd{ptuV9P+x$*Ol zW|?|VweViiqbQVZRM{6BBuJ5|Q8}`-i(G{q_9gzS{T<1#0gdW8?38xn!Gfxmb4t(W7oj__H&?b<*toLv`JzncErB*`5ronIu4H_rRmx)G@Lv^}-X0|OP zad%W+S8KN_M!d^cq$29@m}hB1F1)IplddNYg<@zhP8e&f=-DU;`2#~zk*1$t2fm6I z14FH1IOaw%eM`$is=#0vv{|03Co#mN^dh3ZMjd;|sW9QQJbVL9r{Z5I98E{q6)f!e zfK0!7^uu0UMvQ(G-Ti~-Tpc4A4YqWjX`$QHA#>Fj)Voa?`NEr#`?Z zHZOZcwoc{IwA)lX{Xa)Hrf+ygMj~5uke2N)>SqmApmu#g zB(qYHcbiUs=fPXm>@K&+Mww9F&0r%LTH5yog-7fql&~ISEU6p$E#jj|w|b{MBqzOk zzTsnIkt)`Tk`R@42SQ+F#ZRq!>(((Bje`&!hhp^(ohPr?j8er4!5{E z1nxn)Vy_g3^FdfyZ=f%pi2a}LRxHOdhOU@FdXI=dzS}^eSpO9s2t$wbx+9fC5P@6M zl^DJXLqkPH9Sa3AB^XC{rn^6To^Aem`v=&pPPK-;n(+b$3{PTG>$t_>&YGx7G(1yN z@YzR~U%5AhEnJznS;-(K;+cS_$tnGRRSd`RLyVmQDj(NlXu(Pvtq}L)q9K8w^w>DGg3NTmL$;p*z}m$~n?ZIkhE-pz1) z8z>VV$%+?3!I78;7YIl&asubYiygFy*S9Viybbfnwt9$y$SgqU*7 zZJ&p3np#l>{-blELfN8sq7|H!{TM?B%xmaLUz3H(b14Lx?0XR#$p(C5-5$Hk`YoXv}vQk-M5xx%$Ob>^*Am&ccw)xgy-B zj84%5jd?QmR@qcDqIRS?UEgvAg`4MZ1yaa1#lW?;`uz}5z?1|1I@cQ?+ePyk9G#v4 z%1<6bDSk87&sby9e*7_?JT&LpT36XJHQvfMjWN&1S(lsNG!8pRZl!9!lNMl0E!x-( zmrrbgT<6^>A1k47Vg}ceSU$&*#wWBup++)05lR`8h7c0%s#Zd(@zYvvCx;)O3<&m? z1#hzgT++yt^m~5#9sf$n^tH~@w|I|v75ruc<=hJ8dZG-#fpNo@V+TZ7$zmg%XA3jj z*Y{bxY^9EmMhMV#WcDdT9c@{WMYbASSGkL2_Epw3%y;00!5cJskE=-3m9RI#Se`MO^iGCve%6hkDmmMuzgy4Iz*l> z&Uiibv@nK>%O@o&t4O5^A+k|mDN+NJUKi9jQ*EwLV%nUR@^b}WU_T5x8J6YwsI1Po ziP-KV%*((fqQ@X1Rk;3f_?e~Orcm;g9IMfyiMnP4v1XSm_1##*(fuMhlc9j0+1&z| zb)$*%F;pQZ?gP>e9{;oSGb2%LQJ8LvMax!PZpJO@5M;9Ab>Wa^X#I!b3A+g9^pmx* zX_?lAW&HitUdq(CF*|cB55=ibSuO4fMcf8X16xp3G5dzC(rwe^AN>SRCAaO_-b>4@ z13ix`%HUcrj+aFEoHbv_HNaH~wC1YK3dkB(2xlo{;E&IrV4y^&4*Vev8UYZGOvI+R zEGJ3ErFOa+tAxLh)5 z*v$&8MGcvw!>82Kjv(ooB1@}nxuy17>om}JzZO}l8T9Cxrnt1jY1N7qz|N1^<+9Wm z)&FqyI1wzcrqt49mbMRXjCC+Os`VN_O>y-ez3^H%Sx9*};R%Ck^YGWg70NkodQ|`ZX>}<4tJ0C}CVULU z;m03Uca*OBtla&%zUE&EVt0}|J{@^`<}0EIh9vH8Pv#?Hue|XP+eBkOeqaFQNC^nG zJ_aK7qmRBGJAMP8xeghXRqieB<6Qsyb?U93>4R5dAdLIgW8XLWZr4;s0j%KeFc1a? zMh5oEc$s{fHcy-7_?S>)~y=Q9WU2mKT3OkxOTufJ8yR6?l5`#-e zaId^6@$vS}CdLhf62T#8^ua5g+pO2iXvE8D#LLJQ`8%GjsR)LhfxA2!{Qk`g?9$J? zuwP>LN%hqcmkxypY(nek+eVe{WD5TV#1R!1YgRzHi~Ft#uO=#t8;gBl0w0ybB*?UI z(LK?oCk!R|d8+TNwvaun)puQ>b1Zp9rgkOO|P{=>?7% zww}gxG9(N{nfuYgONVAp!{y5u2wW&;9auS<$MN9ZN|7z~%d zA~8)Nl_|85jDZyLGnCW77p%bFh&i+s!dc_=^cP3d8@va?I*tb-H@mOC$6R_uUoH&@ zUx3GN!!0%X%rRQa=nrkm496*W)Y=zQVbO~(TlTpR-%C>Pa?OWrt}!GdH~)l<`T5 z6;N3FbNlU)veWm|;(QeW%V|+!FPdN*X~4$5R5;zD(8g~)JZj`;zZUNIoZDdRq6Q6m z6YQQC%Q$k4Qm!a5BNPr+8FQVDHeI(f9a~3{({XI@e9_k<6zVkl==4h5=u1+%WnQ~H zky}^%BQ9kTR@0b4x+)pEEE)PL^SG=;(}Hv{R&&jsBdRd^|FdTJi)swRov{Y0nKg_d98g;kx8pE^2kivs^kDMH{j zb`?sn5wo(hZ`{RafGkE+PNuUT6OK-xxPiDtR@T-n@vWO2`rpriaRwqgczg^9Ec?K> z-1lGY8q<3T`T{xI(E6AlbJrd%rMqM zMRWslH<6ay%}bCKvAVEK7}3u0R0Oo$i7RCcpJU(~ptcP}$^N@LWeULZ_C_#|K2ix- zfh*#T;VijdY!bLL;f>)Bae3Z+i6m|g-cCQ-g{ZvD?Tjo(mr}9n8Rg`MS9(J9zZIc0 zy5TVp)pLn24Vqg8+Wov$-pq$4U?Q}s1*vwg{RbQC1bC;0I-*`s+ZcO36a8^Bu^$_Y z>-ZYit+1`}-xuC>dl9L|Oq|ASkcU~|($~k#nTfB?M_DCD8&{M~Q&w~l^{3RL28}F$L&2r^V3r94T2g)&u?v*vC<>QdOa%iU~J?`rZ*>&HEM8ZHV<^%oihH%p^0$W7XBt*W7as+IQn)Y=F5 zw?qhh8y0K!e!Jxnu)DTYwS?LMYib&>7VO1ZWyR1yzm|lNFUKj(0{eeL%a)#R=zir?R79=o-D^Xd2H8&SWR<{74-G>FP)WL6zrrY9K> zgBwfY(7KS0eqm)=mH0_UT_&DLj-`=bgJU|LudVj}tm^e?wtd_D(^!(q-S@9YyVmo4 z|8f@@H+VjTrVI1DhE>Q=AI%Mp1Gm%pj&t*mH2YhBVh)y>I#%(2Yf2KFIJq2;GAtdP zB>p*3b~>g@jVuAF$OZNJKX&J5fS*g>}wgvYtqar5rdGBvf!+x-hJ~{pD7A3X3>O-B8`Lp`3Ys&sk7WexZJhvfBt@@w6h3Dxh`niwDE3dZA4 zKTbc+5%QrkC(oJXTEb`$nieizN5`hFgnswE!&x@9p z>R*PuT~?M&80|uCs^|Uf_xdJ{374F$L4MXCRSy;GhiI2c_QC7L0ydo=TwaNF>S?2H z;-a0^D@tRXGryL^3WReiAlFTTe}v6?Gt)|7Vup^W)T4&TFRoOC5~bwHR>pH^lCbwk zHk-UR5Nt2tfE2L3{Px+n zKV{n4&`mBU=uFD?yLS)n5gme6@RZ9<7^4^tJ*j|SR>Ci?Pk>E!@bW zo{#8Lxz0)YM9E$KN@+jb)@4=trIqd0)OPYazp0GG3TbJraYL?f>r4$|U38bbTqj7; zD`sJzPx4(8G_g9ySiu#bTBx(cU0Lx_>;BbXdqZU%ARh^Up@zN}vk1c!HF1DuVIyAm zX&aXU)WOzy-T!uTG`07tkhorwA%?`LM@3~Bh%>>mXp$w_Lr&JBHJeCVP6yG; z7$DKJsD*wf9XFbxd`Rs|!aB~?jsIsc(8);l53zkbH*-8^=42ahCmZ)ORcyqoD;|@l z80AChm*~qTMiiw!#JFVJ+lLd4_2nj6!jN|}kV#d_g3w5C2w@Oh9YhuiQGPE=hLS9% z3^)Yz5VDu%TLKUDQ1yyXkaz_1b&W|>IhK^sCO_D2+IG~OiGPW@_!keN@#)8f_E z?3+8Gfm@x}r6#K7){x2ZUJ%f3wna_>S6#w_;nu zCHL@sH|;($-^K5YYetcqRHcOz%!CarK2Y+i`0yUQXIgw%X5xGYz;W>BcbH71h08BJ zs68^5T%{^3y+L}jOYo!Cj2*jt89cXx(ibo3!@VYB@Rk;1!I=rW-@~lZIwgFHLN)|=p=GAHWHIR z_@D1h7#eu(Yb!mRv^SXgTx6W{4DNs8gq4hB?CNIj8fNaNf63h^9=47Wz}m+#dQ=XB z?s~EPy;qre&>n3^iK?kfa<0z*4P+$fQ+hZG@7j6)IgKtXYe|c5AustX#<&lA_JZVd z_*t&WWy|yOF4q@ZdYl16kb3jT;Sq=L;$LMa5TgIQR)GlJe|J8PkcC#yw=4AQPS%sl z%Ob$4M914vmcPIS<$wQ=Za*fyH5>ypOAsgf@5JjGm>duT7nkW)@|m5yT^6q=l$i^r zcdn@v10qL8Q{QEKZ>CNcD|Ma@vnH?j8=U&${vY3UCnxnIv+esQM|!QKbJob(voJac zIA(Z8EQ0u3_aY4qsa~*z3rgJEDZ;hk8>bUqY}>;N$^`=2t0*c5`Rj@uF<0O(N+qcBG7OF|cv-Khx;vv~XjP?r^SEJ0|FcsNR6Q_cnGahsd0~I9Y&$id zM7N|C5aX8fjuKT4&91&W_2(it53889qAe4Daq7QaSPOD;Zis!~VP`F-ZVS^KrBQ9L zsc!FUf9|5S+}D81`cBv1D~T7mWSzXMFttA~SJQ%$)Vtytbgi>_Hyip9`pz!%5%fwe zPsJzwl(?yWZRh2DL-mEOMkj2U66z5N?137Q%JnEPcTx$OE+ieNP@@Y)62bmXsF|e{ zluVDYUxS;M4gGnedQ7d>t6UmRMwCPoNFi|uE2ktnD-iyrsFK5x8b+dI7&z8`CCc#n zoUgs@AprKI?LRe5C~6dOVNdB^Auwl8?w+#kTW;E3q+K1i`)=Y6>%=$I%FvyZd9o~A z7f6&1+Ff^ep+&4S!ux|)k!fsEwJM0C_``CjmD23OA0nXl+%Gp5)*I0)rriX5^s;7i z_un7C`jRzh^*0Z}SO!7w>+2wE+{zqk%}a>;WsNrn*f2+4d&8&1^w~PR6JE)W(5eJm zWyWUuF+D$2$ct_UJE-d2&E)sVec|*i)P0XE+r;HgQuAs#&Nh4$2u%(Gn{A4Qn-}jY z$IJ%|G>*gVIB5-+Yw#<_J-VuauSco$A}@cLn|wWpOm~`hNz8SxUlLyA=KGaDKE@1R z`dPn>=gASjTpBKb1aEHvtxXEfx;WU;eSHoxiE_HGy|(CxGW-=;#0Rwo52E8rS@0`N)sYbO5VE zMtzkoPqnmm>lWAlx_4v#>hsEyC1};>Gg_4-Dm8?LhQA) z|DwqbTa!SANUG=AP0OR8!FiVWld*?ylqr%D*SYl?oxD+-TV-yW!JJu*s1qVW`&252k4WK{RO#Aq7~0Bfx7&o(aKxel!zi5eYNb z^R)t_emZtx{03psQT?1ul+YWJRx9Ai!H$@_6|;JpR$uM^)=e1~fHS<5bl$*a@Or(D z@!^1_mX&h*Z-SL>d_DI+nO)c$RA%e^hq<%c2|JSC=T3u*P(nMhozdHgzSQA~eO#s& z2V}Lj8NWRs6Yij9;1m4Qo?1sJ@J~le!{;mdVG(MVi}-m$`)cDT>T9cXyAp1T(BsL{ ztPpwU#dZ^*cgsUZG&Uxio^*coxCt{v`mmLiRx;DZx@&So?0C%P6X0qGIR%izs~o4otMRHtYyDo9R58z;-UPn?|^o70Ln#f zk#;B7uVUP~oyrL3v#|e*t+xz{s{yt}zo5Z`1}C@$f(}jy1Hs+h-Q5}7CAd4mEx1E) zhY;M|T?U7@bL;)Mx6Ya06g5?QdUyBgC5}~p-IFmme|>4er(rh`UmYmv;9cp6cO^*E zWk20w0P6sL{_su7B^b*+ns51YRC3_ zktpy3Tw-wu3P*`ZATFgOL0O`9IlXSc&YD7YozHAmCu9c5kf&Ye6y+g9J~H5;PG`ad zg3>F-4hfYv&7%-H++(F*B*0*0UfpTFP{%7_2ujt60HxM~qc+TX%Da;(rmOqe+`j1AB z4u{riC)+5)*B&_Y0(uI8KmI~OpYg7JL_XsCo@defoxj6t#=`V2@$>WR=DB!jZguAI zd90uGGVb-2-imkT2dV8y6vxj{B4Aravz~qjiayk5-?a(D0MWka%;-dDeaS zRHEbrFb;ysPzT`ggC zs#*>S2UfM%NI+=!!F4s&nka@Rc-%q{GhGu778Z84rf|fDwqB$3? zxf~)dyML8~t3)HMPjX>X_$7#?Glb(u-fdL1pbW??AlbM({-SBBb8hGd)7CO62EUKt zMroke^16FNqsRE_dK*!fgjOz-hS1Q*tt)<+w0an3jVSt$+hwBe8t|IR{zIeWEa7F} z>HpC6YyK1|j(7aB7&eO>_nm%Pcneus(SOugJr0kFesB~IJ3_KTeeg^#xk zZbdQwZEdSKz4Kx2xV+tU`7%()D71oB`?x22*1%0?!6v5FZ!W3@4M&Zb@R+mjTX+f2 zoLLlE-<`a1lTZgBYGr-+r>HeUzc~fP9E2O zbtI?O`Q8X9joC^zk4FuTG1tKWH&16a4Qk2T)Wb8)R-*f<)azeik>j;t1vFbMqMk<- z&{-f^AnerYs$*IG=?6lJ^gKO^y$U%@_{256a{ln7xc;=Gjl*n2sNg)dyALSfcdj(e z_7l;0KD*6Q{Xbm)A81L99Fgy>m#5Shf<|;tdJ_BTUf#d1rKfGLwjG!a0i+U=GFz^u z=Q6AE51QQVHQ57;iHW5!>x|sxB5S+%89M0ctuOAkR#Zft2ex)!xXeov{69Bq|MS7w zpQDF=i(^UAG&Mm!n+P(&Rm#y)4$AohJB!`{7)i%r6eOx)jHa&_zN;B_)-jLSu}`j3 zy1*_ciO^USkTFdT0sP`Q z(}Q@vKq&3DrkEX=GB!=_L8u%glpjCs{hl5`JL%z4GB5L}o68Q^@8z|rdsMnl^cgVA z;y(c-qG5fyr*|g!RQ==%JT!AcEs}>(Z_To=(#zn&eF>U-MdyR?At5d2DMAwS2#i`p zJBfQu0*Kf2dK?8`ogfEEeNlia%~bjZ4Jtfm-i}Bfos91RIIYgw;wtKlpAVQEftfLu z>~zF(N`e4+#ry-up-dc!{+ePf(_tM;C*Ht(-#wus6<;4yh z$``FE!HTQVfQuYep~$M0FaA8mfb3k>aMWqN~XII;gel!To%|emz|sqEk4J<3MDn9c?yK z8p~a_n4$n8H6m_Sz`n?P*)#7-JF5Qa|6f)nOFi>3xFxmD@P?R7(t$H?aroagAn|Xv z`xwUoB1#0P#Dbxx`k@UoVUS{3As;A(q+I4N z8f0h??3ldCw|oAw!eQAYa+1-r+TeirMBtBVl7bC$Q^4lTR^O3)okuhBi*3*QGeGE> zNDu49ZhbgAgjbv%MWl1-y2XWEL6Od?OI3a!o+cfVKYH!2ho~mf#9A`_>}+&&ve{{o4l{o&XB5Q3B81- zl7A!BVm6h3mNfj5lK-6YR6MF?st{u;%Z%N)?90O?v<2HS;W{JXI@I*%{>~`SuGlEE z5(IHLCa7t5EO=+qVE!i6LJYMS%(o*H7r!OUmUvY-~GbKK71iNKG)?qUwHJpMC9^5jx1Yd?3*vcv)rj;8J5-t z5v5F0N9P}jMP@a&eg%x|~2X)|`b z$*PRsRUfOrMe7MP7{0A*y{zicJ~oMA*u;`rqugrT7V3k5-Ru6i zp)!B;%SPk^(N2QTK_QmFaZo;xO@OmW;|wsz8E&7ms88jU35eB1Ku)HYMvklnBFnn`nLG z>DZP*y~&p3AL4O%6{+`Z2vmc&UZK)&&UtNi5NP1e9rBMAHL-Vd__wxzKy9#6{w>g_uuFiP2~HYVt+ehvWqT!>dL+6(Q*+L_tfGxn zFLRgj^X}$+0yE!98d(FE3D&RuU-#WWK5sF{)b59U)krG=>{qqR3GX(*-$u2k2c(l-9hJgph5$@s-?)~Iqm8{SGgHd>N@r<(q{``U zl&ut+8+G7`)@kWxlAcq90DcZN>x>8*h(=tB%~%Rf%Scda z#wt4DR9W9^rHk>%3wn@OjM1?S-8(*pbXsZ5hdf!5z4qL7!a#0$51Ez%j;At3BU;k8 z(g4p#O+gtowz_<4RZ)Vr%qqsTD*E&)`fQcb%r|(KGZLx3JBY+>ft}?%bDNFXb`zwo zcyINqHP20Bw6kjn#z_>}k}Qd3ELp8$X!;>6T6myVd?-i(QMDhMM&a|w+T@!rl4un~ zbeC$Roo}nu*E>LbSRM_XRd{i6&f(D_OIOJc7i8aM9ncO*m6}A5_CB1+xy&nqGsj0s zmaa~7Bs4_#X>_n%(Qqna!wJ$EmL(qvaX9|lvFB{3)9R+OR2Sk<2}7N1d!72%c}2mk zqABm`&cy}e3;maDq>r+CQ4&b%Q9WCbQ}CM_!Q&TiI^Tg%OB(M3Vd8P}3x3pb>Kbp( zOh9tsdqBD+H>pPE<}5-Sz1d+^AkgrKbP9#bbbgQ+fmwshQ*}aIam2Q$O^q6eZ1OHS z{!;?%R&@+pEKz5i(e0&Q*E-S#XI@iMe4odcuaFiwHAUZ{NA2@uIkpIGS?8HI3f_bq z*9dh_-ql4TGB7aQzqSH=EA^UMfx$4fWI(>5Q+#Es&W@`r^4z_tcM zG7`3ye=awfw(~-yOXvyAO}&#j7PGG!SOZlbYl0thlL%KhLHD)WkXBIoSp}6Tn410D zH)7^s1%e=)Z)CT5_&23KbWex1f{8k&K6D7a+A-aZEJl>dCEu?b)S0!}oG?4PwO#i4 zAU1rgH&~*ABqY|6=p4o1M&&T}E`SPL!$eZ=&jW@%m8fNsXmk|B8Cyz24XB-}Xe0i* z)X$tIM8suKlElMqa3BU4Yj`%Zvrlu93r#xaV#DXHKr1ldMGGi(q)QS^A!fNrNQ?25 zHl+oPeKnllK_Wi44tkWmq_7q;?pg~8C3SyX@TdLiUj34_UiEgGjXT-oQ|+`pe;lm`dbxNM>y-6AhQ@o@Ci>iZUh2$8 zo_8uZuG?H8o&%55)_t+b#pP>fX(_av@96w(Q7QIq-fFJ{vh@7A)>!c5CK{3*>6L>H znZo4J4<|9tN9UXlB~%Q}_)EsYf(ZKaG^YsE+g%3b%K&Sv@0dwgtBkWANgV`jwJYMN z9I)Bepk4yZLud7*-o4{xh0ui!61x#lp!+Q7_WtDEDhY47(IAryoNLY%Nt79@isieJ zwqx353u;pvAkJmuuo7*Vu~)56cU7fb9Y`z&?N-S&TUyY5C>3gfrVF2|#5Qw1o-%!@ z7qq{-HI(*_R{izE51Bk+NZ}1v((hDl~pQJMV)+>Fm z^P{HeGcvLR=GyZ%N=ib{%<551@ouB!6Id;?!k<@)d zuYR?8jJI2E7p({7f2?5CGQ3%jqW~JY&h@(e_>;w7LP3Qbp)e0e%$R!wKF+655GQ_{ zAsjCONRuPHw;WAw8^*|vLHjDeJB4#?BLUA<3?`cXjRqhaN|d(*Q*0j-!`OC`samLU z7czm^v!-l9TVf1OT3DH=Y9wh0&kPLPf;5>$45=+2^cB0{ch1Xg6Or$QOs4#JU)}>R zXAKJrRcM|e4s-7hHdJQOO+#WP7%c4JJ>nNj4VyGfIrlucvbJ!7=T`mtW-`n*ItBo!V^jZfPo!TY2JF6b6E(*tqnK zdOP|%i0WwqD&S@Qxi8VYPS+g_%-Y`IVy;;CHt?|N% zO@l1`eZ8VD3U<9oj4O>8FZOSAhAF%|E&6FmL@n$*RbJz~fN|wqdQ5AB1*7-s*qwVp zP4uFT#vxjN$*o{G=xin7g|u|it!01Ujo3w_H4Jo|)vUv{;dx<55p5kuA-H)M{5 z-1rukT~4MZ63A*6@dT$uJ(^f|K$ay*0>w9oXyLuL;df1J=kvvD>*?Ih{({ZPyv@A6 zHK5jY{y8grm!n>0*jc?4e!y{H+sEbCnx;JQ5o_?%*q_%{cZklIQB*6cLoEFty%c@7 z>zM)VqLNHLL9%EC z$2MaD04vgVHf0c%+9q^5g(?~j1U3WV0)a>KSaJmK1BSBVb zpUb??;GNs5=>Ac8weMNW{ynL+zf@KrBF2?ZsVoT)FAg-?e?`vyMMp=+JBK%0fguS< zt^)YXD1L+68bF>Va8JMB*>k+j(*Tm~{*9-T6F{5&h#vaZ{{S#z7}p2g$1pu z(KTI~W_9%?R*baO>~M<>je*8*{>KM%Ry90iDoO;@uqd>W+@Of};#DDqgd$NTds`Q? z8FRiK8&gWZw!LK08$>!X6n{r==iR1tFG=ga{NrQRkdv5;!yo`{?w-51j<^qK>H0NpY%miaCGMUoJ-UMAUIktSKB>#OV zE?n7v{dwY;--ZmDKZy3>QwT5Osyi3?7YCss|H^^)CRXQE=o#=OhDpF`elK>-%8lWF zB`+?L>&T>bH{pZB=)R64PRT#PeyXcKin7S19bt=4uXmPv?> z1c=;<%~%5NGg2+jrk$Y}_pY5}-1x}_O*??|8cWfb55p}6B8H}4Xq4vRSq_cdFC`eO zO?NSgo(;j}NQPz`4VlrO|NY~yQ*eIH-PvVTSnbo?WYawF+TLY!ve7uSNoxozH-MNO zfgwmhwt~qsjY)J8hSAU;V(nnZcxof#URmE1OlmES%0Y#2j17A$0TMe(qs&O7Br3g| z?^~~qBq>3;BM{ShWK9uVGm|CNCOI$v`FMo&G(YvEHu!i<(Xn^X*?R9kd+AdS%)hLm zf^$}~r0N;`H8E#805?X_I`dQg-^_~{8ZR|!{=MeE9~y}AWwCdwB6-G*Rv+zS$@eNu z<|$o`X7XYetn#5{=5y|?Wpr|!yH$Ca2%mtcXV#~P5+9G1h5(tFU;JAYJcld@>#QVB zGqK!!zIDfO=g*=?A|&Kx!?$Xpjy*=#*iNFA zf&wGx_S}(tpQH{e<$~|ESeL@urVx|| zA7=TkD${Ge^5M`)|&uK)zf2zQU(D=9Zi?K25iG{DC!<4Yxj3kn6-u zB+*cW+z>-ObR_=|K(%QsH9cSJU^gAxcxpS#ue0Cm0nUSw03dWvEU&3KayQE>>itjg zgCBB!iSTdSZlB_)1n%;JUfyf~D*JTam2-Y=Z}3UtN=ok$Yn_te!UG5a&jZ~3tHi0c zyrSR*QBEiy0Rs2i<8$W)(C5kaf{iV0kxBn2G-M&0MRCib*)X$1xggYw5UK2}i~@X+*+=*ARA#OzEC=th=Txd~=kiY$Rg$BeOO&L3ge) z<}Ad~JqvOqRfgXx{g%zB3`wOphB!&x_JXLpj>BLfG#zpn6Rp&UI+gse2&-+RcUlqT z6vVg|GlnpXekwdQhU6nM;U(bR$jf}{FcCF2H+r_a!F2}6ZRxI9rku$0mzalaWQ+zS zLB?%F5_28^#P5eco^u{bhEodGO!MOe(8m#ZSH6t7*q^5};%YQANyyGrFTk)aKyonj zAN5iavaJ+C>jKqK4BVoxtKtcI@3P`=bSS>z8eY{mTQRnJCeC&Nf+c{uKpjBhoGXB7 zEr;$_7_zLC2ZG`+fDOgiHmyvbqocaWckN^om==v;lC2k_>Em?*c)lOz6%|%f?;Mful9nvrM;bBt3`}!CKg%^M6C<ld94gFY?$`ON8Ux zpDNw~rIgz|)?z`+`N7qOdPMHOHLD-aSO+pqT~VIc_SgopLYe`F>6~A!yqe02lBoZo zh)jLPY9;T{e{39!YFP(51TOr7b9MY%RRRWj0#|E~31@S^xwNvEoF`N2Q5vo+A}`y2 zYL+4}f7DGthqEc?ZEKW*Ep*L}eb>pg^5y%$tw}jp(KY(<%CO z!)Kz7gQvW4nB)VW6ok7^6eT|?5-W2>%F`h-WZecw1VnS{9r+rK4tLrNdVxqG#WYJg zxW}biys$*{@ndk7F_1}D2+L*Y)ykAcmI*4*UVF&42ZbN-5D=YTLNb=!7ae`F{i<^egre{-mOFb`MQg(X0Ib2%O z?Cd%y3IJrd7Xk|~IT+R;qII=n90hfCz$AF+&K(xGNnS|Y+}w<1@ui$&53v(**|zh) zCR@9^-&eJrr3-k0{?+xvHc;1}UtV4UuwJ|!W>wS#?l%I5jjtdAE$8jmvbMI&>w{ju zQv;U$_5PPW4{&$Th}Y~ug5EC;Cb)zxgmL8~ecXxxzX82fiDC#qyF~gN+Ff*9Zs*9y zM>(j+ziEZ$#q|@0NG^CT2f2cXc0M*CWBQeepw^dv;eDCN@T2v@?a64J_kjjEkwxw9 zJ>snxhDO28*~ahHYQdJR-)--+4BzS2{ulqH4>ulJ8!2UEBm!Cg=leiWmPFWIln=(_ z0Xw<&q{#b5@9E|!0k`k~gcc3#2y%FCO+U&9-Ug9qJc*rps}FigBpONG>Uo0&visl88UelTs? z?T;NomF>lN>l0)cngxy(mB^V_zU&uqG_by^4j{H0Pp`)#smldxE=JXbiZvEK=Ncwz zSxBJzkF0Q5NYv&0nY1-fvyPe{Ky6@MgC=&dgI#|~Jn=0uUH0OEZiK81r@mt>`5}?i zCFjOq5z{ZhO8v= zHlqNb$aN(zs(rjmJ?PXd>WVq5j=oK!>=;-Obn=GiG*9B3at%V~2Nq5{8ZJ9kl;12z zl*9mZr)sHrRNBjOQJ&OjMx!?*!y<9jHe%rxb4q4j++$2Ts(vulo(P+i1wyx571Q!f zPrG$qx&=*v#eXkC#LmUEi@c$lcTXE+s)y%5Xw-2mwx)yaZ3fIA*e{T!=F7qP)&C}$ zzGQUZ50{Jd6P5@CN;+He#Ei;ohv++^GWIBd%#JEBSwE!L;Dl0VF5={bhY6t|LAOMZ z7o-w9Jim2&k|TVyWGO{RqKM#EjvHKa)~x@JDWf3J^zGt!N$;zcDAbwMtud-yiR&?& zl;a*=#?Q(;g?_WjBt^l?#r<3oIQwzRJx;MRIW6LsW~&H=LuaTxqomzr49|lw1YWZ- z@26f*Ltib}1)WJgXZfd0DD_ehzlxAWF!YOC2o`-1c-jGV=2$AB^|Gi9wTmt{+3kDP zzzdQ+-&R999A8^sqq){LyxQOWTe&}SfIzYtpy+d_A=>Nx$*#;tqIT$#m5GUoH%3|a ze$?n%=JG@|J&jrwC$auL_4REt|X-#ErNAC*EDnvV-(TFr0Yld0X;d7CJ}@tQa)wK zGprW>g|Pj4U9~{N)!@3OBU3i)s9C-!3gfS7$3p1>234@5v?MJ`2SW;|z<& z@yjycZgkVVGl=Lp5qzNQvz-*p(}rOdl5+a7^$;ol=XNQ--`u~|IHuKo;C7*1?EG)% zV_!k2S7Wq?TP)fsKaV~NhK6|;r^k0cV62!33h*;+QBAL>8nR|wRYjPI&{W)4%LiBo zOe7~8ggY=~$cg4PMwwQj8K?mSqS*e87$zlM0<1I1H!elbIufAHs-PzJ!}VnJ6X^%H z!e>5qe;w;-e_F+z7@QUXl7%<}?3Ib&)k;QDR~nzwC=B&K92O)PjkOX9$<_@t9}?d& z?t_7nlS;Q9uOa)5!SJ2^omPmo+*945e!@S1uyO`d>e(K}r7)PZh6K~P|iz)TX;J@&Ju^-c7fCJ+y&>G^m#8XT%!8)*-sRDYRp%Hu<@RY_p^*3UWf z@`QT+NEcoLGkL^Ez<@Zp@S zL1+y5%|z=^C+!Nv(*0R$_lGgBChx~kE%m6p!n9;*8Bd^c< zU%zNKUm(w~z~n?Q{th*5*-h30#Q6-JgIXJUaTys;fbajMQr29CZEN^>L8rcUA>-S$ z|NqSl8r*yecadOBwM)~BhxMAd_5TWvi@m4alAj$k<+BM-+a-q0_O{(NS-#1ZWuWaTzo9$ z1Ab<#@Iy)22i2Wk6Zo3^A39>n-5dt;g6O+en7_WqRnzMZZ&)rD1cp#?OMkou#|vf8r}eCcI<#}=5#^^?u|8B`f1VVgZy#m7010n z*9s0DO~X&&(?fYNhJ|d@de%ZF0&6J@w?bhX>5`;lCo3Z^M+>RP<%8dIefoUcr_vs6 z(`%>n=Z*DtgWL}yOHJ(CkS&~t9qu7!SE+%n1f5zQ+yxJYW+(m{*r1g-sGZaUZ$c+1 zmv4A3u`75OT5}6B69?2Z={Jp2u;bz6QcHGHb%Wb=K#kjM`%j=Q#;N2Bg`hVmifL|z zW-RU&V!#v5ERWQjKQ3YEqMSAun`ywilmadSP?MhJo>#|#Wthjy+DlMz2w?&k?Iv#g zW)p2DuVLml=gjM@@*6{Ibuu*757mXhqKOqBtDQ5 z<`oSTd;7ff?VMtVi$g5W9s(%g$Ry>lq>bT7Q=+BJ-1Sf$v+heQ^mF8*+LVy<+*F5W z>wNbOL4ps>MCg$bKyU}xJ2OgvUS$#=7Po-$aII{n)3T_G!4Q(U>JOa>K6UA}%K)EO zkhs)GwYqk8I(dvYC9eeRDC3RmR?s!0C+HQcd_WE-q*pZR=T?m}aKuUYq)r~NOXz4H^fbzb%EP)<0QssAeC;BSuE1~h z%Ml9emMGA&D>zA%A?9fC5 zQJ%?l_ok0ymo`lD7Q^NEyv3uZj`!E;qV#j`eLinUzYIKv;Qc4y-h1J(wY>fcTAx3e zx%_u7I=}qKop|e6>+nKBL3t32=D!Af=&pT8nW>6pv#=N-@UQ3EJpTgoZjSZdc-Oa? zF2YxQpH2k?1ahGDswyg%J{upSA{`d>6#s8U%00LD*I&#Q`Eh?}-84GgQ4~rU`;`?% z=zDW1a2`M2{)_eOHC=OZJ3Q`U%X#0yeY6hpEEGIkcVwuc-_{#6F&b&2mYK^bMOaNA zC%|sKeJ1B(f4wy#OU2$v76IU}yvn$|s-2-oO2q8PeY?2^yT^g1t|(>(t=yrEXo-)i zWre&y#UpVn9e$yw)c(m;guyakU@gKakVJZN01&$GyM!Mus_>GO4W}iWbA?&E@~xpR zL+#bNUqJlzH}s={Z_nfDa) zGTb8DCOEJX$a%i_BRTy84M2-2?_e)?hA zNO{`iesa!*H~x{32Ue90S&DxJg+y0#zBAPrFG>Oen{aP6@$FnS6|I)cB)qf~*NSH3 zDP7#1o&(V?!N=Q(bJK`(RM5(zCrkP!e+ME0pkcF3GlN%tnl;IH0J;;LW3E2v;r7$b zFF~IQHMu*YLm!&fb?LjbLtUp8K!jqoAfKJyT+F+ti|N7?$(N!uVS>}<{5f`+EOwF1 zY#uN?I)6th=5gi&adtD(vYGw4npPRF&BA#@1@~U!J=}XZtoIAa^)mgUs$aBJw11}_ z$qU1L$priD*bBdZ_u1k}((&6;Cu570R?%72&b7c~g|YPT(;jd%zj&^GM{*U41-b?jJy`?og=}N%qx{mIrJ{&qi+sKMXU+FJ?FTfPXeQXUiQf(EZz_(ZL=3#MY1zZ%qKS$)MA|w#(hBDcxa`N+gZw7y?eF@7z>i~>H ztPwN*Hc6P)rJZu}c`emjU@u?pxziQDH38;#3)$b3U4FxVMxXHb2%bL)KJ(qfPR$4S|>@LVUQ zjKqfMX1=%0v2Rj%O*XC}-&}vsKMsav8cqwzOYGv=C17}L5U&rvvK!zhI@HG|v5bu3 z2+=te(YG3Fy1u_qiosaAvwo0y@F}lKDf}|22e1|$HP)~+E)@ylb!o{rfAZ|bJ=J)Jw_3Y+9D;iXx4#!`x_lZjV3`SAKnQZ%Be-Ts8mVNIbVYaGs-le)- zYfEy+k9{eC3%!YIyD?}&z%c)WMDlk)Oo^XEl;M{CEDhh}gV~S}yo-pu6x=HkMiviS zKX_YOL7QPlf;BLWum$1hhd=S(xB}iHT>1n{!Wppc;ryV_W6=7K3nJPVfRY8CE{|=y zMT&Kpp65I~g$ZmXYGyw?6(m^T#a&=KJeLr5>$TCBVuKUlSwgegd~}&9J!pv5`6 zw0tjzcq%nJ#5n-V@-=ZYFDqQgjF=|bl?JS{VX|VE>+LyRZgeGZJN3a2kWo3bs#*N! zib?IQ$8z|WjUPuFr5V|$XYiAP9U^EY$Y;)vq+$?bMgj5o;3Y$UQob$SRrwD(r-{-Q z9=xp*7P|U!GI2%ET`9#x1kzJ$YF#{`qSDjM&X06_=Y5QuKl!~lvaMd zpS;Vg-an}xzW4d#UQ(DNwP8qT#NQ9YSRtXznB(f7wGlpl`}@V~|cDPZ_?IKqm^Lo)eEabBrq!nWXdc^Ed%&(+#xm_+cILO z%dgGbzsM>&wNt%-pRqE<(6c7KCn3go>wcA%J+aYKGN^p1(S~SwvT0G?*Z1{zAmSoA zsMs!TBFC5mP^R5IEk=U|!8|WtIYA)T(}ww4frQVIllu_pqM5Z^KY=O>{=e;fTtPwh z?W2EEm(_fgJ@**YGE1O;`?9Errb_v~g)&VV+Jo}tc^Mno*4e_s!dkB{?GJ(W{(jK{ zI!p1j0x`yJUpRJgb;K#(98V>;O#3Yax^h`CtCol>{vjJ~92ZQQ{?fp&+&77NSx43L zxs(bGbF2k=L_K-x`z&rRXY8!Du=?#mE8%xYvz<{-6FsK6I9&R+Zx~=1{5)QGeqGjL zZ(XCg&MLk(q>23u2@rF z;hska5ln9rPi<1xu94PEWM&*m#5GC0s`LA;uJ|KlPd8?V`NQ6qL|mpv-NX2twI52C z-B)L=ZEGIi1aI65N{k~?ErdUOV^7>nt5|O>EC{GC?pw-wjkDuycO-D%K?u?!G<>XYMiet?aAb&`q!sGrQnyGl2)|=d!~L?Mhji7Cu_d+W-uHekQNZ@!x#vc%M?(~~i~@S)Fq)oP5J0M^5Xe9= zi!hT%V}rb+7@ju&ZadipPWrvcv@p z&FI5&Dirhb%Hz6}^gu^a3*zyW<-v|vwKI+*x)MCy>PVXvbu@+wq~~g6>^1{;E@Vf4 zeV!w>4j*%p-$Yy!+%+_yE4aZ@Ktdai@zB)ga5^PAS?q`uBuM!8qHUc%ks=q)@%Ki`uayEib(*+uTM%hJg`|3IHI+p?|1SP z4=SF{U&)v6>NIBW@9$^T^v0prHXt}=cGB275hZa(ocA^6&K#No$n7%6dK#9yd`UC3hcnDX->OK}#Bd#%j?;YN{IJ^5>hqg(h2xRKtg#sUdkd zz~hB(ccSZKQ7AC=71c=Fa;^z*c z>3DxYG?z{MSMhbuy(SvHek+@1<*(2~x;cb>)0RS;sjxiSz5!guJuTe4WG*MxQ=Hd9 z;*djwIWx6^`U-W=rNo@33E?BM;D*1#~ zs;#j1(;s^HY0CIY;BZn&L`fsR#+!L_c3YQ(i&l&ktVt1hZ>L42K)NHMeIIGUrUzfvOtB$%Y&mFg1Q60aS zVi?PhNKaA-6G>u61MhBjN+Lw{?8vVu25W)(9?OknU9f0~5Rx3`C>hjl3ugR3?}38@ za|Z|3fBk$a!>)7KwT8#%$+&Kjx$T6l@#FbtiAvQvBAb{{M__0EXAdusc2Ztj8((D6 zSiDtXK3}Dyrt3ky{BEzW6Y!J>{PbS^XfT${%uA)VVsmqI*|shE6p05yje`uiYg?zr z3bzwX;&VGQezKg1>hz$SaQNRQ@L}WU0*p8BH-RQ?r=LQ7lOmC9BG1I+Ux{#yvnRlq z-$*qxX+=?f23TW$m7y6Hiu0hJ9_4Moh-%TxkLe)3bj1SvF3q)8)N(Fnfs0T4H}%h4 zNysBqp&%dsPz24cBja43k?M>a%lNa#=JGUBi`Cf1b*Y%WBnlLLovg{Ip=gW1uP92P=f8( zs^O{R9YBvah3$*BlV>#0SAxkl>6-g95K=p(OkgJ=`x6VlS-;OLNfbzV~1R4=5N50=C+aM*s=(MrvLB%u0epa8QWBV zM|weTcjcc=P))zKjWn#hI(*7a!{65!_nNG$Cag6)-l=5#S*q{kSkX`9STb;E2?aXC z@@oA58JJUWM}O@LyiOhlC2_ zk6g%ud*+ccZ``Jt(K@Snc$?E!)>(L_`FU18tart&HDwc*DH;qm&WKR-~pJ0UX8@ow>$@EtL z^j0wSGY?~sQx%hz^YY#FZfu3;z(u#1-3&Tp#&@Vtz}8makJiUS9XaUil#gYr9Fp`c zW4JHwTyX@HzQBh8A5MzXR>cUc@COLCpUijcrk)|>7 z64b!E5B52(Y+A^UsH3(&f7$&48Fr20`Bhq5Tg$cP-g|so+krocD_Zd9pIh3#9KoBD z$VVXjb369y+4aN341ed_>yx$K-?Tqz$U{7gjj&^f-Z;RX;IP$qSZ??f<06Z~&nf-# z=Dw51SFI7<9iQ5-k-ue~IQc}&U!My%oQ2- z4i{CdB)C)GzjMSHfQ<{iyp2aO=h2&V>|X4h&h>j#{nxG;B?F zRX-mSmBp?v>c3iTPZbkZ7wo4VA7WRX8mP(5{0)3)b%1aY92CK1;V2P-2u?ClU1UN$ zlW5sA~nv~g~F$}NNcybbgX+!^*)N0sJ6>qfE$qx>Pl*|C9 zeC4rZ@Po^7FSAbH&uEYOLOsL@33!g6>MV3~tOC!24@%rz`qCl1v|?>o8$_}n`(@ru zoqgdUJ;;DJs|$xCq{&!t&S&K(WN?Uiy~dJ775?O)Zr(kJSy z4qsa5e$W8$askfzIN~KCbeo8CyD&2Id?=>@ESiCrPv0EA3rFZF8a6Z}td12lMNDM= zE{0c&0*W{G=R5bZgBYW!U4#Zve)71&XiKqNi{2(GoenPWHH=1vzFBh3ic?%YKg?v!4G*>lMLXr=sUTMNOwW`$uMG~eK8taVk344@Q^lPjOS$oMs!Gp=)r3|tTCFduVcB`K$)k1qo z$a{(TkwDF|`Dzlx|AHC+yjd)PoFX2HCxrq`cN03npW9^{Z$j0Izq*@khBT<19^4=5 zz2y=z^}EG?t_Mpe@6Dn;(nVIqN;fpg0)262bi8pNv|lij`A$Z=-;ehYS@7Dy&8_+dA?j z$$r;D9v`jQ>FFNmW0pV%Fj9XGN#D-Pe02dnzXWnlq(0;2a61H?mU2h3rAY(bdv~ip zaxsRZSYtZ)qi$+W4&-)L?d#o&(|QfVp_|IvL(ZGWy3gHs8hhwz%j4N`ycF}pWh{iB zg~i^~%znMoXC1A?$F$cG{eK(kd4a^4Za^mJNYKCuY)GRO8&ZOxcyckKJ9)9+; zW06;j#D~A%qkKl0FYkm*wPXlhYR=3DdhzM|YeS5t*SUc){$2|U6+qMO zd|);Ard7txK{0>G+vS0*Cuc%-a$zxWTv5nIYxaUAv5tyuRPa|_Sstk`n-LY@^|qGc zSipr675vUNH$QG}IzABJcTk%|x@&Z}gJNgfV6UCVbwk4-S_@s&SO;nL$33<(W97ep z6Fl=`&3SUlv&*oj%?vRk;0Uv&q6&H5-|{=R#?g)4!14JLP$63v2L1@8`R=6{!arjN zmrZ;eLhVY9VUix*Esi2gsa-YP22Hd-1?Ah>&g;0}S{?v1;4&1B z8q}FeY@n2-f+$jnoi@dfQt}G;l}Z!0k^qMhdr#4^4u*Xl@ze58(OiptU*tt>PV(7B z#}qLfe?)Vo4|c1w9lgFER=2l5PEJ;?u6x+NuAKOKHXUYbJ9PMpt~eJ;SE7IGP-;G; z9as$e)>-2;e6P;%{D{bP!^xG zC_TLWN+T!pFvZw&w5A=)t?J9UGTQ6Bn=k#36~8n~;dgnCG zwkSf1)+js<4AJkcUBa)^F+P2FiM3V#E=73sU$=h6@O$jWi7x?>U03+p z+@3cP0LU{t)T}q_Cv>xTx!9O%JGEYy8r0z_B$6@5vEUr0jfOygX0LmRMy!6oKF}OV zlk=i^a4C7&_uE!E-q=RwJV5SmhPbna8+ejVCEjpQK#tc}Jz}EpwFgu0+2;9sk6(Uk zqd2pX?HN;8e|K|!|2r$=u1(pFWIt&-QWs}uOYG&`+{C}X5!He(;Y)XP()Gs%l&>ch?je%8C*A zgaMf~2^8#=R-H zD+fRHePjDsglMUp=!Ao?$>k>x_aHdkUfXd=kBFcol8ZycMm4n}-DzP4@`T2W9!@VE zi^&n{M|p0hgNK47k7|LeoYrolC~i}^MJV2B9!z3^GMb}GAM{8~9=um)=1Nr;%!u)V zGn19*oEmOjIionz{!$Q+HlvyNQYDY+^fUjDrZ4f{B5(=WBii^ozf>&i++H# zcpsK{Kg|C26ww%Q(lo#RR=eHt?qgW-Dv!VZZVq?b5Pi!Qa`)rQy0&M16~Mx$T+=6D zQx|_qq~h1aKdB1($^X0;rRwI(_M?nK2H$R0?6#N(gI;8 zzjYl1`_M1Xg`n|uEFQK_q+eQERP+92*R;{rw2tax@D9c20An=}6!Rg4GIQuSTY-|j~to0=Avj>2%11KEG*{$N#^zWzOKEm<9j~!F@X8$HrtV`IzD90h~nthI~~p3 zo-F3fzjK_vJ)fIB3s3q@=y^?`{qMn?uF+QOpi(ZImy7$@;>Yi|4L0T)Vr@!VRD`Qo z-yV?Y%O7;|Dw#}m(&4y>nNOlAd=Bps8z@dUH3D1y1hJveD+&I|!-9@In=1fnPv=F}!A$b_b;^;CtHu|8F0@N>&?|7Z zJloS#dfq*gCV|jxxSuU$J|TwZ^5f3*PGlh7zjzRi7>uWRw8-#G`w=E@Ah{IN?op=3 zw>VX2z<0&BgikZfRCRk3l-4WEY$D5qvYM0x;>|1k+B&hw=jX#-3J7&E>ee-UcQk&^ z;a_T+zfoU@v1My-D1PdIsdxBkEP z@I;Sr@BTzsYrfiVWUg=v>-w$RmnS!XvS)?&*8M7>=e@i8rZ!S#OjIbnc8peAbn0qM zI=l_Y5^V@ScvAu@XfkwN>rS zWY;uhQB>j}X{`kb2};5cRSljoclohQtD>|yy{2MjMx|~4-^x2Fez-ub{JW>xvbvVm3g9<2 za%Vjj@8RhQjM)A7Kbrl)C5}%;5WWEmi;M5uD88SG01QskJF>y`d}Cx}M4F)`2>kfM zPy&3lw|fIbb?ZqjWo3-jMGt7F#$SD{|9i29waQW9s6{3B=5RLCW~St4M! z`$geR-Hn>)N&z=sop$0|-0n~x?n^+6>n6!d6$i&(<0=FHET$$(!b1sYtzroJ*ia8V zLpU7(6e7nUX>)>2$K+VUu(CyKZH{n(i ziR@e~^Rf3WPZtQ1pl5D7eS9oGi#!8qsFiL%WQ&s65Wi-%P$&;Ua`|SLPcTiVua3Ye zr<&8kd;b2VHc`9l?YAEon?By#73v4Nb-bR=aJJ5p3vA#2;Hw|3*(+bZx+d}7Mb4ir zuB|jYZU~%}z1)EB;x>w-&wMF%o{Zgg3sVo4Q>|9}gg_=sexBSkz8rg@6kdkO=D8;- z;aj^hn(J=433s3mQY?oU`G%YdEvnaR0k-?}5v_?A(5X$Lj%(oX7F#yBunwww)9K zp|d|U&jWPHdi2`OTD-PU9u_q3q!!!#@jcQ7!6``>2NmcH9X{?WF_q&T&&fUFzNA(f zj-!#Sp#mN@z1^M8pPrqKiyEgpo@zF3slJ}{F07PPK-Vp9HPWsXQGS)`Q4-QxP?$QQ zmZq>#RZ@d8RN8r!0zkab;R3c@K`j2<2xOIzP}+RZNUAbc11+OnfwC+LHSDU^+TAA` z{m3*TDjTds5N<UXwOheOLex={7L8CZ8LJNbR%McQ z9#g>pSN*)R3AcieM`TYu1JS%MGW*+i+JQj|{MB-`k zyfqHYvwV6~<@-vvI$5tdgKzt%+of7_N)w)PHKji~nvgF&Bf~RU*rO7d=xead{^9N% zeR(zSF*IPe!zq%+Vk5QNf4#fg-=8=-b{|)34sJS8Q@^FjxBx;8G11Yr?ExKDPs@`& z1FzgKtE~?^kr^2oW=NpZ)utFQ9Y17xmPQTr|3cra`tM{0R(KGyT8v@OhLrCo`6>L$=X=)a?4pN*9|e53^=%Ai zmD0@|fi`Q9hSZGU^~{T2=0AWg!d@kexZIO=kwMkWLUZTOtA@A@wV7G5=lbDTG-ib& zu@jRLPv$w$BVv%Y^fO|(fjX(VOcb601C@;191}e=&tHDBJ$3~>4XWhrJm^$pIDZT6 zFJ2-NAee^IqMYFxxgpT&R-sEsP>(W`i~>UA#rGize`7J8;b{iaIaz4Wk}YiRW(xil z*%%k24YjpJ;ZSa3NF*l(p{3A7#N%*3LIfu$@)HP#WEswgO;>(yR!_O|F+)%0>dG2Oi>zE&l+K1sOQxwjO{#Jncz))c$JuvP)7DD{9O*@>2J_^Y(C zl-Q|29U&mr?~g{x2#PGU5HAvOPjzI8dIIy&JSx8dDk=e7o&9Yy43JcQibVXB>s8!%3lS9%kM^NkDgZE1F6(yEq-<;lr$#1(i( zCqF=!JXsdtUEQo0*KR+MjQ$JJMT1! zY4)$;?5{*H+}I7Zg)WGP7B)2VvqBt*ZAoxENER1gMI0tkF%;cVrQe#((D7EpSGAy% zd#xP#zO2U+3nM2{-m4b-q?~-*A>Wr^qcfQ`>2HxU6C=C2;O7;6T&7VnwY?xwH1_fy zQ|BcWf}^VNv<2~JtKN3iZGH;n7qxKBL_{oA=356L-b?D@FVSZlM6WnmUKW^-a>F$&jhG0=k=7M z^Km?xUVH5aNAzFos`T^UcW3L*bIS7m`Y??LFWSHA>+3@aJq{laa_-%D>$sFdQHV^R zXD0njS&V*%{jb%%R*m;8cZzd7dG%)J%UDc@uWVY0U5f_s9!^+$MtmKcNx3ZVXbC4J}-otY}PXp6j*Z))o5?oNqPn%)U7^&*4YL z5crw!D{B~a%(viAYx8C-tjoZw*osxyGY<)VFykC5$}z^27Ns<%sZ_jPsv$SqLKZBp zWEB9ID^-Xp1Mb7QFCNu}klY$ct}2l~k1nR=c;`USP1AYc%c;A*Lu^{}zV-@}++gHx z%XUO{w!S~mc8F|uDy$cYixw|i0xGMluB!aCFK6dCI?4|2l9#-OXI?@RMy$SRCDgiO zlc;2x|HzVn6caC;fV2jpsbLziE1ZCtz2g3#oha$J)6s%pgsMc-x6|iZkK1-%uMVHX zgyI;KPHv{!gTszuxi3r6Eln8NKh_F=@vaoea~#deLLn-5PYfH#GdHuLEeX4Cvr(pI zDx`iqp6&WN?U(Id3S&>khu5?I5eZrk>F7iv7}nK_u?gAHb!UU22cZ$VXS@$rCN!l+ zTRP^@BE_F`D*M7o&LgU(yRo{?*(lf8sFQ@b_VutMEBW2hqvoVLU0i+B(W)D1G)0Fb zv+hgU;MC+O#S4W}_Y68~g-oI8D&C` zc`&A}_~$}=ad-ahu5~fnqvvw*F1G#oA@?%1{V}NhM~@zUT`5g*x`{cZp-%v76$$-b zfvTJqRxElCe}?5qO}5j*PBiUW-oTfLl3&a_zr&D+hP$;!S_pd-#Mvy=4FJl4Gl4}_ z*O-;vD0h#V&=Q?waXj8{QkXKvP=F?6G%*s^z-z>Sa<3lryeIQfTcZ-2rQEI@PpzAc z4z3RVGsm#K^7k%qOTJCihubya)D1ub`tELOe{(tR zr^-+^ngtRVes)DUCD*$|Zkjk@EDz3~KYs>X zU97+PJL&>m$mYd`9ci4w_aZfb^5gQMqf{e&)%t%g2mVVN#iFx)tM8X9x=riVQX}6m zu%#p5Mf%dh9O_OE#;8wPNs%%c045c#9J}aS2n{}lk zy2ypJb|v58AExmc0BuLLUF9aM%fuHr0vKDwLyMg3?ZAPAP>(L`sP61JeE+nSKhOgn zBgGUD1U1ulhpEu`Y&&`MCgv~%D`_8A1?A`F2~jNncC)W-LReiuN1>^kEw!^@yX`l7 zHeDN!kIM7^egQ`it;aS>v8pHfmNwhvHM^C{#A?!x(Ij9dN<~T)h%#4W;Us6*mH^61 z>H+(J-yd!~4UJyNzW&6dW?~9gQ!K}Qgwn>hGp+gMtCP;x&nzTPg2lD#;uK7q`KI;j*B+|^1T_+b(NB@mkC7<|vniktBXn4%EZWs~rcoCr z63K{$0~f_K2Z_Wj@jKS6wrRwkdBmP)Qclw#*I^eI-unBjeZPi-CBMtChRalE@39s_ z*Ak-z=jxRTfW50JzCuS68c+`s4A(5T_oP26q4+YNs*6=eW9%Y2ria_0{vlz4c0_T! zIl`$#G8Ddv&fYGn5TZ_Phn?$33Wf{Rk|by1oL@TL2N)Hxiakb*SryzmYNkl|g}U#E zboW~2My6~a31u~rv50EXG>5*j2#t9=E9Jpcz|=yvPmQ2PP4u?`0#CjeeHkWXutfoj z9LY35nqO1rnufWf-ZJw?VJ;>0cK9tHd`n~Wx)&Roo&%e)NO)ewS+7puat!ibuh`?L z7;AS@y_}(=2&NEiG54XQ9TJ&h&7;<(9>atKNUhdz>GZ)8tmPt!>l(#I&vNY2WtEkt z&;IQAQqs~qUJs0`-KOGZY}MBKUl1Hxbj;Wa4ibU#Z_uD+CNKmLXm8Gs06WhAtm9`# zJN75Ctk^S_vyqZ-_cLsPwqf02l)k^eU*~a36GOujJ$yHs=j=oGW{vTnKP1eO9&F~@ z!R?~HI&D&6VIcwzgNdhqCS5v*?FMjg+iW&TxR7G*>B+O&{ZM4Y;a5lef6g13YNSHG z58Z8@4@W3f)I!p6r1pj#C5OOjFkvUpOUU0ZCkFnC+&mj`oI!L}6w0wmyTh$*e`D%R zw`NNjqY`;+RpvP(zI(saz!4qM>nDwiJG^)HSx_6{=PLzYmcFbvQ^x2vb_#>`HEz73 zum#ztc@*LW7;cf;%doh?+ zBQl%t*lc*GzCU1sj&|?+f0ib$o`4j&X4WL1tEKn^_mf33@g&kI~bmj8xPMV2Q z-5FCN|0Od-Ct62xfdz3eq+~px6a;f^f=2jRqB!Ow%x+=?ywKE?aJEV;{hk@-^?u~_ z!KOP%G2yuJ-9r_8&OzvWh@5spd5T}-ueFv(Opw(4o!*e13hXeFXYyc*w7n+yk1BYq zlA*XZfGXd{Yg+!HDARI85z4ScSC%mZW=Vxfn!+@g9J5DZwO%qb!hzOa`}>&E$H>_t zw-VKUVnwVU6-NA|{h(q3rgFbtY}L>43Z@?rl|@vU>F?#q ztSUe$OQ}QgL)3L;=hgU7OzVrLsx+6Pvs6TCErihT4a#1D09UGye=kDQS3=cOOmu7i z*bQ4MX8)$fp&t^4!iCsIL=lz0RH)tqnEqPq<;xuj;~^oTp_2)wVe0zc&a8Ofc{aWt zTEirRPmG0FWvjFrj;&$tzD!f3=v)CBQxiV1|5fRQI5P32Y`wdGS1)Ux=j{i>odvI( zSdslNKAq#H+HEfM>ijpS7PrUqH>}{e0e9CZiI8 zEftB-*RMYbdhB=Ik` zrR&Vax7|8E6uwJ?VYctm1^p?B#Me@>j}!qEqy5X~hR|Al7}y7-{Vmj;AGQy!BON0j zmmxl;ViMAB!NsWv{gnyI@y9d^iPNG=fvxvjvt;%_e~5JDo!?VRNO$qLdI7ZhF*^VFu1M}R}NbB<+z1fS)L zanq>Fo8;IVnIo`Y^;}S`uV$P-4&TG4;EdDLXLh3NHfqJUC}k=}6i7cFF_R-W$PXw% zcNKoc*!SZz62h+Dm4<R zSkr+N=Rf{16#}R1KBg3{IckqDl9s9(zFGz=PJgr(8;}$vE4!k6s2r}jms%L_P$J(w zg$V}*Z5ySzi}*n`9{ED)BTD0lWE=eH$LDTN)6ysa08oXB>aIwyR!z9Ggw?BhC!+mo~UPJxzY1 zZM6Mfmj?-hyatp&x(P_Y9<7$Z+NJN6&4C@D#V+Yh*A<6^gd|YOPdEhqnxW3;D6DNOVM!%mmrs_Sn__pt08#%b^=sv zfroCCB_ACxvi~r@Bfe(sO9h@g97UJ3#zp=$T9+ZlN%rkb;v}V)acrtQEVJefC7!U8-ZsVcM}I z^_ERw;ntBCPDmOsdooIGH>iHxncXf>(qJW_e00$801r8V zG@zWYSj;SMD5PtOaw-XsNSUXEJ~fB{M=d{9auHIIU1qj^{(1^4<~v=Gb8DsgBOmci0C(Jx$op@iF7BJ)Q^qZyW7Ly2|*1YZ}M|D~RjX#0R90766|EcX#M zolejlHl0GmU6=$#v`&E>1I^^(5f?&;yVD%ElTkM4LW=ycaOMSFQZJlbn{rI>3Y(UpB^0*(9oiJfaH9 z={&l^ep>%TK2POrUcEg^E@ne{+x2vx$>b^&jZ}wYsYEqP$~&sYFjq`hiKW4~B9~Y= zTD!(Xhw>D88JmldE6yi{#)yoyPNVLWQPQBCNV#GOc8^rtSfdM$-0jr4oVd2~ZeIa{ zws(Jg@N0wkGc6gm`xxcmDg0PvKua<w>Qes1mlGDbfEXwVbv3{hV5#g~_WmT(u0jo!y zRTPT5q;IUq~TOhYt>4nHt8r3?;qrw?*qFukvb7-TT|>UmwRRHrGLfLJe}X( z2X@fDF>pMfxl?Q+`(KcjEu)+dCr~YS9M)AX#~eSBMdUAKZ#aqqM`&CX+uMe1>u<#Z zucc*W$AAG_W-!%$lOhELh3lGqs9L!t5h1{6AH?^bw)LYYn-T!S|| zeSxxR@S2j8RMWmN@u%y!qP*Pm{q;Vudy6X`=}A>qR<`Cf7wCL>g3NTSVNz!wsRM|Y z^d9QS8;LOgox3E51AA7kiUg|S5CMEz-m z0lXc=oz0x$U6a^wZp=YVQt9-6ziIgE>AdzOC8KB!)f`+HxSAA%->_4>$in?woO7Km zzlah(?Kw@WEF2uT|B?}1o8v@wH}@_RWB{*tX&G7krD5kjvXS%9K2mWgsbWK$D~i~o z1eN-va6@Yz%ah0ziB)c^;7s?)SW=ya!%b&1_-H9l9|=u$&4tfXkf5;j(AFYSM?DwI zXG`VF;F>Btv8m(+!kZ8K`2&2J#6S<`lKvM04ME=?o74*dTtaX65?g7r(RG0iHs4@t z!*C~i&EeX;LQQl-XaQe1rBnu?>);n&5&?Z`YC)*SFk&K61O-EW+A+}}%`j>VtnYb7 z*$SCRQ4ww>vA76^$J7BP2-9C`sMSd)Qlze=zq`|YcmKZi@CT~AU%Jf4b9QsXlHcJ1 zYVDo$XkFC3ai+2|Xq5AaU8dDpvMJ=w+`KAe7hzb*lG@ejC->%VA=*ytbev<;~%ZpYCmtu z+XkiO(S4FulXe`L`l>k1tf-hCltaQWfr-Fe(DRi^#ye{}8}{50p|;BhP2k#z7T&ez zv>!Me)BZY!*4xwb$ZV*W#wf0q$uN@bSz+$~l!tZRNsgk)Ej~`~FK-o-Zb|w3xI{LX zr_9a_$+Rj$bG}q9BM7LAiysoE3=kbf?aLz!+zwL=%tvU}Bmpz;J`thWYCP)v$~5!_ zQZ5*@C?vGQ_bUbA3j%mC$GPbR5~%{RK*jFVVUOqfKG9$vT%abYtPzm|≀ce73V> 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 =ceW@l|Dczj{1u%?fT@l55Y|H|s1#v9X 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%0D@I`*?O#-r#a*@jMZt;b4kVe#xL{zfNhG&YM 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