From 97d7ff3619744cb539d561c64f5676da32bf6547 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 12:10:01 -0800 Subject: [PATCH 01/45] build_train to use build vs release --- ml_service/pipelines/build_train_pipeline.py | 26 ++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/ml_service/pipelines/build_train_pipeline.py b/ml_service/pipelines/build_train_pipeline.py index 481c68e5..f3c42ba8 100644 --- a/ml_service/pipelines/build_train_pipeline.py +++ b/ml_service/pipelines/build_train_pipeline.py @@ -22,6 +22,7 @@ def main(): sources_directory_train = os.environ.get("SOURCES_DIR_TRAIN") train_script_path = os.environ.get("TRAIN_SCRIPT_PATH") evaluate_script_path = os.environ.get("EVALUATE_SCRIPT_PATH") + register_script_path = os.environ.get("REGISTER_SCRIPT_PATH") vm_size = os.environ.get("AML_COMPUTE_CLUSTER_CPU_SKU") compute_name = os.environ.get("AML_COMPUTE_CLUSTER_NAME") model_name = os.environ.get("MODEL_NAME") @@ -57,8 +58,8 @@ def main(): model_name = PipelineParameter( name="model_name", default_value=model_name) - release_id = PipelineParameter( - name="release_id", default_value="0" + build_id = PipelineParameter( + name="build_id", default_value="0" ) train_step = PythonScriptStep( @@ -67,7 +68,7 @@ def main(): compute_target=aml_compute, source_directory=sources_directory_train, arguments=[ - "--release_id", release_id, + "--build_id", build_id, "--model_name", model_name, ], runconfig=run_config, @@ -81,7 +82,7 @@ def main(): compute_target=aml_compute, source_directory=sources_directory_train, arguments=[ - "--release_id", release_id, + "--build_id", build_id, "--model_name", model_name, ], runconfig=run_config, @@ -89,8 +90,23 @@ def main(): ) print("Step Evaluate created") + register_step = PythonScriptStep( + name="Register Model ", + script_name=register_script_path, + compute_target=aml_compute, + source_directory=sources_directory_train, + arguments=[ + "--build_id", build_id, + "--model_name", model_name, + ], + runconfig=run_config, + allow_reuse=False, + ) + print("Step Register created") + evaluate_step.run_after(train_step) - steps = [evaluate_step] + register_step.run_after(evaluate_step) + steps = [train_step,evaluate_step,register_step] train_pipeline = Pipeline(workspace=aml_workspace, steps=steps) train_pipeline.validate() From 1d374b8f3aca47192cfe1c5138c31c06aa62fc62 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 12:18:22 -0800 Subject: [PATCH 02/45] retrieve pipeline id, agentless task to run --- .pipelines/azdo-ci-build-train.yml | 100 ++++++++++++++------- ml_service/pipelines/run_train_pipeline.py | 23 +++-- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index c2453d4d..f819d368 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -12,7 +12,9 @@ trigger: variables: - group: devopsforai-aml-vg -# Choose from default, build_train_pipeline_with_r.py, or build_train_pipeline_with_r_on_dbricks.py +# Choose from default, build_train_pipelinewith_r.py, or build_train_pipeline_with_r_on_dbricks.py +- name: BUILD_BUILDNUMBER2 + value: '20191112.2' - name: build-train-script value: 'build_train_pipeline.py' # Automatically triggers the train, evaluate, register pipeline after the CI steps. @@ -21,30 +23,30 @@ variables: # value: false stages: -- stage: 'Model_CI' - displayName: 'Model CI' - jobs: - - job: "Model_CI_Pipeline" - displayName: "Model CI Pipeline" - pool: - vmImage: 'ubuntu-latest' - container: mcr.microsoft.com/mlops/python:latest - timeoutInMinutes: 0 - steps: - - template: azdo-base-pipeline.yml - - script: | - # Invoke the Python building and publishing a training pipeline - python3 $(Build.SourcesDirectory)/ml_service/pipelines/$(build-train-script) - failOnStderr: 'false' - env: - SP_APP_SECRET: '$(SP_APP_SECRET)' - displayName: 'Publish Azure Machine Learning Pipeline' +# - stage: 'Model_CI' +# displayName: 'Model CI' +# jobs: +# - job: "Model_CI_Pipeline" +# displayName: "Model CI Pipeline" +# pool: +# vmImage: 'ubuntu-latest' +# container: mcr.microsoft.com/mlops/python:latest +# timeoutInMinutes: 0 +# steps: +# - template: azdo-base-pipeline.yml +# - script: | +# # Invoke the Python building and publishing a training pipeline +# python3 $(Build.SourcesDirectory)/ml_service/pipelines/$(build-train-script) +# failOnStderr: 'false' +# env: +# SP_APP_SECRET: '$(SP_APP_SECRET)' +# displayName: 'Publish Azure Machine Learning Pipeline' - stage: 'Trigger_AML_Pipeline' displayName: 'Train, evaluate, register model via previously published AML pipeline' jobs: - - job: "Invoke_Model_Pipeline" + - job: "Get_Pipeline_ID" condition: and(succeeded(), eq(coalesce(variables['auto-trigger-training'], 'true'), 'true')) - displayName: "Invoke Model Pipeline and evaluate results to register" + displayName: "Get Pipeline ID for execution" pool: vmImage: 'ubuntu-latest' container: mcr.microsoft.com/mlops/python:latest @@ -52,20 +54,50 @@ stages: steps: - script: | python $(Build.SourcesDirectory)/ml_service/pipelines/run_train_pipeline.py + source $(Build.SourcesDirectory)/tmp.sh + echo "##vso[task.setvariable variable=AMLPIPELINEID;isOutput=true]$AMLPIPELINE_ID" + name: 'getpipelineid' displayName: 'Trigger Training Pipeline' env: SP_APP_SECRET: '$(SP_APP_SECRET)' - - task: CopyFiles@2 - displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' - inputs: - SourceFolder: '$(Build.SourcesDirectory)' - TargetFolder: '$(Build.ArtifactStagingDirectory)' - Contents: | - code/scoring/** - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact' + - job: "Run_ML_Pipeline" + dependsOn: "Get_Pipeline_ID" + displayName: "Trigger ML Training Pipeline" + pool: server + variables: + AMLPIPELINE_ID: $[ dependencies.Get_Pipeline_ID.outputs['getpipelineid.AMLPIPELINEID'] ] + steps: + - task: ms-air-aiagility.private-vss-services-azureml.azureml-restApi-task.MLPublishedPipelineRestAPITask@0 + displayName: 'Invoke ML pipeline' inputs: - ArtifactName: 'mlops-pipelines' - publishLocation: 'container' - pathtoPublish: '$(Build.ArtifactStagingDirectory)' - TargetPath: '$(Build.ArtifactStagingDirectory)' \ No newline at end of file + azureSubscription: 'aml-abtest-workspace' + PipelineId: '$(AMLPIPELINE_ID)' + ExperimentName: '$(EXPERIMENT_NAME)' + PipelineParameters: '"model_name": "sklearn_regression_model.pkl"' + # - job: "Training_Run_Report" + # dependsOn: "Run_ML_Pipeline" + # displayName: "Determine if evaluation succeeded and new model is registered" + # pool: + # vmImage: 'ubuntu-latest' + # container: mcr.microsoft.com/mlops/python:latest + # timeoutInMinutes: 0 + # steps: + # - script: | + # python $(Build.SourcesDirectory)/ml_service/pipelines/run_train_pipeline.py + # displayName: 'Trigger Training Pipeline' + # env: + # SP_APP_SECRET: '$(SP_APP_SECRET)' + # - task: CopyFiles@2 + # displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' + # inputs: + # SourceFolder: '$(Build.SourcesDirectory)' + # TargetFolder: '$(Build.ArtifactStagingDirectory)' + # Contents: | + # code/scoring/** + # - task: PublishBuildArtifacts@1 + # displayName: 'Publish Artifact' + # inputs: + # ArtifactName: 'mlops-pipelines' + # publishLocation: 'container' + # pathtoPublish: '$(Build.ArtifactStagingDirectory)' + # TargetPath: '$(Build.ArtifactStagingDirectory)' \ No newline at end of file diff --git a/ml_service/pipelines/run_train_pipeline.py b/ml_service/pipelines/run_train_pipeline.py index 1d942a8c..09f0b9f5 100644 --- a/ml_service/pipelines/run_train_pipeline.py +++ b/ml_service/pipelines/run_train_pipeline.py @@ -4,7 +4,6 @@ from azureml.core.authentication import ServicePrincipalAuthentication from dotenv import load_dotenv - def main(): load_dotenv() workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" @@ -16,6 +15,7 @@ def main(): app_id = os.environ.get('SP_APP_ID') app_secret = os.environ.get('SP_APP_SECRET') build_id = os.environ.get('BUILD_BUILDID') + skip_train_execution = True service_principal = ServicePrincipalAuthentication( tenant_id=tenant_id, @@ -45,16 +45,21 @@ def main(): raise KeyError(f"Unable to find a published pipeline for this build {build_id}") # NOQA: E501 else: published_pipeline = matched_pipes[0] + print("published pipeline id is", published_pipeline.id) - pipeline_parameters = {"model_name": model_name} - - response = published_pipeline.submit( - aml_workspace, - experiment_name, - pipeline_parameters) + # Save the Pipeline ID to be used for other AzDO jobs after script is complete + os.environ['amlpipeline_id'] = published_pipeline.id + savePIDcmd = 'echo "export AMLPIPELINE_ID=$amlpipeline_id" >tmp.sh' + os.system(savePIDcmd) + if(skip_train_execution == False): + pipeline_parameters = {"model_name": model_name} + response = published_pipeline.submit( + aml_workspace, + experiment_name, + pipeline_parameters) - run_id = response.id - print("Pipeline run initiated ", run_id) + run_id = response.id + print("Pipeline run initiated ", run_id) if __name__ == "__main__": From 4353c9fdd9ab347fc796c6511e338dc3dc04d945 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 12:25:22 -0800 Subject: [PATCH 03/45] refactor register model --- code/register/register_model.py | 176 +++++++++++++++++------------- ml_service/util/register_model.py | 49 --------- 2 files changed, 102 insertions(+), 123 deletions(-) delete mode 100644 ml_service/util/register_model.py diff --git a/code/register/register_model.py b/code/register/register_model.py index ae2b8216..5cdf3f64 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -26,86 +26,114 @@ import os import json import sys -from azureml.core import Run import argparse +from azureml.core import Run, Experiment, Workspace +from azureml.core.model import Model as AMLModel +from azureml.core.authentication import ServicePrincipalAuthentication +from dotenv import load_dotenv +sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 +from model_helper import get_model_by_build_id -from azureml.core.authentication import AzureCliAuthentication +def main(): + load_dotenv() + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") + subscription_id = os.environ.get("SUBSCRIPTION_ID") + tenant_id = os.environ.get("TENANT_ID") + model_name = os.environ.get("MODEL_NAME") + app_id = os.environ.get('SP_APP_ID') + app_secret = os.environ.get('SP_APP_SECRET') + build_id = os.environ.get('BUILD_BUILDID') -cli_auth = AzureCliAuthentication() + run = Run.get_context() + if (run.id.startswith('OfflineRun')): + # For local development, set values in this section + service_principal = ServicePrincipalAuthentication( + tenant_id=tenant_id, + service_principal_id=app_id, + service_principal_password=app_secret) -# Get workspace -# ws = Workspace.from_config(auth=cli_auth, path='./') - - -run = Run.get_context() -exp = run.experiment -ws = run.experiment.workspace - -parser = argparse.ArgumentParser("register") -parser.add_argument( - "--config_suffix", type=str, help="Datetime suffix for json config files" -) -parser.add_argument( - "--json_config", - type=str, - help="Directory to write all the intermediate json configs", -) -parser.add_argument( - "--model_name", - type=str, - help="Name of the Model", - default="sklearn_regression_model.pkl", -) - -args = parser.parse_args() - -print("Argument 1: %s" % args.config_suffix) -print("Argument 2: %s" % args.json_config) - -if not (args.json_config is None): - os.makedirs(args.json_config, exist_ok=True) - print("%s created" % args.json_config) - -evaluate_run_id_json = "run_id_{}.json".format(args.config_suffix) -evaluate_output_path = os.path.join(args.json_config, evaluate_run_id_json) -model_name = args.model_name + aml_workspace = Workspace.get( + name=workspace_name, + subscription_id=subscription_id, + resource_group=resource_group, + auth=service_principal + ) + ws = aml_workspace + exp = Experiment(ws, "abtest") + run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" + else: + exp = run.experiment + ws = run.experiment.workspace + + parser = argparse.ArgumentParser("register") + parser.add_argument( + "--build_id", + type=str, + help="The Build ID of the build triggering this pipeline run", + ) + parser.add_argument( + "--run_id", + type=str, + help="Training run ID", + ) + parser.add_argument( + "--model_name", + type=str, + help="Name of the Model", + default="sklearn_regression_model.pkl", + ) -# Get the latest evaluation result -try: - with open(evaluate_output_path) as f: - config = json.load(f) - if not config["run_id"]: - raise Exception( - "No new model to register as production model perform better") -except Exception: - print("No new model to register as production model perform better") - sys.exit(0) + args = parser.parse_args() + if (args.build_id is not None): + build_id = args.build_id + if (args.run_id is not None): + run_id = args.run_id + if (run_id is None): + run_id = run.parent.id() + model_name = args.model_name -run_id = config["run_id"] -experiment_name = config["experiment_name"] -# exp = Experiment(workspace=ws, name=experiment_name) + if (build_id is None): + register_aml_model(model_name, exp, run_id) + else: + register_aml_model(model_name, exp, run_id, build_id) -run = Run(experiment=exp, run_id=run_id) -names = run.get_file_names -names() -print("Run ID for last run: {}".format(run_id)) +def model_already_registered(model_name, exp, run_id): + model_list = AMLModel.list(exp.workspace,name=model_name,run_id=run_id) + if len(model_list) >= 1: + print("Model name:", model_name, "in workspace", \ + exp.workspace, "with run_id ", run_id, "is already registered.") + sys.exit(0) + +def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): + try: + if (build_id != 'none'): + get_model_by_build_id(model_name, build_id, exp.workspace) + model_already_registered(model_name, exp, run_id) + run = Run(experiment=exp, run_id=run_id) + tagsValue={"area": "diabetes", "type": "regression", "build_id": build_id, "run_id": run_id} + else: + run = Run(experiment=exp, run_id=run_id) + if (run is not None): + tagsValue={"area": "diabetes", "type": "regression", "run_id": run_id} + else: + print("A model run for experiment", exp.name, + "matching properties run_id =", run_id, + "was not found. Skipping model registration.") + sys.exit(0) -model = run.register_model(model_name=model_name, - model_path="./outputs/" + model_name, - tags={"area": "diabetes", "type": "regression"}) -os.chdir("..") -print( - "Model registered: {} \nModel Description: {} \nModel Version: {}".format( - model.name, model.description, model.version - ) -) + model = run.register_model(model_name=model_name, + model_path="./outputs/" + model_name, + tags=tagsValue) + os.chdir("..") + print( + "Model registered: {} \nModel Description: {} \nModel Version: {}".format( + model.name, model.description, model.version + ) + ) + except Exception as e: + print(e) + print("Model registration failed") -# Writing the registered model details to /aml_config/model.json -model_json = {} -model_json["model_name"] = model.name -model_json["model_version"] = model.version -model_json["run_id"] = run_id -filename = "model_{}.json".format(args.config_suffix) -output_path = os.path.join(args.json_config, filename) -with open(output_path, "w") as outfile: - json.dump(model_json, outfile) +if __name__ == '__main__': + main() diff --git a/ml_service/util/register_model.py b/ml_service/util/register_model.py deleted file mode 100644 index ea26a997..00000000 --- a/ml_service/util/register_model.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -import os -import os.path -from dotenv import load_dotenv -from azureml.core import Workspace -from azureml.core.model import Model -from azureml.core.authentication import ServicePrincipalAuthentication - -# Load the environment variables from .env in case this script -# is called outside an existing process -load_dotenv() - -TENANT_ID = os.environ.get('TENANT_ID') -APP_ID = os.environ.get('SP_APP_ID') -APP_SECRET = os.environ.get('SP_APP_SECRET') -MODEL_PATH = os.environ.get('MODEL_PATH') -MODEL_NAME = os.environ.get('MODEL_NAME') -WORKSPACE_NAME = os.environ.get("BASE_NAME")+"-AML-WS" -SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') -RESOURCE_GROUP = os.environ.get("BASE_NAME")+"-AML-RG" - - -if os.path.isfile(MODEL_PATH) is False: - print("The given model path %s is invalid" % (MODEL_PATH)) - sys.exit(1) - -SP_AUTH = ServicePrincipalAuthentication( - tenant_id=TENANT_ID, - service_principal_id=APP_ID, - service_principal_password=APP_SECRET) - -WORKSPACE = Workspace.get( - WORKSPACE_NAME, - SP_AUTH, - SUBSCRIPTION_ID, - RESOURCE_GROUP -) - -try: - MODEL = Model.register( - model_path=MODEL_PATH, - model_name=MODEL_NAME, - description="Forecasting Model", - workspace=WORKSPACE) - - print("Model registered successfully. ID: " + MODEL.id) -except Exception as caught_error: - print("Error while registering the model: " + str(caught_error)) - sys.exit(1) From 1bca18612a0ea1726c7637bca2c0f72a39c036e1 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 12:26:39 -0800 Subject: [PATCH 04/45] add model helper util --- ml_service/util/model_helper.py | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 ml_service/util/model_helper.py diff --git a/ml_service/util/model_helper.py b/ml_service/util/model_helper.py new file mode 100644 index 00000000..2b5570f8 --- /dev/null +++ b/ml_service/util/model_helper.py @@ -0,0 +1,80 @@ +""" +model_helper.py +""" +from azureml.core import Run, Model +from azureml.core import Workspace +from azureml.core.model import Model as AMLModel + +def get_current_workspace() -> Workspace: + """ + Retrieves and returns the latest model from the workspace + by its name and tag. Will not work when ran locally. + + Parameters: + None + + Return: + The current workspace. + """ + run = Run.get_context(allow_offline=False) + experiment = run.experiment + return experiment.workspace + +def _get_model_by_build_id( + model_name: str, + build_id: str, + aml_workspace: Workspace = None + ) -> AMLModel: + """ + Retrieves and returns the latest model from the workspace + by its name and tag. + + Parameters: + aml_workspace (Workspace): aml.core Workspace that the model lives. + model_name (str): name of the model we are looking for + build_id (str): the build id the model was registered under. + + Return: + A single aml model from the workspace that matches the name and tag. + """ + # Validate params. cannot be None. + if model_name is None: + raise ValueError("model_name[:str] is required") + if build_id is None: + raise ValueError("build_id[:str] is required") + if aml_workspace is None: + aml_workspace = get_current_workspace() + + # get model by tag. + model_list = AMLModel.list( + aml_workspace, name=model_name, tags=[["BuildId", build_id]], latest=True + ) + + # latest should only return 1 model, but if it does, then maybe + # internal code was accidentally changed or the source code has changed. + should_not_happen = ("THIS SHOULD NOT HAPPEN: found more than one"\ + "model for the latest with {{model_name: {model_name}, BuildId:"\ + "{build_id}. Models found: {model_list}}}")\ + .format(model_name=model_name, build_id=build_id, model_list=model_list) + if len(model_list) > 1: + raise ValueError(should_not_happen) + + return model_list + +def get_model_by_build_id( + model_name: str, + build_id: str, + aml_workspace: Workspace = None + ) -> AMLModel: + """ + Wrapper function for get_model_by_id that throws an error if model is none + """ + model_list = _get_model_by_build_id(model_name, build_id, aml_workspace) + + if model_list: + return model_list[0] + + no_model_found = ("Model not found with model_name: {model_name} "\ + "BuildId: {build_id}.")\ + .format(model_name=model_name, build_id=build_id) + raise Exception(no_model_found) From 53f45d85e5b62352ee6435516de79057d3a62f66 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 12:28:54 -0800 Subject: [PATCH 05/45] refactor evaluate script --- code/evaluate/evaluate_model.py | 133 +++++++++++++++++++------------- 1 file changed, 80 insertions(+), 53 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index ec5dc5e0..f0dcce02 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -24,21 +24,55 @@ POSSIBILITY OF SUCH DAMAGE. """ import os -from azureml.core import Model, Run +import sys +from azureml.core import Model, Run, Workspace, Experiment import argparse +from azureml.core.authentication import ServicePrincipalAuthentication +from dotenv import load_dotenv +sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 +from model_helper import get_model_by_build_id -# Get workspace run = Run.get_context() -exp = run.experiment -ws = run.experiment.workspace +if (run.id.startswith('OfflineRun')): + # For local development, set values in this section + load_dotenv() + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") + subscription_id = os.environ.get("SUBSCRIPTION_ID") + tenant_id = os.environ.get("TENANT_ID") + model_name = os.environ.get("MODEL_NAME") + app_id = os.environ.get('SP_APP_ID') + app_secret = os.environ.get('SP_APP_SECRET') + build_id = os.environ.get('BUILD_BUILDID') + service_principal = ServicePrincipalAuthentication( + tenant_id=tenant_id, + service_principal_id=app_id, + service_principal_password=app_secret) - -parser = argparse.ArgumentParser("evaluate") + aml_workspace = Workspace.get( + name=workspace_name, + subscription_id=subscription_id, + resource_group=resource_group, + auth=service_principal + ) + ws = aml_workspace + exp = Experiment(ws, "abtest") + run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" +else: + exp = run.experiment + ws = run.experiment.workspace + +parser = argparse.ArgumentParser("register") parser.add_argument( - "--release_id", + "--build_id", type=str, - help="The ID of the release triggering this pipeline run", + help="The Build ID of the build triggering this pipeline run", +) +parser.add_argument( + "--run_id", + type=str, + help="Training run ID", ) parser.add_argument( "--model_name", @@ -46,29 +80,20 @@ help="Name of the Model", default="sklearn_regression_model.pkl", ) -args = parser.parse_args() -print("Argument 1: %s" % args.release_id) -print("Argument 2: %s" % args.model_name) +args = parser.parse_args() +if (args.build_id is not None): + build_id = args.build_id +if (args.run_id is not None): + run_id = args.run_id +if (run_id is None): + run_id = run.parent.id() model_name = args.model_name -release_id = args.release_id +metric_eval = "mse" -# Paramaterize the matrics on which the models should be compared +# Paramaterize the matrices on which the models should be compared # Add golden data set on which all the model performance can be evaluated - -all_runs = exp.get_runs( - properties={"release_id": release_id, "run_type": "train"}, - include_children=True - ) -new_model_run = next(all_runs) -new_model_run_id = new_model_run.id -print(f'New Run found with Run ID of: {new_model_run_id}') - try: - # Get most recently registered model, we assume that - # is the model in production. - # Download this model and compare it with the recently - # trained model by running test with same data set. model_list = Model.list(ws) production_model = next( filter( @@ -77,37 +102,39 @@ model_list, ) ) - production_model_run_id = production_model.tags.get("run_id") - run_list = exp.get_runs() + # TODO add logic for 1st time registering model evaluation + production_model_run_id = production_model.run_id # Get the run history for both production model and # newly trained model and compare mse production_model_run = Run(exp, run_id=production_model_run_id) - new_model_run = Run(exp, run_id=new_model_run_id) + new_model_run = run.parent + if (production_model_run.id == new_model_run.id): + print("Production and new model are same run.") + sys.exit(0) + else: + print("Production model run is", production_model_run) - production_model_mse = production_model_run.get_metrics().get("mse") - new_model_mse = new_model_run.get_metrics().get("mse") - print( - "Current Production model mse: {}, New trained model mse: {}".format( - production_model_mse, new_model_mse + production_model_mse = production_model_run.get_metrics().get(metric_eval) + new_model_mse = new_model_run.get_metrics().get(metric_eval) + if (production_model_mse is None or new_model_mse is None): + print("Unable to find", metric_eval, "metrics, " + "exiting evaluation") + sys.exit(0) + else: + print( + "Current Production model mse: {}, New trained model mse: {}".format( + production_model_mse, new_model_mse + ) ) - ) - - promote_new_model = False + if new_model_mse < production_model_mse: - promote_new_model = True - print("New trained model performs better, thus it will be registered") -except Exception: - promote_new_model = True - print("This is the first model to be trained, \ - thus nothing to evaluate for now") - - -# Writing the run id to /aml_config/run_id.json -if promote_new_model: - model_path = os.path.join('outputs', model_name) - new_model_run.register_model( - model_name=model_name, - model_path=model_path, - properties={"release_id": release_id}) - print("Registered new model!") + print("New trained model performs better, thus it should be registered") + else: + print("New trained model metric is less than or equal to " + "production model so skipping model registration.") + run.parent.cancel() + # sys.exit(1) +except Exception as e: + print(e) + print("Something went wrong trying to evaluate. Exiting.") From 4b036a107897af408641be35d9932084a39ee558 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 12:30:04 -0800 Subject: [PATCH 06/45] CI cleanup local dev --- .pipelines/azdo-ci-build-train.yml | 38 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index f819d368..f61c7b44 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -13,8 +13,6 @@ trigger: variables: - group: devopsforai-aml-vg # Choose from default, build_train_pipelinewith_r.py, or build_train_pipeline_with_r_on_dbricks.py -- name: BUILD_BUILDNUMBER2 - value: '20191112.2' - name: build-train-script value: 'build_train_pipeline.py' # Automatically triggers the train, evaluate, register pipeline after the CI steps. @@ -23,24 +21,24 @@ variables: # value: false stages: -# - stage: 'Model_CI' -# displayName: 'Model CI' -# jobs: -# - job: "Model_CI_Pipeline" -# displayName: "Model CI Pipeline" -# pool: -# vmImage: 'ubuntu-latest' -# container: mcr.microsoft.com/mlops/python:latest -# timeoutInMinutes: 0 -# steps: -# - template: azdo-base-pipeline.yml -# - script: | -# # Invoke the Python building and publishing a training pipeline -# python3 $(Build.SourcesDirectory)/ml_service/pipelines/$(build-train-script) -# failOnStderr: 'false' -# env: -# SP_APP_SECRET: '$(SP_APP_SECRET)' -# displayName: 'Publish Azure Machine Learning Pipeline' +- stage: 'Model_CI' + displayName: 'Model CI' + jobs: + - job: "Model_CI_Pipeline" + displayName: "Model CI Pipeline" + pool: + vmImage: 'ubuntu-latest' + container: mcr.microsoft.com/mlops/python:latest + timeoutInMinutes: 0 + steps: + - template: azdo-base-pipeline.yml + - script: | + # Invoke the Python building and publishing a training pipeline + python3 $(Build.SourcesDirectory)/ml_service/pipelines/$(build-train-script) + failOnStderr: 'false' + env: + SP_APP_SECRET: '$(SP_APP_SECRET)' + displayName: 'Publish Azure Machine Learning Pipeline' - stage: 'Trigger_AML_Pipeline' displayName: 'Train, evaluate, register model via previously published AML pipeline' jobs: From 738f037de023f5357b5df203779e98def6bb1189 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 16:20:36 -0800 Subject: [PATCH 07/45] Fix linting --- code/evaluate/evaluate_model.py | 14 ++++---- code/register/register_model.py | 32 ++++++++++------- ml_service/pipelines/build_train_pipeline.py | 2 +- ml_service/pipelines/run_train_pipeline.py | 13 +++---- ml_service/util/model_helper.py | 37 +++++++++++--------- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index f0dcce02..f31a4513 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -30,7 +30,7 @@ from azureml.core.authentication import ServicePrincipalAuthentication from dotenv import load_dotenv sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 -from model_helper import get_model_by_build_id +# from model_helper import get_model_by_build_id run = Run.get_context() @@ -62,7 +62,7 @@ else: exp = run.experiment ws = run.experiment.workspace - + parser = argparse.ArgumentParser("register") parser.add_argument( "--build_id", @@ -119,17 +119,19 @@ new_model_mse = new_model_run.get_metrics().get(metric_eval) if (production_model_mse is None or new_model_mse is None): print("Unable to find", metric_eval, "metrics, " - "exiting evaluation") + "exiting evaluation") sys.exit(0) else: print( - "Current Production model mse: {}, New trained model mse: {}".format( + "Current Production model mse: {}, " + "New trained model mse: {}".format( production_model_mse, new_model_mse ) ) - + if new_model_mse < production_model_mse: - print("New trained model performs better, thus it should be registered") + print("New trained model performs better, " + "thus it should be registered") else: print("New trained model metric is less than or equal to " "production model so skipping model registration.") diff --git a/code/register/register_model.py b/code/register/register_model.py index 5cdf3f64..0f694cd4 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -24,7 +24,6 @@ POSSIBILITY OF SUCH DAMAGE. """ import os -import json import sys import argparse from azureml.core import Run, Experiment, Workspace @@ -34,6 +33,7 @@ sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 from model_helper import get_model_by_build_id + def main(): load_dotenv() workspace_name = os.environ.get("WORKSPACE_NAME") @@ -65,7 +65,7 @@ def main(): else: exp = run.experiment ws = run.experiment.workspace - + parser = argparse.ArgumentParser("register") parser.add_argument( "--build_id", @@ -98,36 +98,41 @@ def main(): else: register_aml_model(model_name, exp, run_id, build_id) + def model_already_registered(model_name, exp, run_id): - model_list = AMLModel.list(exp.workspace,name=model_name,run_id=run_id) + model_list = AMLModel.list(exp.workspace, name=model_name, run_id=run_id) if len(model_list) >= 1: - print("Model name:", model_name, "in workspace", \ - exp.workspace, "with run_id ", run_id, "is already registered.") + print("Model name:", model_name, "in workspace", + exp.workspace, "with run_id ", run_id, "is already registered.") sys.exit(0) - + + def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): try: if (build_id != 'none'): get_model_by_build_id(model_name, build_id, exp.workspace) model_already_registered(model_name, exp, run_id) run = Run(experiment=exp, run_id=run_id) - tagsValue={"area": "diabetes", "type": "regression", "build_id": build_id, "run_id": run_id} + tagsValue = {"area": "diabetes", "type": "regression", + "build_id": build_id, "run_id": run_id} else: run = Run(experiment=exp, run_id=run_id) if (run is not None): - tagsValue={"area": "diabetes", "type": "regression", "run_id": run_id} + tagsValue = {"area": "diabetes", + "type": "regression", "run_id": run_id} else: print("A model run for experiment", exp.name, - "matching properties run_id =", run_id, - "was not found. Skipping model registration.") + "matching properties run_id =", run_id, + "was not found. Skipping model registration.") sys.exit(0) model = run.register_model(model_name=model_name, - model_path="./outputs/" + model_name, - tags=tagsValue) + model_path="./outputs/" + model_name, + tags=tagsValue) os.chdir("..") print( - "Model registered: {} \nModel Description: {} \nModel Version: {}".format( + "Model registered: {} \nModel Description: {} " + "\nModel Version: {}".format( model.name, model.description, model.version ) ) @@ -135,5 +140,6 @@ def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): print(e) print("Model registration failed") + if __name__ == '__main__': main() diff --git a/ml_service/pipelines/build_train_pipeline.py b/ml_service/pipelines/build_train_pipeline.py index f3c42ba8..832ab541 100644 --- a/ml_service/pipelines/build_train_pipeline.py +++ b/ml_service/pipelines/build_train_pipeline.py @@ -106,7 +106,7 @@ def main(): evaluate_step.run_after(train_step) register_step.run_after(evaluate_step) - steps = [train_step,evaluate_step,register_step] + steps = [train_step, evaluate_step, register_step] train_pipeline = Pipeline(workspace=aml_workspace, steps=steps) train_pipeline.validate() diff --git a/ml_service/pipelines/run_train_pipeline.py b/ml_service/pipelines/run_train_pipeline.py index 09f0b9f5..2be225fa 100644 --- a/ml_service/pipelines/run_train_pipeline.py +++ b/ml_service/pipelines/run_train_pipeline.py @@ -4,6 +4,7 @@ from azureml.core.authentication import ServicePrincipalAuthentication from dotenv import load_dotenv + def main(): load_dotenv() workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" @@ -18,16 +19,16 @@ def main(): skip_train_execution = True service_principal = ServicePrincipalAuthentication( - tenant_id=tenant_id, - service_principal_id=app_id, - service_principal_password=app_secret) + tenant_id=tenant_id, + service_principal_id=app_id, + service_principal_password=app_secret) aml_workspace = Workspace.get( name=workspace_name, subscription_id=subscription_id, resource_group=resource_group, auth=service_principal - ) + ) # Find the pipeline that was published by the specified build ID pipelines = PublishedPipeline.list(aml_workspace) @@ -47,11 +48,11 @@ def main(): published_pipeline = matched_pipes[0] print("published pipeline id is", published_pipeline.id) - # Save the Pipeline ID to be used for other AzDO jobs after script is complete + # Save the Pipeline ID for other AzDO jobs after script is complete os.environ['amlpipeline_id'] = published_pipeline.id savePIDcmd = 'echo "export AMLPIPELINE_ID=$amlpipeline_id" >tmp.sh' os.system(savePIDcmd) - if(skip_train_execution == False): + if(skip_train_execution is False): pipeline_parameters = {"model_name": model_name} response = published_pipeline.submit( aml_workspace, diff --git a/ml_service/util/model_helper.py b/ml_service/util/model_helper.py index 2b5570f8..1609642a 100644 --- a/ml_service/util/model_helper.py +++ b/ml_service/util/model_helper.py @@ -1,10 +1,11 @@ """ model_helper.py """ -from azureml.core import Run, Model +from azureml.core import Run from azureml.core import Workspace from azureml.core.model import Model as AMLModel + def get_current_workspace() -> Workspace: """ Retrieves and returns the latest model from the workspace @@ -20,11 +21,12 @@ def get_current_workspace() -> Workspace: experiment = run.experiment return experiment.workspace + def _get_model_by_build_id( - model_name: str, - build_id: str, - aml_workspace: Workspace = None - ) -> AMLModel: + model_name: str, + build_id: str, + aml_workspace: Workspace = None +) -> AMLModel: """ Retrieves and returns the latest model from the workspace by its name and tag. @@ -47,25 +49,28 @@ def _get_model_by_build_id( # get model by tag. model_list = AMLModel.list( - aml_workspace, name=model_name, tags=[["BuildId", build_id]], latest=True + aml_workspace, name=model_name, + tags=[["BuildId", build_id]], latest=True ) # latest should only return 1 model, but if it does, then maybe # internal code was accidentally changed or the source code has changed. - should_not_happen = ("THIS SHOULD NOT HAPPEN: found more than one"\ - "model for the latest with {{model_name: {model_name}, BuildId:"\ - "{build_id}. Models found: {model_list}}}")\ - .format(model_name=model_name, build_id=build_id, model_list=model_list) + should_not_happen = ("THIS SHOULD NOT HAPPEN: found more than one model " + "for the latest with {{model_name: {model_name}," + "BuildId: {build_id}. Models found: {model_list}}}")\ + .format(model_name=model_name, build_id=build_id, + model_list=model_list) if len(model_list) > 1: raise ValueError(should_not_happen) return model_list + def get_model_by_build_id( - model_name: str, - build_id: str, - aml_workspace: Workspace = None - ) -> AMLModel: + model_name: str, + build_id: str, + aml_workspace: Workspace = None +) -> AMLModel: """ Wrapper function for get_model_by_id that throws an error if model is none """ @@ -74,7 +79,7 @@ def get_model_by_build_id( if model_list: return model_list[0] - no_model_found = ("Model not found with model_name: {model_name} "\ + no_model_found = ("Model not found with model_name: {model_name} " "BuildId: {build_id}.")\ - .format(model_name=model_name, build_id=build_id) + .format(model_name=model_name, build_id=build_id) raise Exception(no_model_found) From 0855beb2d88ba9a635528e99d1e25b6cef03bad1 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 16:31:03 -0800 Subject: [PATCH 08/45] temp disable unit test --- tests/unit/{code_test.py => code_test.py_} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{code_test.py => code_test.py_} (100%) diff --git a/tests/unit/code_test.py b/tests/unit/code_test.py_ similarity index 100% rename from tests/unit/code_test.py rename to tests/unit/code_test.py_ From 74689be072cf71951e5f9347dc95725503b0e6b0 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 16:48:33 -0800 Subject: [PATCH 09/45] replace base_name var --- ml_service/pipelines/build_train_pipeline.py | 4 ++-- .../pipelines/build_train_pipeline_with_r_on_dbricks.py | 4 ++-- ml_service/pipelines/run_train_pipeline.py | 4 ++-- ml_service/util/create_scoring_image.py | 4 ++-- tests/unit/{code_test.py_ => code_test.py} | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename tests/unit/{code_test.py_ => code_test.py} (84%) diff --git a/ml_service/pipelines/build_train_pipeline.py b/ml_service/pipelines/build_train_pipeline.py index 832ab541..bd408420 100644 --- a/ml_service/pipelines/build_train_pipeline.py +++ b/ml_service/pipelines/build_train_pipeline.py @@ -13,8 +13,8 @@ def main(): load_dotenv() - workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" - resource_group = os.environ.get("BASE_NAME")+"-AML-RG" + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") app_id = os.environ.get("SP_APP_ID") diff --git a/ml_service/pipelines/build_train_pipeline_with_r_on_dbricks.py b/ml_service/pipelines/build_train_pipeline_with_r_on_dbricks.py index 95de9e55..ef5ff09e 100644 --- a/ml_service/pipelines/build_train_pipeline_with_r_on_dbricks.py +++ b/ml_service/pipelines/build_train_pipeline_with_r_on_dbricks.py @@ -10,8 +10,8 @@ def main(): load_dotenv() - workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" - resource_group = os.environ.get("BASE_NAME")+"-AML-RG" + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") app_id = os.environ.get("SP_APP_ID") diff --git a/ml_service/pipelines/run_train_pipeline.py b/ml_service/pipelines/run_train_pipeline.py index 2be225fa..da521440 100644 --- a/ml_service/pipelines/run_train_pipeline.py +++ b/ml_service/pipelines/run_train_pipeline.py @@ -7,8 +7,8 @@ def main(): load_dotenv() - workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" - resource_group = os.environ.get("BASE_NAME")+"-AML-RG" + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") experiment_name = os.environ.get("EXPERIMENT_NAME") diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index 08ae49b5..63a0eefe 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -10,9 +10,9 @@ TENANT_ID = os.environ.get('TENANT_ID') APP_ID = os.environ.get('SP_APP_ID') APP_SECRET = os.environ.get('SP_APP_SECRET') -WORKSPACE_NAME = os.environ.get("BASE_NAME")+"-AML-WS" +WORKSPACE_NAME = os.environ.get("WORKSPACE_NAME") +RESOURCE_GROUP = os.environ.get("RESOURCE_GROUP") SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') -RESOURCE_GROUP = os.environ.get("BASE_NAME")+"-AML-RG" MODEL_NAME = os.environ.get('MODEL_NAME') MODEL_VERSION = os.environ.get('MODEL_VERSION') IMAGE_NAME = os.environ.get('IMAGE_NAME') diff --git a/tests/unit/code_test.py_ b/tests/unit/code_test.py similarity index 84% rename from tests/unit/code_test.py_ rename to tests/unit/code_test.py index b22b186c..1825825c 100644 --- a/tests/unit/code_test.py_ +++ b/tests/unit/code_test.py @@ -7,8 +7,8 @@ # Just an example of a unit test against # a utility function common_scoring.next_saturday def test_get_workspace(): - workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" - resource_group = os.environ.get("BASE_NAME")+"-AML-RG" + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") app_id = os.environ.get("SP_APP_ID") From d83338b071bf21d82613d9bef121ab16f53adb11 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 19:13:40 -0800 Subject: [PATCH 10/45] add registration validation logic --- .pipelines/azdo-ci-build-train.yml | 54 +++++++++---------- code/evaluate/evaluate_model.py | 2 +- code/register/register_model.py | 28 ++++++++-- .../pipelines/build_train_pipeline_with_r.py | 4 +- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index f61c7b44..b0092f8c 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -72,30 +72,30 @@ stages: PipelineId: '$(AMLPIPELINE_ID)' ExperimentName: '$(EXPERIMENT_NAME)' PipelineParameters: '"model_name": "sklearn_regression_model.pkl"' - # - job: "Training_Run_Report" - # dependsOn: "Run_ML_Pipeline" - # displayName: "Determine if evaluation succeeded and new model is registered" - # pool: - # vmImage: 'ubuntu-latest' - # container: mcr.microsoft.com/mlops/python:latest - # timeoutInMinutes: 0 - # steps: - # - script: | - # python $(Build.SourcesDirectory)/ml_service/pipelines/run_train_pipeline.py - # displayName: 'Trigger Training Pipeline' - # env: - # SP_APP_SECRET: '$(SP_APP_SECRET)' - # - task: CopyFiles@2 - # displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' - # inputs: - # SourceFolder: '$(Build.SourcesDirectory)' - # TargetFolder: '$(Build.ArtifactStagingDirectory)' - # Contents: | - # code/scoring/** - # - task: PublishBuildArtifacts@1 - # displayName: 'Publish Artifact' - # inputs: - # ArtifactName: 'mlops-pipelines' - # publishLocation: 'container' - # pathtoPublish: '$(Build.ArtifactStagingDirectory)' - # TargetPath: '$(Build.ArtifactStagingDirectory)' \ No newline at end of file + - job: "Training_Run_Report" + dependsOn: "Run_ML_Pipeline" + displayName: "Determine if evaluation succeeded and new model is registered" + pool: + vmImage: 'ubuntu-latest' + container: mcr.microsoft.com/mlops/python:latest + timeoutInMinutes: 0 + steps: + - script: | + python $(Build.SourcesDirectory)/code/register/register_model.py --build_id $(Build.BuildId) --validate True + displayName: 'Trigger Training Pipeline' + env: + SP_APP_SECRET: '$(SP_APP_SECRET)' + - task: CopyFiles@2 + displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' + inputs: + SourceFolder: '$(Build.SourcesDirectory)' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + Contents: | + code/scoring/** + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact' + inputs: + ArtifactName: 'mlops-pipelines' + publishLocation: 'container' + pathtoPublish: '$(Build.ArtifactStagingDirectory)' + TargetPath: '$(Build.ArtifactStagingDirectory)' \ No newline at end of file diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index f31a4513..4f5531f9 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -63,7 +63,7 @@ exp = run.experiment ws = run.experiment.workspace -parser = argparse.ArgumentParser("register") +parser = argparse.ArgumentParser("evaluate") parser.add_argument( "--build_id", type=str, diff --git a/code/register/register_model.py b/code/register/register_model.py index 0f694cd4..61d70370 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -60,7 +60,7 @@ def main(): auth=service_principal ) ws = aml_workspace - exp = Experiment(ws, "abtest") + exp = Experiment(ws, workspace_name) run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" else: exp = run.experiment @@ -83,6 +83,12 @@ def main(): help="Name of the Model", default="sklearn_regression_model.pkl", ) + parser.add_argument( + "--validate", + type=str, + help="Set to true to only validate if model is registered for run", + default=False, + ) args = parser.parse_args() if (args.build_id is not None): @@ -91,12 +97,23 @@ def main(): run_id = args.run_id if (run_id is None): run_id = run.parent.id() + if (args.validate is not None): + validate = args.validate model_name = args.model_name - if (build_id is None): - register_aml_model(model_name, exp, run_id) + if (validate): + try: + get_model_by_build_id(model_name, build_id, exp.workspace) + print("Model was registered for this build.") + except Exception as e: + print(e) + print("Model was not registered for this run.") + sys.exit(1) else: - register_aml_model(model_name, exp, run_id, build_id) + if (build_id is None): + register_aml_model(model_name, exp, run_id) + else: + register_aml_model(model_name, exp, run_id, build_id) def model_already_registered(model_name, exp, run_id): @@ -104,7 +121,8 @@ def model_already_registered(model_name, exp, run_id): if len(model_list) >= 1: print("Model name:", model_name, "in workspace", exp.workspace, "with run_id ", run_id, "is already registered.") - sys.exit(0) + else: + raise Exception("Model is not registered") def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): diff --git a/ml_service/pipelines/build_train_pipeline_with_r.py b/ml_service/pipelines/build_train_pipeline_with_r.py index 7eae2c98..242fb368 100644 --- a/ml_service/pipelines/build_train_pipeline_with_r.py +++ b/ml_service/pipelines/build_train_pipeline_with_r.py @@ -12,8 +12,8 @@ def main(): load_dotenv() - workspace_name = os.environ.get("BASE_NAME")+"-AML-WS" - resource_group = os.environ.get("BASE_NAME")+"-AML-RG" + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") app_id = os.environ.get("SP_APP_ID") From 8d4ff0ff4c6bce784afcccad349240391c911a34 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 19:18:57 -0800 Subject: [PATCH 11/45] Workspace svc connection to var --- .pipelines/azdo-ci-build-train.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index b0092f8c..ae3a8a14 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -68,7 +68,7 @@ stages: - task: ms-air-aiagility.private-vss-services-azureml.azureml-restApi-task.MLPublishedPipelineRestAPITask@0 displayName: 'Invoke ML pipeline' inputs: - azureSubscription: 'aml-abtest-workspace' + azureSubscription: '$(WORKSPACE_SVC_CONNECTION)' PipelineId: '$(AMLPIPELINE_ID)' ExperimentName: '$(EXPERIMENT_NAME)' PipelineParameters: '"model_name": "sklearn_regression_model.pkl"' From 976c86997f51159fdf719e773d52a12c4fe30d3d Mon Sep 17 00:00:00 2001 From: David Tesar Date: Mon, 18 Nov 2019 19:23:47 -0800 Subject: [PATCH 12/45] Add new env vars to example --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 2f62dad7..5ac21e79 100644 --- a/.env.example +++ b/.env.example @@ -5,14 +5,17 @@ TENANT_ID = '' BASE_NAME = '' SP_APP_ID = '' SP_APP_SECRET = '' +RESOURCE_GROUP = '' # Mock build/release ID for local testing - update ReleaseID each "release" BUILD_BUILDID = '001' RELEASE_RELEASEID = '001' # Azure ML Workspace Variables +WORKSPACE_NAME = 'aml-workspace' EXPERIMENT_NAME = '' SCRIPT_FOLDER = './' +WORKSPACE_SVC_CONNECTION = 'aml-workspace-svc' # AML Compute Cluster Config AML_COMPUTE_CLUSTER_NAME = '' From eb20d03f9497113e38fb4f3e5d07737e54147cd8 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 09:43:52 -0800 Subject: [PATCH 13/45] fix build_id flow --- .pipelines/azdo-ci-build-train.yml | 2 +- ml_service/pipelines/build_train_pipeline.py | 19 +++++++++---------- ml_service/pipelines/run_train_pipeline.py | 6 ++++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index ae3a8a14..55fbe18f 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -55,7 +55,7 @@ stages: source $(Build.SourcesDirectory)/tmp.sh echo "##vso[task.setvariable variable=AMLPIPELINEID;isOutput=true]$AMLPIPELINE_ID" name: 'getpipelineid' - displayName: 'Trigger Training Pipeline' + displayName: 'Get Pipeline ID' env: SP_APP_SECRET: '$(SP_APP_SECRET)' - job: "Run_ML_Pipeline" diff --git a/ml_service/pipelines/build_train_pipeline.py b/ml_service/pipelines/build_train_pipeline.py index bd408420..9b0a23c9 100644 --- a/ml_service/pipelines/build_train_pipeline.py +++ b/ml_service/pipelines/build_train_pipeline.py @@ -56,11 +56,10 @@ def main(): ) run_config.environment.docker.enabled = True - model_name = PipelineParameter( + model_name_param = PipelineParameter( name="model_name", default_value=model_name) - build_id = PipelineParameter( - name="build_id", default_value="0" - ) + build_id_param = PipelineParameter( + name="build_id", default_value=build_id) train_step = PythonScriptStep( name="Train Model", @@ -68,8 +67,8 @@ def main(): compute_target=aml_compute, source_directory=sources_directory_train, arguments=[ - "--build_id", build_id, - "--model_name", model_name, + "--build_id", build_id_param, + "--model_name", model_name_param, ], runconfig=run_config, allow_reuse=False, @@ -82,8 +81,8 @@ def main(): compute_target=aml_compute, source_directory=sources_directory_train, arguments=[ - "--build_id", build_id, - "--model_name", model_name, + "--build_id", build_id_param, + "--model_name", model_name_param, ], runconfig=run_config, allow_reuse=False, @@ -96,8 +95,8 @@ def main(): compute_target=aml_compute, source_directory=sources_directory_train, arguments=[ - "--build_id", build_id, - "--model_name", model_name, + "--build_id", build_id_param, + "--model_name", model_name_param, ], runconfig=run_config, allow_reuse=False, diff --git a/ml_service/pipelines/run_train_pipeline.py b/ml_service/pipelines/run_train_pipeline.py index da521440..e15f2a75 100644 --- a/ml_service/pipelines/run_train_pipeline.py +++ b/ml_service/pipelines/run_train_pipeline.py @@ -16,6 +16,7 @@ def main(): app_id = os.environ.get('SP_APP_ID') app_secret = os.environ.get('SP_APP_SECRET') build_id = os.environ.get('BUILD_BUILDID') + pipeline_name = os.environ.get("TRAINING_PIPELINE_NAME") skip_train_execution = True service_principal = ServicePrincipalAuthentication( @@ -35,8 +36,9 @@ def main(): matched_pipes = [] for p in pipelines: - if p.version == build_id: - matched_pipes.append(p) + if p.name == pipeline_name: + if p.version == build_id: + matched_pipes.append(p) if(len(matched_pipes) > 1): published_pipeline = None From 390acade0377f8db26c3b780e94f9f8241561692 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 10:43:42 -0800 Subject: [PATCH 14/45] use buildID vs release, tag vs properties --- code/training/train.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/code/training/train.py b/code/training/train.py index d703964f..a63be492 100644 --- a/code/training/train.py +++ b/code/training/train.py @@ -36,9 +36,9 @@ parser = argparse.ArgumentParser("train") parser.add_argument( - "--release_id", + "--build_id", type=str, - help="The ID of the release triggering this pipeline run", + help="The build ID of the build triggering this pipeline run", ) parser.add_argument( "--model_name", @@ -49,11 +49,11 @@ args = parser.parse_args() -print("Argument 1: %s" % args.release_id) +print("Argument 1: %s" % args.build_id) print("Argument 2: %s" % args.model_name) model_name = args.model_name -release_id = args.release_id +build_id = args.build_id run = Run.get_context() exp = run.experiment @@ -72,16 +72,11 @@ alphas = np.arange(0.0, 1.0, 0.05) alpha = alphas[np.random.choice(alphas.shape[0], 1, replace=False)][0] print(alpha) -run.log("alpha", alpha) +run.parent.log("alpha", alpha) reg = Ridge(alpha=alpha) reg.fit(data["train"]["X"], data["train"]["y"]) preds = reg.predict(data["test"]["X"]) -run.log("mse", mean_squared_error(preds, data["test"]["y"])) - - -# Save model as part of the run history - -# model_name = "." +run.parent.log("mse", mean_squared_error(preds, data["test"]["y"]),"Mean squared error metric") with open(model_name, "wb") as file: joblib.dump(value=reg, filename=model_name) @@ -96,7 +91,7 @@ print(run.get_file_names()) # Add properties to identify this specific training run -run.add_properties({"release_id": release_id, "run_type": "train"}) -print(f"added properties: {run.properties}") +run.tag({"BuildId": build_id, "run_type": "train"}) +print(f"tags now present for run: {run.tags}") run.complete() From d2e650ae829c029269e52d9262a06a85413dae32 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 10:48:01 -0800 Subject: [PATCH 15/45] fix lint --- code/training/train.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/training/train.py b/code/training/train.py index a63be492..0f17a000 100644 --- a/code/training/train.py +++ b/code/training/train.py @@ -76,7 +76,8 @@ reg = Ridge(alpha=alpha) reg.fit(data["train"]["X"], data["train"]["y"]) preds = reg.predict(data["test"]["X"]) -run.parent.log("mse", mean_squared_error(preds, data["test"]["y"]),"Mean squared error metric") +run.parent.log("mse", mean_squared_error( + preds, data["test"]["y"]), "Mean squared error metric") with open(model_name, "wb") as file: joblib.dump(value=reg, filename=model_name) From c8a897fdd2a9811662812942e8bdefeff525ea79 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 11:18:29 -0800 Subject: [PATCH 16/45] fix tagging syntax --- code/training/train.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/training/train.py b/code/training/train.py index 0f17a000..2d8030e7 100644 --- a/code/training/train.py +++ b/code/training/train.py @@ -92,7 +92,8 @@ print(run.get_file_names()) # Add properties to identify this specific training run -run.tag({"BuildId": build_id, "run_type": "train"}) +run.tag("BuildId", value=build_id) +run.tag("run_type", value="train") print(f"tags now present for run: {run.tags}") run.complete() From 88318bcd371986ef00fd520ef1ab8d531cd624a4 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 12:06:49 -0800 Subject: [PATCH 17/45] local agent, eval tweaks --- .pipelines/azdo-ci-build-train.yml | 12 ++++++------ code/evaluate/evaluate_model.py | 13 ++++++------- code/register/register_model.py | 18 +++++++++--------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index 55fbe18f..7037416a 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -26,8 +26,8 @@ stages: jobs: - job: "Model_CI_Pipeline" displayName: "Model CI Pipeline" - pool: - vmImage: 'ubuntu-latest' + pool: 'davete' + # vmImage: 'ubuntu-latest' container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: @@ -45,8 +45,8 @@ stages: - job: "Get_Pipeline_ID" condition: and(succeeded(), eq(coalesce(variables['auto-trigger-training'], 'true'), 'true')) displayName: "Get Pipeline ID for execution" - pool: - vmImage: 'ubuntu-latest' + pool: 'davete' + # vmImage: 'ubuntu-latest' container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: @@ -75,8 +75,8 @@ stages: - job: "Training_Run_Report" dependsOn: "Run_ML_Pipeline" displayName: "Determine if evaluation succeeded and new model is registered" - pool: - vmImage: 'ubuntu-latest' + pool: 'davete' + # vmImage: 'ubuntu-latest' container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 4f5531f9..422ea25b 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -32,8 +32,8 @@ sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 # from model_helper import get_model_by_build_id - run = Run.get_context() +print("the current run id name is: ", run.id) if (run.id.startswith('OfflineRun')): # For local development, set values in this section load_dotenv() @@ -102,7 +102,6 @@ model_list, ) ) - # TODO add logic for 1st time registering model evaluation production_model_run_id = production_model.run_id # Get the run history for both production model and @@ -110,8 +109,8 @@ production_model_run = Run(exp, run_id=production_model_run_id) new_model_run = run.parent if (production_model_run.id == new_model_run.id): - print("Production and new model are same run.") - sys.exit(0) + print("Production and new model are same.") + firstRegistration = True else: print("Production model run is", production_model_run) @@ -120,7 +119,7 @@ if (production_model_mse is None or new_model_mse is None): print("Unable to find", metric_eval, "metrics, " "exiting evaluation") - sys.exit(0) + run.parent.cancel() else: print( "Current Production model mse: {}, " @@ -129,14 +128,14 @@ ) ) - if new_model_mse < production_model_mse: + if (new_model_mse < production_model_mse or firstRegistration): print("New trained model performs better, " "thus it should be registered") else: print("New trained model metric is less than or equal to " "production model so skipping model registration.") run.parent.cancel() - # sys.exit(1) + except Exception as e: print(e) print("Something went wrong trying to evaluate. Exiting.") diff --git a/code/register/register_model.py b/code/register/register_model.py index 61d70370..ecd5fc7a 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -35,19 +35,19 @@ def main(): - load_dotenv() - workspace_name = os.environ.get("WORKSPACE_NAME") - resource_group = os.environ.get("RESOURCE_GROUP") - subscription_id = os.environ.get("SUBSCRIPTION_ID") - tenant_id = os.environ.get("TENANT_ID") - model_name = os.environ.get("MODEL_NAME") - app_id = os.environ.get('SP_APP_ID') - app_secret = os.environ.get('SP_APP_SECRET') - build_id = os.environ.get('BUILD_BUILDID') run = Run.get_context() if (run.id.startswith('OfflineRun')): # For local development, set values in this section + load_dotenv() + workspace_name = os.environ.get("WORKSPACE_NAME") + resource_group = os.environ.get("RESOURCE_GROUP") + subscription_id = os.environ.get("SUBSCRIPTION_ID") + tenant_id = os.environ.get("TENANT_ID") + model_name = os.environ.get("MODEL_NAME") + app_id = os.environ.get('SP_APP_ID') + app_secret = os.environ.get('SP_APP_SECRET') + build_id = os.environ.get('BUILD_BUILDID') service_principal = ServicePrincipalAuthentication( tenant_id=tenant_id, service_principal_id=app_id, From e34a7f7ac03357d14d81a1b921090173b8f55269 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 12:40:17 -0800 Subject: [PATCH 18/45] local no container test --- .pipelines/azdo-ci-build-train.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index 7037416a..24b97231 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -28,7 +28,7 @@ stages: displayName: "Model CI Pipeline" pool: 'davete' # vmImage: 'ubuntu-latest' - container: mcr.microsoft.com/mlops/python:latest + # container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: - template: azdo-base-pipeline.yml @@ -47,7 +47,7 @@ stages: displayName: "Get Pipeline ID for execution" pool: 'davete' # vmImage: 'ubuntu-latest' - container: mcr.microsoft.com/mlops/python:latest + # container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: - script: | @@ -77,7 +77,7 @@ stages: displayName: "Determine if evaluation succeeded and new model is registered" pool: 'davete' # vmImage: 'ubuntu-latest' - container: mcr.microsoft.com/mlops/python:latest + # container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: - script: | From 76ec63528c1eabdb2e857dc87b0f78878d708972 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 13:33:52 -0800 Subject: [PATCH 19/45] Revert to hosted agents --- .pipelines/azdo-ci-build-train.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index 24b97231..ca8ae045 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -26,9 +26,9 @@ stages: jobs: - job: "Model_CI_Pipeline" displayName: "Model CI Pipeline" - pool: 'davete' - # vmImage: 'ubuntu-latest' - # container: mcr.microsoft.com/mlops/python:latest + pool: + vmImage: 'ubuntu-latest' + container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: - template: azdo-base-pipeline.yml @@ -45,9 +45,9 @@ stages: - job: "Get_Pipeline_ID" condition: and(succeeded(), eq(coalesce(variables['auto-trigger-training'], 'true'), 'true')) displayName: "Get Pipeline ID for execution" - pool: 'davete' - # vmImage: 'ubuntu-latest' - # container: mcr.microsoft.com/mlops/python:latest + pool: + vmImage: 'ubuntu-latest' + container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: - script: | @@ -75,9 +75,9 @@ stages: - job: "Training_Run_Report" dependsOn: "Run_ML_Pipeline" displayName: "Determine if evaluation succeeded and new model is registered" - pool: 'davete' - # vmImage: 'ubuntu-latest' - # container: mcr.microsoft.com/mlops/python:latest + pool: + vmImage: 'ubuntu-latest' + container: mcr.microsoft.com/mlops/python:latest timeoutInMinutes: 0 steps: - script: | From 09626273ff1bc7d033be2af552d0270224f9fc2b Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 13:57:39 -0800 Subject: [PATCH 20/45] move dotenv import under condition --- code/evaluate/evaluate_model.py | 6 +----- code/register/register_model.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 422ea25b..0facd813 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -24,17 +24,13 @@ POSSIBILITY OF SUCH DAMAGE. """ import os -import sys from azureml.core import Model, Run, Workspace, Experiment import argparse from azureml.core.authentication import ServicePrincipalAuthentication -from dotenv import load_dotenv -sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 -# from model_helper import get_model_by_build_id run = Run.get_context() -print("the current run id name is: ", run.id) if (run.id.startswith('OfflineRun')): + from dotenv import load_dotenv # For local development, set values in this section load_dotenv() workspace_name = os.environ.get("WORKSPACE_NAME") diff --git a/code/register/register_model.py b/code/register/register_model.py index ecd5fc7a..911f4852 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -29,7 +29,6 @@ from azureml.core import Run, Experiment, Workspace from azureml.core.model import Model as AMLModel from azureml.core.authentication import ServicePrincipalAuthentication -from dotenv import load_dotenv sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 from model_helper import get_model_by_build_id @@ -38,6 +37,7 @@ def main(): run = Run.get_context() if (run.id.startswith('OfflineRun')): + from dotenv import load_dotenv # For local development, set values in this section load_dotenv() workspace_name = os.environ.get("WORKSPACE_NAME") From 5511ac5be66be6a70b3a968e487e2acd9de0994e Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 14:30:21 -0800 Subject: [PATCH 21/45] fix run_id logic --- .pipelines/azdo-ci-build-train.yml | 2 +- code/evaluate/evaluate_model.py | 5 +++-- code/register/register_model.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index ca8ae045..c63960b6 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -12,7 +12,7 @@ trigger: variables: - group: devopsforai-aml-vg -# Choose from default, build_train_pipelinewith_r.py, or build_train_pipeline_with_r_on_dbricks.py +# Choose from default, build_train_pipeline_with_r.py, or build_train_pipeline_with_r_on_dbricks.py - name: build-train-script value: 'build_train_pipeline.py' # Automatically triggers the train, evaluate, register pipeline after the CI steps. diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 0facd813..fdec063f 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -53,11 +53,12 @@ auth=service_principal ) ws = aml_workspace - exp = Experiment(ws, "abtest") + exp = Experiment(ws, workspace_name) run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" else: exp = run.experiment ws = run.experiment.workspace + run_id = 'amlcompute' parser = argparse.ArgumentParser("evaluate") parser.add_argument( @@ -82,7 +83,7 @@ build_id = args.build_id if (args.run_id is not None): run_id = args.run_id -if (run_id is None): +if (run_id == 'amlcompute'): run_id = run.parent.id() model_name = args.model_name metric_eval = "mse" diff --git a/code/register/register_model.py b/code/register/register_model.py index 911f4852..c760fbd4 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -63,8 +63,9 @@ def main(): exp = Experiment(ws, workspace_name) run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" else: - exp = run.experiment ws = run.experiment.workspace + exp = run.experiment + run = 'amlcompute' parser = argparse.ArgumentParser("register") parser.add_argument( @@ -95,7 +96,7 @@ def main(): build_id = args.build_id if (args.run_id is not None): run_id = args.run_id - if (run_id is None): + if (run_id == 'amlcompute'): run_id = run.parent.id() if (args.validate is not None): validate = args.validate From 43678c93034b55972c3c5101cb770f2dd0b22479 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 14:52:53 -0800 Subject: [PATCH 22/45] fix parent.id --- code/evaluate/evaluate_model.py | 2 +- code/register/register_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index fdec063f..dc4ead1d 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -84,7 +84,7 @@ if (args.run_id is not None): run_id = args.run_id if (run_id == 'amlcompute'): - run_id = run.parent.id() + run_id = run.parent.id model_name = args.model_name metric_eval = "mse" diff --git a/code/register/register_model.py b/code/register/register_model.py index c760fbd4..62a4a419 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -97,7 +97,7 @@ def main(): if (args.run_id is not None): run_id = args.run_id if (run_id == 'amlcompute'): - run_id = run.parent.id() + run_id = run.parent.id if (args.validate is not None): validate = args.validate model_name = args.model_name From 7842091726f521237abbc16942d859934a948b52 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 15:28:50 -0800 Subject: [PATCH 23/45] move model helper --- code/register/register_model.py | 2 +- {ml_service => code}/util/model_helper.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {ml_service => code}/util/model_helper.py (100%) diff --git a/code/register/register_model.py b/code/register/register_model.py index 62a4a419..ea8bb5b4 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -29,7 +29,7 @@ from azureml.core import Run, Experiment, Workspace from azureml.core.model import Model as AMLModel from azureml.core.authentication import ServicePrincipalAuthentication -sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 +sys.path.append(os.path.abspath("./util")) # NOQA: E402 from model_helper import get_model_by_build_id diff --git a/ml_service/util/model_helper.py b/code/util/model_helper.py similarity index 100% rename from ml_service/util/model_helper.py rename to code/util/model_helper.py From f94823d12d5594caa34a899a4128e17f25cfe082 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 15:46:11 -0800 Subject: [PATCH 24/45] log to train also, fix run_id --- code/register/register_model.py | 2 +- code/training/train.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/code/register/register_model.py b/code/register/register_model.py index ea8bb5b4..a9f47224 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -65,7 +65,7 @@ def main(): else: ws = run.experiment.workspace exp = run.experiment - run = 'amlcompute' + run_id = 'amlcompute' parser = argparse.ArgumentParser("register") parser.add_argument( diff --git a/code/training/train.py b/code/training/train.py index 2d8030e7..7f109922 100644 --- a/code/training/train.py +++ b/code/training/train.py @@ -72,12 +72,15 @@ alphas = np.arange(0.0, 1.0, 0.05) alpha = alphas[np.random.choice(alphas.shape[0], 1, replace=False)][0] print(alpha) +run.log("alpha", alpha) run.parent.log("alpha", alpha) reg = Ridge(alpha=alpha) reg.fit(data["train"]["X"], data["train"]["y"]) preds = reg.predict(data["test"]["X"]) +run.log("mse", mean_squared_error( + preds, data["test"]["y"]), description="Mean squared error metric") run.parent.log("mse", mean_squared_error( - preds, data["test"]["y"]), "Mean squared error metric") + preds, data["test"]["y"]), description="Mean squared error metric") with open(model_name, "wb") as file: joblib.dump(value=reg, filename=model_name) From 2bcd6c9ff89f79f90cccec78c7fdf16853d28ced Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 17:01:13 -0800 Subject: [PATCH 25/45] fix paths, exp name --- code/evaluate/evaluate_model.py | 4 +++- code/register/register_model.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index dc4ead1d..c4cf1d9c 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -34,6 +34,7 @@ # For local development, set values in this section load_dotenv() workspace_name = os.environ.get("WORKSPACE_NAME") + experiment_name = os.environ.get("EXPERIMENT_NAME") resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") @@ -53,7 +54,7 @@ auth=service_principal ) ws = aml_workspace - exp = Experiment(ws, workspace_name) + exp = Experiment(ws, experiment_name) run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" else: exp = run.experiment @@ -87,6 +88,7 @@ run_id = run.parent.id model_name = args.model_name metric_eval = "mse" +run.tag("BuildId", value=build_id) # Paramaterize the matrices on which the models should be compared # Add golden data set on which all the model performance can be evaluated diff --git a/code/register/register_model.py b/code/register/register_model.py index a9f47224..5de26b1e 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -29,8 +29,6 @@ from azureml.core import Run, Experiment, Workspace from azureml.core.model import Model as AMLModel from azureml.core.authentication import ServicePrincipalAuthentication -sys.path.append(os.path.abspath("./util")) # NOQA: E402 -from model_helper import get_model_by_build_id def main(): @@ -38,9 +36,12 @@ def main(): run = Run.get_context() if (run.id.startswith('OfflineRun')): from dotenv import load_dotenv + sys.path.append(os.path.abspath("./code/util")) # NOQA: E402 + from model_helper import get_model_by_build_id # For local development, set values in this section load_dotenv() workspace_name = os.environ.get("WORKSPACE_NAME") + experiment_name = os.environ.get("EXPERIMENT_NAME") resource_group = os.environ.get("RESOURCE_GROUP") subscription_id = os.environ.get("SUBSCRIPTION_ID") tenant_id = os.environ.get("TENANT_ID") @@ -60,9 +61,11 @@ def main(): auth=service_principal ) ws = aml_workspace - exp = Experiment(ws, workspace_name) - run_id = "e78b2c27-5ceb-49d9-8e84-abe7aecf37d5" + exp = Experiment(ws, experiment_name) + run_id = "bd184a18-2ac8-4951-8e78-e290bef3b012" else: + sys.path.append(os.path.abspath("./util")) # NOQA: E402 + from model_helper import get_model_by_build_id ws = run.experiment.workspace exp = run.experiment run_id = 'amlcompute' @@ -114,22 +117,23 @@ def main(): if (build_id is None): register_aml_model(model_name, exp, run_id) else: + run.tag("BuildId", value=build_id) register_aml_model(model_name, exp, run_id, build_id) def model_already_registered(model_name, exp, run_id): model_list = AMLModel.list(exp.workspace, name=model_name, run_id=run_id) if len(model_list) >= 1: - print("Model name:", model_name, "in workspace", + e = ("Model name:", model_name, "in workspace", exp.workspace, "with run_id ", run_id, "is already registered.") + print(e) + raise Exception(e) else: - raise Exception("Model is not registered") - + print("Model is not registered for this run.") def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): try: if (build_id != 'none'): - get_model_by_build_id(model_name, build_id, exp.workspace) model_already_registered(model_name, exp, run_id) run = Run(experiment=exp, run_id=run_id) tagsValue = {"area": "diabetes", "type": "regression", From be6a66b97daf3f3be0d3a79564364a9661e507cc Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 17:02:58 -0800 Subject: [PATCH 26/45] better name for validation step --- .pipelines/azdo-ci-build-train.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index c63960b6..3c1c19f0 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -82,7 +82,7 @@ stages: steps: - script: | python $(Build.SourcesDirectory)/code/register/register_model.py --build_id $(Build.BuildId) --validate True - displayName: 'Trigger Training Pipeline' + displayName: 'Check if new model registered' env: SP_APP_SECRET: '$(SP_APP_SECRET)' - task: CopyFiles@2 From 9d2a840c665d52f0e3e718efc783e9f042977e54 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 17:14:28 -0800 Subject: [PATCH 27/45] fix lint --- code/register/register_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/register/register_model.py b/code/register/register_model.py index 5de26b1e..4f07070b 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -125,12 +125,13 @@ def model_already_registered(model_name, exp, run_id): model_list = AMLModel.list(exp.workspace, name=model_name, run_id=run_id) if len(model_list) >= 1: e = ("Model name:", model_name, "in workspace", - exp.workspace, "with run_id ", run_id, "is already registered.") + exp.workspace, "with run_id ", run_id, "is already registered.") print(e) raise Exception(e) else: print("Model is not registered for this run.") + def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): try: if (build_id != 'none'): From 98eebf149e808973ee8bfbd9cc0a86280250251f Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 21:35:54 -0800 Subject: [PATCH 28/45] upload file to parent run id --- code/training/train.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/training/train.py b/code/training/train.py index 7f109922..5f8c19ef 100644 --- a/code/training/train.py +++ b/code/training/train.py @@ -85,14 +85,15 @@ with open(model_name, "wb") as file: joblib.dump(value=reg, filename=model_name) -# upload the model file explicitly into artifacts -run.upload_file(name="./outputs/" + model_name, path_or_stream=model_name) +# upload model file explicitly into artifacts for parent run +run.parent.upload_file(name="./outputs/" + model_name, + path_or_stream=model_name) print("Uploaded the model {} to experiment {}".format( model_name, run.experiment.name)) dirpath = os.getcwd() print(dirpath) print("Following files are uploaded ") -print(run.get_file_names()) +print(run.parent.get_file_names()) # Add properties to identify this specific training run run.tag("BuildId", value=build_id) From ce36d76a46c61ddc2bece192a5a6ace3b0bae876 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 21:45:10 -0800 Subject: [PATCH 29/45] fail pipeline if exception thrown to reg --- code/register/register_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/register/register_model.py b/code/register/register_model.py index 4f07070b..a794224c 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -163,7 +163,7 @@ def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): except Exception as e: print(e) print("Model registration failed") - + sys.exit(1) if __name__ == '__main__': main() From 2462e331364b7ea2073db30f4e4bc0c7215bbc6c Mon Sep 17 00:00:00 2001 From: David Tesar Date: Tue, 19 Nov 2019 22:37:31 -0800 Subject: [PATCH 30/45] fix tag suffix --- code/register/register_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/register/register_model.py b/code/register/register_model.py index a794224c..ab1e6d76 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -138,7 +138,7 @@ def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): model_already_registered(model_name, exp, run_id) run = Run(experiment=exp, run_id=run_id) tagsValue = {"area": "diabetes", "type": "regression", - "build_id": build_id, "run_id": run_id} + "BuildId": build_id, "run_id": run_id} else: run = Run(experiment=exp, run_id=run_id) if (run is not None): @@ -165,5 +165,6 @@ def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): print("Model registration failed") sys.exit(1) + if __name__ == '__main__': main() From 5fb45a887ea80f55186c0fc406041e2de8b8b38d Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 20 Nov 2019 10:28:57 -0800 Subject: [PATCH 31/45] aml workspace svc cleanup --- .env.example | 3 +-- .pipelines/azdo-variables.yml | 4 ++-- docs/getting_started.md | 11 +++++++++++ docs/images/svc-connection.png | Bin 0 -> 121479 bytes 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 docs/images/svc-connection.png diff --git a/.env.example b/.env.example index a2d68248..f6b2fe58 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,6 @@ BUILD_BUILDID = '001' WORKSPACE_NAME = 'aml-workspace' EXPERIMENT_NAME = '' SCRIPT_FOLDER = './' -WORKSPACE_SVC_CONNECTION = 'aml-workspace-svc' # AML Compute Cluster Config AML_COMPUTE_CLUSTER_NAME = 'train-cluster' @@ -37,4 +36,4 @@ SOURCES_DIR_TRAIN = 'code' DB_CLUSTER_ID = '' # Optional. Container Image name for image creation -IMAGE_NAME = 'ml-trained' \ No newline at end of file +IMAGE_NAME = 'mltrained' \ No newline at end of file diff --git a/.pipelines/azdo-variables.yml b/.pipelines/azdo-variables.yml index 64a42d5b..5d7da750 100644 --- a/.pipelines/azdo-variables.yml +++ b/.pipelines/azdo-variables.yml @@ -24,7 +24,7 @@ variables: value: '1' # AML Pipeline Config - name: TRAINING_PIPELINE_NAME - value: 'Training Pipeline' + value: 'Training-Pipeline' - name: MODEL_PATH value: '' - name: EVALUATE_SCRIPT_PATH @@ -34,7 +34,7 @@ variables: - name: SOURCES_DIR_TRAIN value: code - name: IMAGE_NAME - value: '' + value: 'mltrained' # Optional. Used by a training pipeline with R on Databricks - name: DB_CLUSTER_ID value: '' \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md index cc56c6c4..a3806b22 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -31,6 +31,16 @@ subscription. Contact your subscription administrator if you don't have the permissions. Normally a subscription admin can create a Service principal and can provide you the details. +## Create an Azure DevOps Azure ML Workspace Service Connection +You need to create a service connection to your ML workspace so the Azure DevOps Azure ML task can execute the Azure ML training pipeline. To get there, go to your Azure DevOps project settings page (by clicking on the cog wheel to the bottom left of the screen), and then click on **Service connections** under the **Pipelines** section: + +**Note:** Creating service connection using Azure Machine Learning extension requires 'Owner' or 'User Access Administrator' permissions on the Workspace. + +This is how your service connection looks like. Make sure to pick your resource group and type AML workspace. The connection name specified here needs to be used for the value of the `WORKSPACE_SVC_CONNECTION` set in the variable group below. + +![](./images/svc-connection.png) + + ## Create a Variable Group for your Pipelines We make use of variable group inside Azure DevOps to store variables and their @@ -59,6 +69,7 @@ The variable group should contain the following required variables: | TENANT_ID | | | RESOURCE_GROUP | | | WORKSPACE_NAME | mlops-AML-WS | +| WORKSPACE_SVC_CONNECTION | aml-workspace-connection | Mark **SP_APP_SECRET** variable as a secret one. diff --git a/docs/images/svc-connection.png b/docs/images/svc-connection.png new file mode 100644 index 0000000000000000000000000000000000000000..dc17e575c7c8047406324f54218ead852da65654 GIT binary patch literal 121479 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

4ZX%&uYnY_oM_aV!2yT)x(fIxvg{%UQFwytn&X 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Π_!)yui>!1fB?WDwdm`C?eJf-oTGnj#ltk{O?!@A`Ya=C$?<|a!-?vZIc*g@*f%|?r(G;u_&jHqDEaRmW%8Q( ziU&2ke;=mvny`TR5GZdP#)qF(&z_d~mFpcyI079g^uKIcrSpMC<+%DiFxm$y;1eit zPH+OYaaxt>UCc(fN*gN7I-(((;f0S^6Iuo6H{sBP&|E%qql zR3Yd)ztH#D_eA$SUFegWcxL<^Ej?wPY;)b7C;~0@K_k5$SrGb!m-KOJCIJekgj3W{y z)I213b`nnA@Wj$@u{~QP>tWyF>f<|93zDg&V-x%NKPE;qb(l=?M~Nh~QfKczsi7$fZ7cBBVy%)veyPU@!JrEmqdY&yN-XhyR?e?SoXi+7(xM&&3y#1LSpx^en104GW0u! zDaONT>Jwg;60X-~EQibf6&={FjO2QGU3xsGY(la!?W)}MCep!2r z$66hR1ivRlZhnV~-rmN!GRlBqz*=lL34t9~Zdfdk7^ZbZued*9G z$m>phNPVa|!M)z2p`lbhxG-v>>w7lje4R1w+ENHrL*RTf`A>nA_1*8Xarmw_eEGyF z6;=T-%HM#v=!`+EoqZ|Ax(N1NU+U$3pd3e)NV~POePP7pM2OBdkSJ zkT(gwD)af<-~GrCiPm{7K({RA*XEUl&$enWKi1&Q5$+)z{d>E#)aMYq{4c+%{4Ba% zEeZkUnd@oVi3a5+3bOkjHF`cc(AdSWM`l3J(=oV&h{VDPp^bcr46@H?wtL)%wOVbq zlV_d#i2n`GFUJ?s7N6BxDl}0YSmb@Q9|%EK)V%3#OUn|wyQMh>xT-am#V5KrgR8p+&D{e1lL<3)v!ElhptQZJ{CHx`ffo!X+yn&1 z!90!_)3)gwjkkxx+MP{@*(F>OD-0D1=0+allXKg><8W zumQ}$L(NJe?^=QG;KZypoVZu@RE4{NA6(k%una#tcI;+H>Fg-R{+Y+aa`A9m$ln~h z?LlwxY5rZLu5B+X-PRp1>&6|Tkl+1(DAp~bQaINWhs3Y4F64V0Z0LRA4o311qj$Vs z-fqA*6*Ex^rTFst`3K%$JLE6qPNft6>m`x-JBW3npwW;C3fE8Q*k&!Q!J2m>e)~Ev zI~RR^&m=0hl*XhHuI9|wJ$v0?Uw4u!{+lIY)W~bBFFxVvtj$3h+wUcRbCH9v-$^ex z5FAp^LIr0`UM7SG>QXZN;jruQ7_R1CAh-y%i%p$^!%D?W6y)-Ikh%gi1YhB zGv+^(!*<+z^ql?IiyS+mucd@0^l+@RM-Sh#LrgsOAQDf-PDbG7H|{#Ej};Rs_oj7M zJ#xi5xWN)4XU%ldJGe$W+A@z)X@C#KaBz)f&T+tX~Ki6n@ zWwKSop8O){kSQ$m;LC==>MgD^#+-J$iTDwO7qCi3#Ciz1qxBAAk*S1Ut_$cH$8mH! zQodtvF=if*K4WU*R}Mm0V5?_XaNP-jY&W(Jetb6h3_o(y1il)E^iTrY{>+#a$D91a zUa6_7_%WEGgew*`nd24@S65A5l8WO=n!ZWPiWkv%{Gmgws>0^^pQj`;je{&vQSHu( z)ghrZIiq9A)ps0`Z$5>OF{Y1TQ3;V^X>&Ot+=TRcd)fNihKhH-Dk-$wk!qo;&FbZr zjDMo1FuRX$Ve8v`|9K&mZl`k<-&MA3Djb&U@48m(dP8GeJN;I!=V1Tp2>4#aY8FAe ztgBE~S82;;)WF3W~fqKGZW|N{;)jM_p3yWTQlO#m{1~k%+F=fOUjg4Qp zpERfOUEkqEe}T|`uBsklx2miPj&9Z4XXw&!JGLxqZCRuf;)^Ferq?|TUPvyhSFVGT8||-THBH)gR5FlAl6;>Ddg?6)>aVNU zHX;P>XXRkT$QjcA#=t&_QP}#A;!=M7Ny^-{97{uQU|ydKVIu88#!pLz$vdx-%&B^-Iz#5xlY9S;qjPp zGwi!$lo>SG<9Q4?zfvJGNuXw_+Md+*)FH#*pbPe+1gDevXtqbLm(*0MNxA~o9(8IE z?q`a(km4dTbDO{=YS$b3mT8g==R^kEo!YAm=6PQF*0efyd=H9CCZ+)t)w?64 zB4&fDquz`=hy6=0SYXjYxot}boUuDea zrzAP;ZFKcZv`W7X*XD!4y zB})x?-}#U|gi}~6guxSw*(17wE&aU-(*e#dvBl5M>pfT8TJ8?MHG6%d%r0gZX9i7e z5^)%)-=EtmEsrfu0=(T2<((wIQ9cU;o*sPa_Eq6jtgPcsKa1e3J~j{K;gOV%kmB^` z{DoANii7v9RocWT$m49ANkm#PZ>QZG>lh6&&l36=T--8rR0i@Q7&p_PQ?&~dx&aA| zDNO1oo;XhP>~-S?5kw~5SuQ!3$)gw_4>5pzhhVbpvqG>|tKZ;-$dzCrNUKQomXenI zkgF|xbyz9j8nqDpY)n{55ASdUmq_OH5PFo$xmKuB;shCK>+|kd8_N}$N1=sZDS3XJly4_nF z1V>ul%s12UtqgKFe)1V)$2@s@0AO<6?b$w-A)Qb%yO~cmhm#(55zS2 zc*#8LjabCqhecC#yxmH5@u3T5%eKL~-=Ac>85^9pvi$jQfF?4ptEFwRmg!UA)70p$ zF^t8iVXC-xHgSb4p}4>4B9^A2vr_nu*T?uQ)0J$}Q|W8<<4KD^OA3sIF{vWrb(4lBVPJZAoH6TM41rPip0v z+8?qq1C6GwZHqW|XH#{4|^Sb1e$kgBaCqA$%?k4=cx^2*P)0&)9@;hq$^HWh*g&HqAvwj#U zN9OF@Nm{5GLzdU-V1AFcxFrL-vu0^tq?tS9+dS(8@%50BNnyKeyxlU|58?Dtv?pMX zSzKv>ur7G2pc6IsD#KZGt&qE?%Ep_}aU-SHKj4!JPbcSee&mk8UG1Vo#5 zAMr9_BiWgl$OuUap0;b5xr5WcEygR#j8vuee>PJ=7<<~(7-Y2x1{UM zheTE87YivBfeiV)h}5by%wePHjI+ocv?WLQ6zh%nztBGnGnPxgwZR{yEdVELU( zVR$6hM7ZdkTkGDcov_kXE(g9(3^$&+f>+#i)))2_V=Z;2C1ZCzRi70b+^}pRFR^{E z(Mlt;()~Wg(4L=>g%1#gvd8?!&y^bpG?YqPi&h^CE+5Hp(wzV-P(i;Hok9^oOfP1_ zT2;YjD8%8IB23ItW7;>4bXCyJG#A!I-Y_sIM7N&krFnVk z@@t{#kOTxX>eq>_h6H31DPuFtAZZ;JGIK-9Z;Yjc*5js*Vc9gdQP^L(8Eg|}DUr<( z0|Jg)xQK?_``2?{Vmx)A6h0|X#ydgUaslwGncO<+B|R}vx7$+lw6`cxV*L$pzA6{AWmX{}{1WEMNDA?<1kr8Rac0daKa}CL7ga4{?_8nli36iO6Bd>U z-Md@L%13bvt6x(A_Hho;>(^(O=yPe7FAsF@7@#E#su( zqokv4Jx#gaj$UT#Flhuq6LF6Mnv)b1}cBmVjpg_N{wu5`oY^Pj&*@64cg#+sU1 zU_I&k`-m;K2VKH(U#y6VVLoelal+&@DXe;iubz5pPh25`K?i%4?>z^b_07)3M@4kK z*A*%$i9gb$(lTc?M{+SMR<}|}F+E9PIC;9nfN|AK&puYN_ls$;$X5pv;&`MR^~hVn zjmI9bax}b-{iMNXKsu4ce8;2FxV_Lq&i{3&-8$sIoM1A0D4B}UyB9hx&s_oVFz;rq zU|-9dmNUh9kvqc9=w}MJ72fP%Qlc_IC5Ko2n5_%XdZ@g6r!MC`b{N=fWwNEuT7c-VAiWu>5u?ITK7?5w|kN~7l1Je7-Nf2Q+A#fOV= z4=gPv7&2cuCe3VTK-bXm$6Wb862X95wD5z(x5S_}*qiMXKz0SDyO9LP41sqSlbHJF zVc048Uk*E?m#M-QpoF_}Q$uG}wGf3?8qRkzuVD!MU#Vb8#{W>kFxwd({X==+i}~b< zD~RICIiFafJAiR#VM#5`B^&xA{qW>l%`VZPs zgKSJ1CYt#s&pF|Xg9DGSL-owl`vD_=pyQKvy7Y^t>jEsS8tQVeNk9%6A9@AUc>JNg z3X^b=3-gGrkd_ckw5YinM9$r8 z?mAQI9T+(0KX`k>gd$^>?zOqeM8^LIYiRL&K?3iNhSR;n{fqcRqO&hQiRsSI&%u5G z0&PE{=R%+ z@JvRld6fBGUSD4yB;FxFfx^QJVcChe*Gl*bzM%mi;l|7Y)_1XKnxNu7`{;^|@M}HN z*V-|=gi1K2V+-JBE*dfavzj0n8mKJ5AZq3XLNC)m?jzHtU!yU# zM>1r`DN0%oN-CI4bhfsl64rCiy5pIQefL@XlleQEOqAo7~5oL5|IO*GVBElfJ&<=P=og4cgxunZN zLdN0p^;)$MAK&3+P!UOip+w2*K)jEpNi2q-W#4=c$ScJZy9B`E=4}}1zU#vrbl6pn zSfOdzeDwbe{qCBLEF|;(I^26Qf0#~u$SAFkSTpPRsCu!VQ8!`CNpL3lJZtf1$CWB# zMtIJ?5ZM51pyS^G7r#+TsAp*36-q@Nrb2g%^{Y3*+soG8UaMpNApr2-%}0r4j+k1% zfD){op{?_4)y*-W&kNwtpxYvofei9j5b}t~HKbb5dw?ZGdHd-eOwlQ>D)s8Qb|#;* z`ysngh7A;6`{1*tEVzAvGH)!^@LPKvCw)Ner=o0Uw+<@^;dg+d%E$nq^eEmr1w9~U zzcS<89_Timu?8rct&H;xE1e?UmL6)Je^VIaYiik>Faj4pf7)-VEe$vjyTNAFNY z1A;2t1}G?CB=>`fNtg*5c^;} z_X}1`=E#aa<%^>p<3NM)f zpz^8R;C3#}&&OFBk9P*3wCOm#iR0Qa3a#BQ4KQ5fFPr)IwHMX?O>s18cebnFol$DI z{_w7`s?R;+C=aAvQJO%O$4tr^Kkywo$JLxlK9<~da3baqUIVNfQs`rN0LE*qsuHdd ze)YwA@TL%LaRy8dL1m80I%-L-PrPFV&d2ad#632s!ywFNg-#uSl84g%XI?2lZ@PI? z2F#T4QMyaiE~XK5v$3})7?Z)o$ZdRfUone{7{wL=bpfzN$~)yj!uj(jSYL0zPf^51 z$xT7Do^#Rg2p9}Ffbf>X@C!JT9&TZI`$MFbo1X zDNxOP0z?oSYinu7tst~oa=lEx-+ZB>>0k&L>x*D6XG`e7l+S=lNa9`e7G3tCvk-%^ z70GD?II!oOhHEPJ(v0Z53M@@-pgwZN1REI>Q&7uxE{?)_hZV|+VPj(>H{)&!91;g3 zypzr8>NvSC*4g`j*{*B#ZvK<{G=v zu_he6xSA-cu_8l3p`~wQ)t6sSJsmuq+>HamLi!;+p!Zka+DRR!MbU+U>Q)DsLNs0= z2onpbd?)bGTECf;JvU&SHu`Ko38(9hyV4d|w04uq2+{J2dTVG9Tk?|MslYLUE3zQZ zHW#ztY-BuDJyzI{v`Y!Tq#v=hODUhn*v5!@Il| zJc6LRp;qMzNeM#M0gi47?>Br2gE@SuEbCaUwz^u!Q_ve;s2b;p*-Sg(&3`luzi%4< zbVWm(vQxP$r&axr-NU~wmk$OfwU=`#<>a=u>%FpD#u96^wp_o|OWz8=|NJ?xVEO6R zCNqPMBsN({Y654#*2gs!^HL2pekX3qgwE#P>~x%VaEYgDYx}dve9;ougBPp1NuW6J z@#CYw71dVJ;+Cu0`(~jPyZYKja^e)>s;$a_%X}qH1k!wJtGr~?^1Mb^R|pT~AE;|z zC#$_norMAN8Mjt+Vf?>Axffd)lx0;_qd$6DUd6^5{g)*E(FCYn&Ec3Sd{X>sC?f2i zGDT5_Qzu}x)NUQeh=b8Z^?BQeIHgfDJz%dIiJ4=END^f zJ{YoJG5&h{n2#TR&DIKSz1_3EBm>zFu!`)R_Dx{aG}}Cne!R?HS(?} zMQ`-?2dhS7x8%1_wV>kvNlfq`73Aw5V_z5oSZ@Kcf&JP@u@o5>6hop_az5MS-mCrO zV0`?Eu<#gSYptsP^MfFQKfG4h)R#pniC-}=)lgx z!OqULIoU`IMRhht7o}tc4vP|nbcw;aXMnD@I50HL+nR4(OiTKde0D$F*SUFl5wRXe z_qg2T>GImKwrjqYh^Y2hWg(yVJVJq`NAE*f+h$l zlytKGzHQS1(Y7p^F3VeICTfPGDP*Tx8?+{u(ta3D+SUowF^rFQy8osn?iS?5l+r)e zIpG_q2MI>I1Ezn{e`{ED)8k);MKy$&jK9G(?Xmj>rrynXuUo)}FERQuH8CNUO%}+> z-SiRijrOK46YUJ8F&NG0kL=KZ<~@3DSlQ(%)mUNM|GKyH#2CU?{`17|R@x`eIL>+3 z7(He;TRaXSpw4GO#?>3Eo16AcQ{#RPQ|-MtIzmKe4D)n>$n)x7#6hfNqUb80)5~No zq4CL+M@cmhs=%)}J%(zY_sibUJ82)3oD4XW7#+r99Ck5iUYk6Ib?slE;$nnsPH?UQ z>R?GhL7k5b;g(3`ocCu*=!~F=zy!B7`MYPKeju^Y6t;8eD2kG<8vTMX($rK@pP!#o zT>NJ(5sCc#qp`8#3F>H*aWhGRjRu5g!v?P%FzZ0913^+5qxV}i-OT#x>cS7;$O6V8 z@&z*tE(BXnuG!);#Ez9w?|yVNP`V+VqJ?a^zyHD4QE=EVx~589;TALDnO?$lgIm>! zI$nWxQSpQ}4I`ZE9C7wS;f$F0s#W+=Dx9fQ1(z=Zj&O!=?HM}$X&e|D9-JH={V_I) zD9-m}q8=U7`u0Z4XRe}it-u$Di64J{#fAJAj3v@v1h1f63;b{b{fZD!s)CO6nX9WS zBuzXOftc*PJT@%?;;WnNKW$qZR1AE4e1ID5KK^v2op)ft;d+AC^Xs1D1*K0Ue!`^z zA>-R5G+K{$Dn*oSRaLKMway5F*sz3)vX5y{A{cOwEOcmHSwFoOi z7pSeQU%7@5F~Td!HNi6T;9u;x-K|nmvbNTM5L4lXEE77E5()ogY6&I&y%meA0+EkNyY|X%9-TJ{Br}wDX2k46%r*{zl_aY?Pp#iKoVXQlQhu zdyhLTcyRLdL%vqm>lMxwnDPvR1|@6#X*`N^VAu$rI^R4|1}t5Xd3`YBYmdj9781Ln zS*4DuuI*=Fj#wOHQH~4D^W%V<1K=JY_$~5b-tlh6X9WG`R3m&Yf z_ug|brVHC8jUH7c1q+`1T~j0rR8|H{JPYPEYi4_%xxTM57hpnI#%LLQMx*8{yVK7vvaHI^J4Z6yN;AG4Bq)@cxC{F_Lhg;2o8r}? zxaTU_6!xh$WKT&Fg5k~8g|m4(NNd|X;`2P#psz2HXoMUC6l-|`XlAGyx93*Ir>h=g zwwd!LwIA+57080;afAEi@G}~99F})dC$#Z048`8@EB7*jrC9@3vLa-FAT(K|%(04! z>=erIUi*mR@V-H@5|cM%5y@>BHBepZ7(HKxU+emN67R_FdxBA)m^RAsk8#_Z(-RYJ zjmUiV-uR+QYiCz@f8+8`0LCEG2x>@zpbK;z9~%=9742GY#0Ph(ZdSJ6)-uh5{QXCF2cT3GzL#pzKIjZZ<>HWu75V}JLk?Ke z7*jdo=ri0dGkC@Rt`shiN6Oe{X}?$tax}je9nqtQ7J=ZC-c< zIy8x1n#O~w(sRbku1i!D0|f#H3-cWEC;K7B;mhR_<7LjWK=ua8j}UhME5|=`O&L0y zB&!jP0e^1=nB}|W?t*X8`DwuA*5zN`OM%UsZHS)R?E%%Hq_qgxj>or#nxK6U4OfpL z_Dh^mW7&M~^-mcqfFA;c73ch&fie*sn_JM$2Z%CvaU-F^pM(@?wonaBCu=}|kc z48Q4*Pg67rw!N2Zoa0D?Ik@Q-dC;a%LK9SUJxHdy8EGRvz->Y5hlw>q+jG%M)fgz< z1goZof#Ej^KC(|d$Y2S}WXwR$uBC}XnUj45Ib-)o8<_`lE30$_%I1f;msOX}xiDLv zs)(Xilpg=Z?8Psq3pQD11(O>xGL94*3G!|%4`FLw% zZ+jgrMBI)P5HUGPQ)3M3EM7)~R$gQi%vbb*Vg|==gvLP8&V}qrj_k<^q=ABz>T>E(j#S%fk-q$=&qZuuV;8lM-$1k1(DaX))kAu!$I zif@1adT%2enJ24MF8B_e1C@5GHj+xFBuS=sMQ)c5rQ)I$b)WSo zn6`a+^fvYK!1L?ng;Rjl)bM(zxJ)lCs@$kiheeC(2FyHv_0fFtf$0^D&>N~lQReh` zp&sV$zzkN*+_90+oq~}EzZAjs(`Q$hyQp@=Hwc+uD-&eBBg3N@AgLQz<@Q@f+t$JW zSCg?I9Tqfe2~mjkDSu@Dh$`v1E;}ZD5R?~|34#=h`%a;+1Unbs1qtnB2$FCpc||%R zIsDAC?Csh^c8|()yVZ7VPEmeAi|S|w#zZC+Ze^}7OdJq>Jr2=FbawqDNrtZ|c_j^OZtcQAhqOk`v$-Wc?)*x8u@x zE)2!e`C_l0>%g{X`I1^$Qg6OVuBz_iKbz?Lf;oHv;Fz&&A-vl}Y-%rKGv3l*Ikv{B8@x3!S;c^w? zjx%C=$w-cs(Wxm;*#(6aRWz1siY)O`4RrUe2Q?QsU{;rJ!x0J5dR31pwJh|;@sP|^;>m_CB@F-eBo!YSRVE_+kYdu->M5ys&I2; zJ8fwdSMIwBzML3?Qx}R$Rg{%XhImifA0IHGkpY3!G(-P-#}UkrX`b1_d?N#{LjLP? z3=d%K3QGCG6Ttz-A)>y=~*zg%M&Ha8r~l_>}fv@DRv{5nE0`DN1esr5SIcf@-kMTbxM9M zMBBHRPvQ?=hiKQg4a#UXH$C)N_KKBaZyd(`G8S4Lzry310@t^IuzJjju-}QBRU%tj zX3mqg8 zM%t~C)N^xk6vl4g;Z+uOMDovchfd$wE@!ih-cbz&n_)SM=WsJyal2j2kD$DO<(6?mfV%p_#S9S>@`uIZQ~XXp zq>7WqA34p|B{rJWfbW4P@-C7C|L+|vyMC*s;Ft%FpDn-9zP5m?-|dL)c?~$HBtG0fJAmYeq8YEpsk*s$VEYl|@{v?8~+ksmZggsvPh)wtuKRkn;U zvlWm<%ME%nli1#qNPngi9oVq|lH02u=rg4jf3Quz(uhX8_77OtEGFGR4FK+7zJdC= z_yJnzV}QOFag*s&KjMP>iH1Uz)n}+haV7nx^jH(uOa+hlytB9%SUbo3@bs)hD zOo?MR2Gb*`A=>*PIM1vCRo;+%aGb2Tcc1ABN#1La?=Wfbp;VQrMfEE%H73JQgh9@d zy}-_v?{CQmJb$5bp3DR@W2&zKpgg=?^rYw2MmH^3c)c}1l3p$0U3AO@+v`bMzgTPQ z6Ugfsi0;KWe7$__b0-D$PYkDHW|~M*qdj5a{v{#QeAKTolvxQ$4>kDM45;=L9K)f~ z3;o?+`3sM%N~*wObsA*^l~Y;yxcwNynQnaOa8jk~CcP<@DC~a?vgxZ}r+u-H^TW?N zhfb^+=-5q;vvmt}2Q4>ro*ztFf#kYFkMuLQ`i#K8+?hndoyo+jn&(O&Q}1%`wDvfA z&1g=6jj}MG0X2g{c8@SiNfz3TsIeC9ahN!1ve(_&qkTlk+!pqbNe)~-R5@9x?HyR! zP*1ZWFplecs^Be0Je|9fsgZBfnTF>+&R~KDI7LztWfzWx z?W-#XRJ=w2t`HZRoz?ii^qRG}FG1@EI>!!u)&w^7AwQ7H_9+VdGI(c<9)+Pb2ZE_J z^uIjfcL-XTEN+aDyor%|Dc6H$mCM*~rcep1Htlq)X>Q;tc@(BslCF^vO%{RS4D|$z49nh^y*!$CMq=LP8CQ}5hZEuqeoLKd{NFgcXo5Ep+Q=eB zs_}t`9`7^f1L;;Y<(`BQ+bqK)Rg`aT(Dgx3zg$sds5OoQecRR55R-Ae8j07WbafTZ2mC@y02w&t1`nU#hC zqI9X)i@1$tHwsr*9D}3>w6rjnmRs*iY`if(G$gP7c;YnypcP*?04GvMMT!db!sJwU zKD_9TC(mH~LuwtRFMuS8RPBZMK|eZMW*UOr+ZVv|@o)273cNem z2dKeFDq2IpU363k4mj0`8M!xabF=?K35dDo4?*twz>EwJ4Ntyw9Ec|60ipUda<`g* z3ysRD7kRj2fguLvNUGbYI8p=um0x2)P1|0J)?O!v0iwZ%x_^EM`e)+3Ou5`yo zq59J&-*x>tP%}r6oSEx}arca>=?E(u zeB!-xFY`et+;pOI$=P@dFzRqvf|p|;3eGs6%^&%Kk1;_dC7aFtbIM~|CGu51f?Y<5fp2E^c{`z+HpyhA> z*Wo$(=F5L9yTeU$zue)IXe^;94FBN(jTsQ6LFk1o61SK$tK8&^;VJ8Xb_8I^_TPe` zt?iAuCX^}fksk{S_xyRs8kd4E5zbP;pQ>>bUIjuV)%UV@LW*)WSg@l@cV9_@Xp@n* z9cAwZO+3su6HP)V7afdwZX49R6a@Ht#L$={h#+e_!Urncm(}eMAc$(R1m`JGoC{9{ z@kcS#F-YDkl`$$nf5ESo@i{1Rhtl#F*k!`86B|%y#C0C-X$_IFfVW3fO}g3xw0Ed9R5N! zPX2ll;nd7b%$TakVs*F}WCAD?E_q6WlL4%K7M4oOZ%^yoI8=V2i836s&O;@)(_7N& z{4@EVeujSyWvSH8-HSwWfi9PjkjQZFY2E@_XA`79fOAJTzibZI2P2S!GyEjBV1Sv$ z+DjtK(rbEJY)x*fuXq3`9DSz=Vy$1?2zKNiMYW5$D2mWu7=U-(0f4;KaFtp&}BKlqb#U?WA68F@b zlk>+;N9})n?M?o=hoi#}5eu5%qy8{-RaX&TczSDCtxq;K{-`iQ&BRgPKE4J+b98z* zVAbn;>X)~-JUu8bAN*Mpt1Mk#J8t~3`TzeyZOJ^dzSkF!T5YEzp?WESeNpAHue}$O zI${j7*#EQ+nvwfZP*n8cV_{Lj$D*PF_VYsem$h06I_&@YiKrheiH*^O9=g1A?vztd zV8Bs+`7d(NKfjbDE(Tt6^IXFgV@=WpAyer4OO&z3zdn{H5w<_}r)r2i%3ky0Gfay$iaRUAJ`jkd3ka0QJTT<4Y{y96P&A(mL)Z&Exk6ZnApaqbpn9!H&|2MPxy^{YhtDj$*8X3{jP7E-X1>-La zjHXY||IoS&4Gd_4W3SdlQ{&$%qM*WRw+rnjBO_0=b#%nHyz5SF=?d7s7wTWcBk)<5 zjf3D(SCby;v7!Ap3psk?+CSW|1rZWoPA)E1GaDtxH*5Z<&M4cL_6VGK@dro3TnrpH#elXy6Xd#S)eFBV7J82R7;pq6Vv9`9J zzK)@6p5?~T30?wSm(ZxFx;2>n6cAxA_Pv1YFD{yCu(1l{&I~I_)dJb$loPW9L-&dIu{u%x))WASy#k0sUB zTT_W8NiP@}#_LXfPJDbgOAI$0-s$O&Yn_m{Y#|$mZdr8w+gS$O@@@hMI1~Co!Jj}B zY#kaXiuvdvUNS|*Ve0nD6yB*aD(5>9crp}o{O%cm@ZGNTn0LIJ(S=W9B1oCnREK{2C{ZJmpHIY`hnE01 z?sbTW@sppWiMJdeJ{^gnBP$LI#SmIj08hYcQLF7F`2D0tspwd<&)?UNt&pAU@aMiV zu0HmuZisfTe78U_2Od1!N@-#Uvl+Tyr>OLRk&llL;#nwTFHxke)qzi>fayR4?;AUqrFfCE+G(2Q|2H86WcwS6n~!p~0HUfXFNP{2-s;uY-oABU zF2Gg!N!b3r%=0QJn4qa7vT98NyFmWKd9Iq{h?&bl3PkA#Z!Z*Fd@*$Eav)@OTxT$1 z-Sg1heb1xn@Qs99?Y^0hj@9?(K(Q6zhBAz6j&88S;& z%8Kln>^(z9b}3s#R?5iuJh`2O?#>#8f#dA`r{^?E%Y^NTF>BcU_a ztFj+#&y^QeR%&BGd;y~6ORR^}Q123=O4AG@xS0zIk&QD5(nj!FIL#YyDFi~h9Q3$L zz2A{5g{{nnS}||F^5eDXOXVZ7iu1YcRJ^m!7WK;`QPt7i!FOA)(%w>nCd})6&Wj+{quN8Bg;$_@@B{^-t0qt+k?Bd}y@{*_6AG^VTC|=iSo!U^s0m&bS0XBsh2GHQ&oAn*%Uo5)OTq7@u=-TzqX@ z(1!_hAa2NQo7GoBJk03sB4gI+3c4Q1Z=vQE4q`fO z+amYtKY+m(8Y#(i`WFUWC3ie6vE0PNI^ckZSE-AWrpqNyeH3pi+6epw(~Kv3nNdyj z&j`h9^`l?E-efs!>wvRTr~T%$%ps#O06KagxM}yMS@&ncg&sWgVOxpc12ch}(<0#s zKj&;lF{<&R@+5tK3)J&YlZ0DI7fm|CLFV!<6kQ!d^!ia^ydS6cSn4F?kHE%30*NB&J`Rs5d%JT^WBi@`qm08iUi;Sj#n)2TS za7eRjdfGrW&Yf82OEziDR!6T+P%dyI#fY0)fh#*@|p)D|tLa!O%VIs8m zDhEFzxvJYPY$omxescT-mNKLJ@;TcUsnCHs2GZ1B*q+aTN>SwXtQ69t9kB1e#!890 zN<>pGZ$$V0Yw*Wm#d;R9MDNc^zechZB~Kv^2I5tU$_Kk9e!Ttzx;R_I2w$ksOiP#X zsp4HLE+On0woGrjxGeWLPvrKK?ZERtE;3hd|CUUw8<;QLr7Z*S27o0Hq&)4NdB$Fo ztPkZKhaf49Rnwhh_9>9|T3sxp{3c}Xa_|0j)5Fpq%azqBLNyjAkh1x=Y3oAS*{|Qf z&6}tF9p@%q4P6yR=V6k@sSsyZMB$uBt53;9Z&1XEzl*C?_1GkmK1}rbU z0|W^JaH*sLi3Zs`G_-=jZ0W1rLx2uw&Ww-fB;6^}abc!IMQS?^HbQC#G)711uR6Xp z$^*j8df8j1{!1dK$rW_8MvIm6b-B|w`6roYpkRm2c&o+fR_rM!0843mU=zJds1?{W zn-1XK?{%mghAEVc{E79i7>CBCm2-dr1x)I80Q#)f1h^ncdQaY^C%vU1p`f81aI#)S zri*Ar@K(Vv8I$)4&KYpz_W5OgmU5mq_yf`7XZmhQZ9TtslXFYdyDO|83R1SMfpwe$ z+AH(?j>!jO?)ke36zTnIIcN-^XK7YRAD!s3-w||w} zK4um6cl+=Fby0@&pe9J&_8!~D5u31#%?GIdvVU+cwJrX0Z zi<+}87O2RLk)(G5ydsiArMN^QpYB+V?T-NMOWro}=e$s(=M3$TjFkH8!VXHRiql<> zY_V56@a)O1CtquNq;8G%&^`T?tshnSFC{12$*#LqQSZ`ICL!^TXu_cOii-os8?;f>^|%Gb1u%JCS?TcK7=& zX98vjZ93eWkG~f`jRyIZ9JDz`cmwPS3%z<3Apd}1u3fCDAl&p@Vwg*aF|0h?PkLFk zx=b)R!v2~5t2?H@{r7H)&G64L+?Ct25ASD*y5JvB5N7xQN_S{i6FX20(&ZCCjouk1 zsM;@3@AkOy_@OF<3?M79{qA>@ zrl>;Y#m+GQL0C%1*h*lmsmsF;GT_Wuj5Nq{orj{eR#c}eto+L1sX=`gCiockT-0&WL=?yq@bV9)rs0=n*&sASZ!dngF_c` zGtP`#e;LDZR#n@vE8RrzH)N&GdJJEqrmR(&S-FqA10n(6j1tlM94j8;jJk{x*?Q&q zz^n;Me`5nDD>ukf6_Nphlr$L?@(^pP_D!;ym4z15r8wQ_TKWVEy7VM{~b4^b#d^;L#cKTda2vQW^7!&2kPjoLM-d+KVC#c>Bur@aHerbm&gxg(O} zQUi?{9brAu+RZc)Hpi`g`YUQVSMdY!I5ucyURuq%a)p)fCh#Z~Z*e-f`JE;))K!hd zR)2g$+46;YhnoUz_Z#Ra0J08%aK*$2Qt?q7BfwASe1XRS0uEel(@8{479iuwqv55X zKY{sPC?a`z!HZojHV0dIu)N6$tRvJuAJQc#@WYrh@n}Q*K&$srPQ^;Z4Z+Hu3P8=x z;@Z%A2;Ox3m?B2&*L1Mx$a-dMU`&ZmTTNv24G|4J9AjrNE&IjLy34Tj@XZeBG;9Sk zlD@CmQqA0O^MyK8ZkdW#2Y80Iqn~#)V!N>B%}?dd-^X-o<^@{cIL|p53Vf2Df5{<3 zvf6TR8j_N|Vyoe!aBy;>jE7Zszp?dCBQUlUO`&``GG@L86(*o>`ozxFzsDlk|A$M1dLh^)~Hz zq)`eGGy=Fp@?tF)tsK5FtrZaB@<{R!i+~-XrTaJf3MfjrI|{dE8{tT=d0iq-UzM|b znX|0+8tx5F6^ySxcxlt{QU4B}$0Ok4#lfeu9o6gr6G0#}`1&2d00h>uxhUCwkia1* z^FD9}dJfMRu>5JV5t_X{k{jW>T<3#wJED<|ot+8s6B+xH?*RH}MJFMRP=qUB4S+(H zLx>vVxeg#GQWV5qg2smty=p5*!9jcT!{nqMu4U7)K{A&L!~Y6D(hJ>FvXs23k?;-t+RLCoHrfSz>#7epm&~&8HxV0oq*>Qb|uf@;!Z= z0;Zs|Tj%??rb>+uuD1TXnh9lBQzeE!KRK1VSwLBaQQ{#0Bmj@ELvWxu7|`D;&BevV zfl}!F7lg94fSL#f9KU^lGa3UAwq<`$Tf?r9#4TX_ZqNV-f{4vUsDN=R1OJgdisjNy zOg;WRPZz^Qu78x&#o*tdJ#uBhlcfmq#%>lsVp=JLDcN!^%VF%xlChv4|E_I4y?^$h zmE01vXwbi*@^~wy^<*XR&;9cs5T%&+=GCKq5se>#hb(80fejD%T@6$B?@gd!UYh`0 z`59a}a%a8g-hl3NJ)u8C2)TPbushMnO?#f79sna22sB*Ix!2jy+`{?X*^fEMJG6iy z9`Zs`FxA*Z^W*&_Lw_kF#aBinV1!-L%-9GivP z!jC!spG(7fd`M;re%L7$PBhRFdY%0_KWnA2guut2a0mO+9#5X{vYhWCeLGMSZi3+o z)c^TP;E_4mF&v9qcY7FS6Nn&qUA}kj{B$k_jr9~}G{_hVJ0@wDI-aw0)27csd&$j(i zT$QP{+woit(;T8SyNkS@Uc|j-hm@1qziG$@; z*?%SPbp3b`@jfNmW+2etKW~tkf*!CE(t-dL`2vpwZRXA_$cf!i$K!>)I{u?lIA)dx*xut)2z7=>a1aT3+XqN?0 zU^f?`G!e+fJ@6Kmge@Rp#xQD&-2KQ|AH2M19YdC^2P_z{MvuU+_QX~=Sf+3PR}`F&3+SA>JQj5K z8^~$$RA3gkE|Al>!qFPz3ldG)`yO^laEP~pKM6*F+pF1QdEWd%_xr<;;?zbj=opv+ zzpq|;Z(ICeSU}P*f|VTojN3+vi_+7gN3bJaWGwlcAD$R9nuvxr5LXF;30u!uGP2A; zw~B?W)Xb$f+56m=@Iyx$i=-tVjCSEQ40lTA-H}KXUWwGr&uz=vM^$A@e{6Z1=zV3L z1?iu~`*`H?|0_V5z6WOaWG*zTQKYE=z>B~g(zD>Kw}483U=!rB3&;&lg{S=T?md`< zF+R;y+h4>4Y&dc*lqMFc;Sdyc_rnsrQc??B?YJ!CU#!`~M^*s11PC<^ju>FXlaT}i z^;5OFkDP2*x}Njvt96-OHoTmnhq=x}e+xK5ICl`J*c-OJrtBG@L}2x<$gZ`+3#_QE z(fyv<1vL*zDZ4h;QJb}pBju+kiu?3ovYM)Q+$1XtbyW#QUl%>k_Yf@VzF`uTNb3Av z)8Wbw3&SKeKMOo3IFGoxm>f~BWGs^MwE|@cN3EPawRZ~%4~`Vvbt{G^hWmKMHW z-wwO16y|QFiij3Z=X@dg)?BOH;sT)F0HIz3y&ZWxNlq&SoF#WF(scm5TDabq-qk72 zTbpwBu*47LNS>%OJwCNYfp)9{<*!&wO#itM2di2VzHf9qC_^I!wJeiB1~$6cJ5(5> zcPt`>ix33gn3Z5*#z6KgHup&YMG&t}K0JarpW|pGs0xtx=gDbYh+w?e!KYyj8MVW;g$O6Yg~46L6(F{{b;7Vrs1d!Z^Mv6I%bkhOpk+gUp}K_+g{F!_B_<)*CoMqwL5UV6c9gu=x=s=d^&({$ zp_~H6P+t2UC4R&;Ha`e=3^40$dX^@lVMKZ=Vuf_gfV<1&j=uhIx&)zSD?}@-KlmNY zm<0wMaDEs_5->}lK=kwQE8>P{2`(-w`nXJQTVnc(_1?&pA_P>p!6@yiW|gHs+AQ3` z_oFFzd6e}k!0q4&yX7+m;SNeeB|ql(WPTo(;*kdH zsKjXPpGe`hK6@)x#=i;-S^I0ZZnUBAGY!YhEs$G$`5Ji@v^s>kD`?9Id5>8D)p%iY z`#_RikRcJLBTvp<2c=ZO31}_g1)co(u?ozScO7JW-OpfYf<-EmiNYt+?a2WF!la5=i8uz}h$njA&$8Neyx8x4 zW5!@Hx#NFTuKXif^b0obPQWT_)Vg}L~ml$Kfco#&-s}k zSJex{%qKT#Ixel><2j{!#<~?2I?z6#sKbWJB)7|zkR_o~m{>C?;2j}iyAr*ihT>;> zssPqO_@L?j`JhdS!$3!|3;Uw#;sB&@fO~iJpOmsKM>Vwc^mM!7SEebMFA?)!Bj!Kf z^MynT4p>t_nS%etREIv;w#BsJx8xY^d4`|Vh|k^k$_g@%6!XyoqEYaeBT zfq|lih4t_`<~dGF>xB8}+|<?8Wu+b*~X%P`6=eFnAeW@QBj{CpFwNU zRgC}j9Q1<${a+16U7AX|+j+%bXpBD%o4UyPzOVUn+i82H*gvK0msyLRW5Xx^mb}`$ z=GNNsS&qN=5cyZd7!mm`R+pETH`X>*zbUS6EUqoDWj7<_Z)MkeL|l@x^WzbG6MX48d?#5v175LV=?ZTF7c6(@Tbf{pp*ak^Ew@2)B)~U(T#gwQa>F>d}{DIA?Ok+ z4CF#bz8m?Xu9s+@f%D4HW5LIdbN0OG^!*|7vj6?FtK_O=W|9lHm zP?6Wp$s!9;0L^?#-oL&W?qu}tQ27Z-`@A2LCHME=rxp`%=T}O=Ty<5E`%Y_gu#@ik z94b4fJSXRk*Sxv89VQDEG})M%nl?8#ul(n-cG+@{J(g0Y#K$FK7>wt}4X);AKh$V; z`jRU0AeQ)n=9$Fh7^fnZsXJecwP!vVj*jWi7zkbCISAlBz8)y#0Tnsmv4O!(Vf_~Q zK6sr{&i3rgZShy$ft6ABX`1<)flCU$zJK7jhQOM64Gq$QaI#AI?1CQsK&)Ra^{g~v zu>Xg)xq09XeF?W(HnWRS9j+Rw!j>?K9QD5WyBJ1momL(nJy=Rlx4*@5HLu>XWBS<5 z+VKVbM$hHAyUbJKh4ptA!}9T$ipw4xWY*0Fp4{%vb9(N0 zfbm4iIT}HR04xP`35>b{O)+h*?sydH5qP>`u1XBEq<84!w`M>&UwJ7m=DWW(ikN){ zfA8-@oy7(mCt>YsUeBG5WIeTOhCu|$xs#`Z?gYc7A!W#mG)5o314@T~R2d@QwI1XZ zm*&vY1t7~vZSaV@$G@KN#VwUzs^7m| z27J@oT$iUNr*!r1=uO<2V%4(l9lv~eJR!*}a|;}?LjI?rN!yB}*?eqEya4g06bJqL$r*qxpTwX8*7{Q0K7K1~d% zDfyt@k+lT0wK*I(K{i{d9)h*h0&2ZSr@y8UZ&dEaFF-pB=xOd>4r}Sp&;Rqa**DSq z_-PnCIZs)nJ=Y)_|5KgY66|Bs;_Phq5!qv0rDE{?x;jC*lLb;AX3h%>mvi9>m_H?g z0;FKfy<{Y#DEj~&@zu&-xrBd#%0>Bq#J0#vrTzH3 zP>ks(ahJ_$bxKD@i|5aECnh8w)ST%~O_FG#&W~^4qt@}7V)5JHGY0$z2r6j-#%C>_ z>lA0#fGlV|E^aldcfVL~ojL^4`t$l>|E*WPTk;#A8xC1|+6wlSvYTs|xFi;xFwZUn z1OMFj(IW#bZ?Fci`0cbKJ^`jFTR2nW?SpG;n_nw3rIR;zKDD6*f?B5G#IV9J-K0&+ zod(%7!2;=!4gm58(AWK0c+8@*IO>)B4Osgq8$75JN^0s5sMrBW>jpC%{;MHd354$~ zO(4wx;~6xVUIz1LsU){Exn_v5v^02df)K@^JfSv^%tk|5yqULGaEdoPXHb zy_s`j*g2|r1O$5qmC%Oy zDofBg%a^}MPhYbjr2qrxEm2yP3RjOrcLz}i6HMhaiJ1^T%rB4$<9z*2->ARia(`9$ z;W8+1+zwFpmN1DJa>f(vmCJ7dfz0_K%lQ%% zZ{@lIhu{8_31iG=hDSt4|8i^@;~rVh=GPBu|5}l)-sbM_?V#1L)Gem%Qa%M8T=UB> zc|vC`nppR*uqoY29QR%bgxiX38pqXU1+xj#r63O5J0fa1+=aL<)wIyAa{NE9Gq=ZR-Qm`l7C%JJmVNv0-*L(ti9I__VvQxWnpy)~71#4*Sl4 zJ-g3{z8!@JZ-eWr6_~!dj-dz5B&2I%A&!{t-;zx??(BUMw4TR5!TNe&ao+3?g0N=) z)8;($6GjE>8z$1xEL;iWa0G>ug8<{A#Ac8q&-@}F37xNz&!%{v^3U3K2sWYXq894I zMbbm*r?CdP9o{lV0>oKOkc|UqLT-1T$e$#buWo6QXt`U?U&4EjY1Itd3!DL<;|aGj zE}nT^4G$&}u`n4dY*ImHMH-T4m~8I13I@+%k<5^EQ}E)ruFSrH`T*j@4s?(9y^GAZ ztA#1Hxr4UCKo-k&A=8Q7hOSy))^#3DN@qd+WS^{-c$rVfsKu{-U95w$xkIkiFuTSuxZ@0o-7tI@?1+q{!}&ogU~`^x^<1<^A-?Hm z*vi0=!E}{7g(6k9ek&h!1ka(0h#-eu`@OrQw!)gv7P;-cFPP&bXw!BpNpcxSmg@|^ zyn28Xp;$%CUm9p&hfKikPpm&9&oS*aP%9Vh51*4ZwFN4k_g_<$1sOS%Xg2`7iC8QkzJM`J z=&rt=-r_0b44!=jU&&_mFqret!OXo1MR!k65x50e?_99?5{KM(F|C!FG0`G_0Srcv zqV>bQQ%xao{aXZ*z+!g%2u%(iw2aMj|F-Lp^zmO*xSvw~2@P8u>UgOkp12OHzz5i! z7aqwylu~G`6v><8etG4-nV+WAOyIv(+Qmt>db@x9`v0sO|7XAbJ99yP?cZ>+zu)IY z4jkXVUT@AVb`U22`**X-|BTXque0v|TA;3h<|2Fi=g*(xW0U_`!)gKQ_(RX(tMa#9 z7%b-vjEz47W{2Dz2u1wz#nh!vBEc>wSaj|`KOOHsgv@T$L8^!~w01%g6S`+>Ufv4K z3UE$txgEmSZ>+EH<$>G4>u~Sh7dSd1Ev%r^usnyexwDH)S!wB|D|5~U9>u$hPlNy* znFzU~r4HJ%!kH3Ha4k408e34wkV9yp1iD@mKIqP-B>46 zQ>GdaQqzW4Z2{!(3k6%3e!;Wu>4r@bR=kPgX>`BsrRW-;NSoLs{^crxUeDg&*c!rk z4M;nU|B4%k8^lyw_QriDlcjky!Z4Ip+Zg)O@5xEmKJWJNBPW83+#^$tJZM!DXp5>TN*L*2n zsb1SJJD)$y!em`P&{=kHD!8^R!;N=?Kmv}TB9DQ_Ec}h157|6nY6rxRwpPUvc25K! zhW&v-+WR-$YtSk@SQ}weI1Ynk1t9m@vf~-0LehP$cISb+FS65WUdp#9RpBsKfd`hs zD==%jJ?*qpVlRq=ipeuLX@9u6ipmLU_h{pRCnMKR#X@k<@ev$N;gQKsc_6HM3?~r1 z=hLncwMoPubaPX;5hd2cu=KuI2Lh3mtN4HC5rUp9Va6*L(RaoXxR%4RH`<_|!xi@L(B!8tHR3qAt z>9sC%d`^q6Q`C{A?0w&gd)(NyKjh@cMc0RE+{x*~akTs#lB90$&Fa4PDXecr!igd# zbjdDUzNJ2#(UPb~%Gjk&-PMJ@Z79!DBeaZ2CLf`xs4*~m{cT=SMd>i^($*;ty6gP8Kfz{Jf(CfiC zmdm>a-B=8dvSTcDV#rt_Y%a$-p-<8XCXR^7UhaEDd@so)WA-34WZ`)5qevGZq%To% zlx(+mE({T|eV7l@naP5)(OAEt?c>a`bR`M1GfNxbmdkKff`S>{aU>G3f;x-5n|ze1 zR@s~uUdO3eYDstLVwX~6gP!UosmyYD)%8DWC8?v2f5BZVaGg_y zLB$q>_3cY3j~?qL@P&ej9HgW|YVWvxK#2oZKJZE%0fr#+nOVEP3^uzaKwKo=qeFY^ zLj-s+s_7RKz@i$KlX!!774DcXq>@aPo8Qt&b;Wrmvp=F31}0!KRYocDLcpE#qPW<% zP@vRjPXXD+Krnm`kK@pS*BCZ{%l6r6$E^=;`Psj2+r>VslmDL1UBs;;3_k{DMBIT^ zD2MX_?mQR1Q#V zVIAmV6wVy8;r=2fNt7GYZ&)WU95;-;*86T`*0!T(NLdgfNR^|d5k?ZmDV;oc;?6;U z&!dA`%BCIZn5{?4@=>HDE&U~;O2wz{aBaIcPs?|+xT2({A~!3l>q3S==;NTACkko= zOJzxxk(yfhWaW&|zikbeHm7pA*2qP!3#QSN3cU{wOE8bGf15qJ&5E7w$@&f_heJv! zC1=x!h$@-zR*n(s`=X7Pr*sQb{*cof8${`>7*^%vNxCE~Jeaj-Q7s_{#v{Uk^!^;O6S*HQ#7|=Ga1N(l@ZY@ z(AGcJ`-E&zx6>s0hm>)!O@4=GhvJexm4l*<0zZHM=9VhF;3ZIv=HGQRN_4i(b_FF2 zpc$_@<-NEQI5hbd54EYyxir(e)KARh_eIzhp1ZcjM zQaJq<>{)y#I6HZfiz1iIC~PP8W(nGT*tc>VmG~98&8**5%#Nj4Cr5_eFT$xY%pa6g zcUILp8rmlq95Ppgo-H!B#BGY^`L@yT z&8Ubmn_Aftl{&NUwnWGEOGn&K=aEExVYag=C32+W{W%gstFJR#Fc`r!FjQ6CqW86* z_LIcTydCbWMtRK){Yll}5MIENx=l)g)ZHRDH-EN&aHbaj5gBrAvHI7tqh}>e<-7=1 zwcdmc3}?Y?lEjx-rzN&TKm?@W7*X*$%esU#%13IWhCYP6C0OuxcWibWt}~W#sZ$sg z0Y(NZlCKXdAXFC*o7v_qhpa|{ZY0X8f{XwON=x5BS0w;XcMPXbhO%6<5?@Jp*eYqj z4yKCQ2)(Yz0&IrL7k8PZk#Jus+8H>~GLq%!FDFtrCvTq$|-rtMq% zsBD(dS#-A%dO(%BgTG1lL*d??51!5i3lN%d0dqI(8Ck}3BxA(W33Q5K;y-ce8@AzJ zNI6f~IHUdqYhLBKD`M*s=6jPMG-RpsdB{bGT*t(5_6#@!*Z#*T&)I0bw($~D?4Goh z&!mBeigDUpBD_x+&E#jxxOhCC4eWM}dvDBFJ5gzHQ_sG3zLAsEve&e*T)xpTNP4dYKi zM8w366mOz&S4e+^vUiRp7RKve835`B3~}{Mt`QX_>WFJCQUVYr8@*|Y>*8r0u8)<~ z6ZM~=cn6j1*hDl(Rmo!1KCqZc(R6#_l79ipmtn8^{aJ=7!$ppYs>>EIsrV%6p%V5;(coGr!FGcy zsHs*;(ozaso_o5jKt7$Zh<)&T>d(?xm8|GzYXbr+a4Mxu;^ri|TAC4KDl9JAFY;`$ z4T4e%41X{aSLzSH)BXVdb7An)%Ow~5@;0)`%%55BPc1~gSgX9gA6{>Njaak3&Uji# z@BMLOuYuj9&Jx#a=cQTNn}Z|hrW(r)gd|loOAY)QU9O;t`xbw3@%{V{t;^AW<~AMD z%khn(+i$#7ixG^e!a~*UdOwkW^%G#0=tp~JkWT6?kgLlWxKUcmx#8I*`Z%^=*}_B3 zTdP|#SbaRJMBP(Qkl8|dQJ}g5x7y)th=;-&ji#CJ4++dDosP6NC8|r9d_}Ix`_@Br5UJO1TijB48- zmKM?a9ptunPq7pzISQCVr{xIA@nfx26k^?^=$80&+TM3?Y05{{8m$*ZkjvkGF8es` zHnD$Jr;n*5qjq9`O>`6wLkRf|c~x_P*p@tDLHj$MSw$sixEI1=nV)R;<5l}*F=CF! z^K<0WF=Uf5pie5ltpb zZ!d+khUl5?cCA&$=*5ii5l{2|Fk0*V(CIvTK`*Gq%yP^H-{N}G6pg&TU9f~DhWs~DttTmg2Et6?;4BbBng<|s=Frl%6McBbGMIz| z;1BT^DoDbLW7YEJn248Sj34k&o+~&k%)XnIxXj)t`cBd~My zY@lS`d>qn{3OP5?$&aB#X@psqq{=)sPT%Gn@(HJAFGkTbIHU)A?Fn<4_GEv{AuH;` zrPHQ8Mbc}W4H51^H+SAuls`WMjn(RKrce`JM_4LPFC;o$#NY_b3urwVWaHWCmmurF z;0zzmMsqM6B$Nteqg;pxS2%1Np(fCp29gu+lb=OZXWt z8YJFUxFEzwH*Wbo;ZZFn`=d0WwTr>xR?gH)-6t0-Iv?%iZ35M^F3cc02;ctihx$tJ;0kjGIE4 z?j~6uEqQhrEhgO+yLP;Rj?DQ1S&1!e!d_Z^R+^-{(E}vh3i8CH!fq4ur_vw2F2*02 z5!0v;DY4nz3pp9T!Ilqpd=(6tSFiN46`MX)T0ed~mCGcL;x7uG$2n@#RF~NVbMyl2 zsE!SHjxXE4|0qOfkfey$87`Tum7nV-a)(SXS`WK&1L)D+kSkF^J?Oi*@#E0Ms4lK; zjU_c^w10`fPCF?VP5U8}K$x&gzUp=Jnyz<`$82xX)2oW_djG%_AYbJ9PSByZ4Ym>7 z8-ZTo*e%+Wm`{FTy;@p1_=1G_{_#{&2@YxT91JI24FE8Ma420*#j2&j13%n$Cd$Il z*|6zBXR)M7g-S8~QB?K=(%K*N`lSYS=nEg)6p{?8;$p(8w0WL+;7C36ljP+wxEO#V z+4dthuN>QB;uIw(5L#7f6;Hqs)_r@Y7v1t=Dck)on9kJ3IEp38!M<^&ziaVr56zg zAm(ygRg#-RTVZyV^}+@e=kBa44g^Vg+wAe~Y(in$ctkD@&*rh*DW(0zn$>5OsO+0M zT<0~WUo{GPe#MoE*)nzx7X$Vi*90O_!kDozxDAS^o^}8U6C)P^8)eoJY6m~n^suai& z7ZfZWOgA0t9^YT2m2m1N=4fayDblMJoUN88J9uiAuh>rSZXhMwrih57H`kQ8FOPc) z)cN*&`s6hstT5wQLUhX^{pH2j4-aV4s9xxORM|-ziCLp;!2H26P&v=VaFEJdAu#vZ z{Fsg7nW*jMU*Xf_*6R8Q!rHKpsb`Xx)t01Z`(h<|Z1j>+M+v?WC1G*7d~CNdLl=qV ztaZ=C#Yw(ITGIk#wm~BHMA)qsais{i)ePw5HF@63DnztdMv2c$kv} zF)cWM3)d#S1r~kC$-c|jCQSSBzf()GEst~;$23b z@hzkDvQ+tZa>m`Ww5 zC`w_JAo~`Z4E~w|cM{uRgX3+ya2}dwH3do9@Q@HTtJw~Pw+(zwv0KV1J5tMGWC1F3 zJctM=4b4xDOo%-xl7>#y1siMSzj9_ZtSq4Yucio6YLl+5t!0sNUxrMAd;s8MOneFOPhCtA|B{4+XzA#P+4d(Lx}^e+f4TLvv!8{l z==vNE;*#QmV&la)CFDQA;~5r|mAUYpev0k652tWTH z(_R1L2YD%Aj2J5f1_suj1&l(fBakT*6TjLu{qg2s8$#XD^NoMgKV8HYNf(iP7+A}s z1J6!?A%NT}dZI&6F>tQ=8Wx5@cn&0gJ$U$#+W8e$D~kXf296iJS^I0+>9(VI5lSl{ zAqEN~4fEgRoeSf?D^`g#sA`$T>@>jbV(6(8ymkN6+x5pFpaQ6`8iX962LbcqfB}sB zIeU;l2nd+5UTHxB+lYK3i2Dt7B@(_RtO??#NE zHwDyTh~paxGX}DhKCiLS7eq)99cEbPY6;pq_@6Du6Xl46DmyznH`f|=Sb(fe-#HtO zv?nJg1N88=wpPgKEzh8yo?b%_ZyD+crtOmyw6z=wstDHo`qwhk9j@xEX4#}Z0@^%0 zq(nQv$=KYO1h5peMr_EDv_G)2xcT7&OCxa#`i$f3%XN*SO2PNTn-Y}j$-bfk@#)4- zHmvtz0-RZ2bw~w>eA8=Wa9V$Tc#OOFNcG0vPx3g@sL*w!l$3H$ap7+ z7R9QMwZ9_wbZ*=UdkI};U>`#>SKTm7(1-o;Cv3(&i)hsms2Xy(kLQ0Y6KX4VZyf3JTzRs;e`hDH>7Ir{7<2u_Rhz04r-2G zE0I6mri_K%G1_Ktjf?XkAt;P?Bd;39XvA;Qx5gTM0tlVl!K}yM1CHOWau*Y}J*AXN zdD_P>7dkIvZSfQaf#e7&O#^#puFySJ{9CUHC@@{W-ca6ZJ}~o3$A~T;fDz^`L^%HT zK@zil+Pa~i@1`cdjwqHQ(2mc;5Rb#*0o;f8i2=_#I3bCpClgHEB2Q{CuP_H3EZvPaEP(v+EBITegxv)g^Dgr&-mHRP9e<&u zfQ-koS77b0fD|0fjH#Cin#>UGcnG6PScrHS2;cVOmM{|nL$1vXS&A?B_x>Y^P!T4eKpU5uJ^{A( zP9hRjdVs`7f%g^yj1|8%wEYsXd(FuG(6qsGeI5(#0rTwyJB!Z4$_2dV*q% zM)=XQI{A;oye~_z@UHnL<4|EecF1A}@FDos;GZLyjkKG;TxxQ=DF zq`l&j5hbQ;(H8H;F3RpKD{h!@IqPt_UxZr9UJ_*wS{)>R+!inJ_)Q=p&MQkMM)J-O zrNI^ktS@LjFCo$qGyoN#^M;4F3wLp8?5lhuVb*w`3sks#XLL3;S3rg5k}=XV$w4L# z97UmC*cB4pP1h8G_HCjK++FMjBg>dlJ};?(&^Qb zr{icID#qBt^?4P3zwTt2>7QjVQG$B|$UZ1pgGjWWu-Veu)R&fOz9Jigru(OZZ&I_A zQ^oA#x?QX0h`8ag-s1J?iVl!&RRpRe*uBS;IxfEc0|Y=6L9kw~ z*C20(y09-rE)R085aJFp24Z?9LoAumANV_=IS-F-l+%wal*Mogloel3P$N@7mZ#l}l zyMtnDwNqx-QmVj@G4k#l#m$>H?G}ijrjz$wUSVOBpvE4sXm+UnASCn| zxxE_I2$H(jm!HyKj2+B-&c%p>i<>xQ;L!j6ef!w*RaLGC1{F>rSw=B2G3}#6*8Vgp zYAlgko|9IYUwY>n8K13sFSZ#tIO$9qB%B$fKFwp76l2L5eR)s5DkUk&LiHu+a57$Z zm}PTYxVcr{#hVNdm*2J7L)my((`%$A1&v1apawHiJ#D0>E6Fm*qaavIFK3jTNdx^> zzs4{|RnO?Cwt&WStTrP$EA%WZlnmZslm}ru4?39w9=V5rl;2^6PKFpIC1rue{u<^Z z>b(->h1i&w>E12!DjqwV)5Al_DpHA&+g7c0*^To@g1r~v+p-5rRhDYBpnRan=SZ+I(y!I zxg_$XC4>57gB0rvl{nuVL`Iu+^e=Fp{tq=F5S0)liS zsiL6dAw{I6W1i#v&#d_{^JUiDb=SJCC71J__ZLs>{p|e|PYISCrGma1NsajLuj#1Z zw_<-T(opUH>kmFl`t|EqZ*TAG*8(q_X0o!fEE2EVp4KG?F|)BXzsw#(UGJp)H@ARZ zKv<4nj4nrN5E&L3g@#+lQGqJ+`fd4gx>c6E!$z8|YK6kLgYl*Oi`Vg7uXbX1xYwAm z-)NIxv{Pgy?1J|5vN9J3hvocgK~!Rsn(K-_as0~KnqB_&pi_1`Q0?5_1rzpJuDVpSC?fZWK`MVi00kbzv8#WZPLBX zdC6J9JVxb)S#xlnwXw0WmDMxrJXZN^EpKmccXxMgab-2mCKy@0XJ8Pm`18}{mV7pa zK%jk&LJFI=N6yB=^3nhAg$Ep4FiGL0pKG<|4yz|=Uu>ePidzpS+v8FuMm4&0j^u2) zD0xiQq;MQ`*2gR_QPVLToNIfHV?`a)1U&2|q|UW#UTlQbm0_vo%zK1O7@$V;@bH-G zZwHI^%Q?|ATWxdm!AK_w^WZ?Yac7%4XzNJB7NbNuKXB&A`oM>n5TR09SXg**H{WkD z8`2aG9>4Qnw2>{TeAO3u8NQl5-)16z0{lRisgI77UcAx1m>6V8x<)8vKEt)U`p=!* zT#G`7bOl3b-1z&+JeHI^D;%8DjRa{84dsv&i*|Gw9|I5EJqZ-wj8%w8TswVt-8_e^ zKfxy_+pxA>EcasT>(AQsoJ&hfT+8~tbi>ngdoaL$`e8_S$}W7^hIg@RI5Ga3JX_BI+mo zgx31{R32V(5J^2T zk`DKAplX2iBPHZPV3b=P4hZ3i@8N|m@$cy-;o{<=b-v>MNYYq^M7^%~Ll(@V_+Bqc z@fEqnE~@kgi0>o#*$*mPW-OD%uXT2IqB{Xs9A-@%otlE#UM&q{H?F#&CDkHS7?lw1 z10Z%2C@SklWbKbB8Q4azDfPdpt<5wDAC3=DP(_W~UBwgJSy^6IO1HEeOt3Oh!;zy< zYr(uDT5jXVQ){OpNfD^I|3bt3$%Z9|q&*44I8MBy2Rl+>QBOBPHx22*OWV*&N5vR_ z?XgU`j8ol1)~7v%=;-D@LSNe1NUNURKhi>$d;c-+lSBo99;^zoh3uDee5aPB3Y6;<$+z-5Lt>ULgM1-4r&%fS=_j@U0bmU zd19p8VmO<8Orn!AKV$7Ejp!9j$!h-iQg_$i;-U3U=nosO#9bw`eX?qd40H;J9slto zj;p7~?KDRPrHdYC zOR`qnYUx?7@vgeD7>8L!CAw`E3t#h=-FVrDDj_8edZ=K*tV2^C=$*6nXw#lp%|c(T zFoF3Fr+98@{F}*3t17mHEy0oFffMlYB}8n3k+y_cxD+sqVKJ!0a!8$Gq+g!>CG%1) zsdw6iZF@n^8_(l?PB|BEAld0CCSKMNeF*vBtQ_RjCy9bwHc zBVx+mk=PVbf5JsnSMJw;OVYBnogvSXq$PYN7FX{uEsc5t(w32n4oo&Ut;FcUVp2r` z@V^^qWD>YxZl27nC=uan0ctjg)Tah=3`W zSCCqdJb(8tski)|1J_saoaoh_fq~ApM^5aK_-WzDksAyS?7Rt`YU0YSyuM(uIQJyU za(H#sNrFV3H#UzDHR)zYOs0)zgKEqYzZPjF{ViU6(@|v{a$N;s;p9-@!`c7P4Q=EU zGigoBK8GVC+@HI@dcG0Vx(%HU7~CE>IoWs;wDtJeWR9UStlI{y=+2!R$)h5sI5;?9 zNNvs~rs&?l_eySkFwQq=9ZbK#Xq#3bqcYmm_ zf8vL<&t=bLJrvXPj^5S`6L8Bz#cX%Vi&j!P2?Xdo?jiHpT{V&%u?a1cSEA>m;RtC z!XoWfgO{+2tE2m3fOq4Tu4`8(n)zsGInvftMgMuMhj!ql+sse)kNc?;dsex>b1Mj@ zbA(P>%Xo4iGwE7myO@jwhcn;C%b>D@EJmG3VOmPqy86T(>G@Jv^w*~nhDXSNw%k7y zEd)31;@ng8%l91sEgz}~QWSAR;a<&uv z!}J_zZw{nN*f)3*aTF41WOtfi6&@`Oz?hXk5po_``U>wjRb!c89w|!@+cLb5!g(DA zUXG*%cs4iWH#G`N5~!FazL8oxiI5W4NDApex_P(n>Iwbz_JJ zWqj@VZ0|Dq_8^mLVX>pt?AVA|e@40CD`Sm}SU&1R$C<9<5i_!m)DgFG8k@nY){;64 zhWKk1y+oJ239_iO^MknaDoaO>W3?W8+%`7WZsiuZXuC}nzRFQ9)B$Jbvc{ko^WcG1 z^eS;tRv;hkmzt{;uDgs<-j5GNJRWf}58^F{`||Ko4VRe8=OWvFc-gwjj?rDmp{GWT zZ*j^S(KBCtJVN~Gam!!z^oLY87PPI|GgJfZoIhFSN7{!OrW$;O8(A<&l^jkA)@Fu^kj+5Go`}25&g-Ah7)ywNI6uC*5 z*Kgm>#SVI~@^swFU6TEru~gqkz=EJaQ*-0@Me|*0Lo$ZNT}6nZ>bnWr$OR&BakDX& zM18LkfBZ1Ei{kT^AGxHPGi!)%%T0gHQ|*4%JSRKdx2R=hqDtKk8f&UVX|e$;9I6P< zoa|@j>|$3W$SsMlhgBF#Q>SOsv_6h#xjmri>Wz4)!H==7zGrJSRcc{?S3TK9Xs?7R zKL_o75Fn-xo|zR|(tJq_?vT$`(J0P_vdczD6?_ zp`_+RogS5C&mjGYNbXQfHvJ`i1ZSp^um{3`N+d)wJt4mHGroPa^_yI^O4=IM?Dz=q z=9&~t$}(lZo9DEtX&Elb>=mb@S@gHq@)ctnOk(~vMg26EMp?I5T;meKx&ZDVvI-OV z!?x=+AMY0nq;ABTU>NyRvRngRE#ag`VR^-JpZF*?|H!tc8yN*2evZVBju>7}-!sEa z_@ttFo1q6!^hf4)&#`DFF5D+N)~9$&;g&^Xc<*f9sS;E!+^{1=_an3A^LH?-9wc=& zAMSD%^@<>CB=@;vPjNQMNHd0qw338HUQ`umY!!yS>C27# zIZQ+NPX2HCEGoR+H@nbai4`p8o$*URw%^p(4~uA2N-~&3Z-JEaYHI|cKG`lO4|Tc& z%ZQRqr>G;vU3MZ1=BSTnSZQ7kIr*X7VP^%#i&9r>7IuM zt*T`BDVG9MI4{OxsRvK+y#tcUnq|rlgoW)aZ~?_i5CAp1Xx773=i!!B^;@rEbrKM!$`@N0LU&eZ_d9k=%CiI zdH<|TSa*e%(o#4RKHH01@H2ehG0sYU0mkWXq1sbzNIP)LmJjaTth*@S6Xv}cC z52Tl*@uBE=1`InYxGWi2(GgLlbNxSffBtHjR$L`>in^9oJvSY$B^dG3-ZWPD|o=hK#9+E-Tgyq?`LGAZl&8)ll)DJ$jern$>@-tDo$e7ejb zNrEv~-R?`1;t@C1lkMh?j(7fA`JPhfkj*jo>{mA5_t1co^sSM+lclu$4@*a2sFmK_F4v zJtnGeKY!|s-=-KrEB#QS5luC!##rN0ePKmsY2LBHp$6-fe!HlVuY3`l`TLf|=iY~% zwzL)Q8Vg?_v^R)ZiaU2POOZ1fyo6VDr^b(P@%WgwqTKvmWG!1dd~-IAxiaZ4!Jke~CeS}&mT#6ENsS`OxY|jMuIf%EC8XtitL1rI4aPlz!-4$; zQB`y#rp%HalIb(K&biq+y*`UwF zJ3d%?v_KOXkJakjwQl>X#qv!@cX7x|MZ@l@gcWsS_Asd-QRby~%UM&A**mAxczqZ6 zP14%uD@0VUqc#Olt*n_`xNhnW$lb`q&bGj@`we1$cmh>KrGBa(8zJ4wT`y3rC7|)y z=%VsUXI5-a`{VM8Ni`Y-V|687bS!%h4ENOu4qpneOJX*;W>^!>L8L=(pbbw+XCL;^ za?7(K3e8~EJKG+Uhsm8ioLi~*{aKWFvs}cjr7A{_ic+Zif))8-(Jhjz^QF$?Bsi?E z{r`AZhsT>NSgzBBSNLrEl^91JMeo0uMVZN)zuxlf9;C^qTt05T#4H z^EXzx?baB=D#iK!7!u#$IV~GJLj2$OYN|LW#t?t17_lLDPh5qa&M-E9E9z;vl&I&z z8iInkjP6VJ?75CS?9^eN-cqfL zLYGqS$ruY}Y}d-BMsUIozk`oiSw{aw>;Y<+z)EQ6@@y3AU%l&5H|Hxir(U@75~(D5 z%{t`_q5E;d~6zqkA#lSf^!ZnmYa zKPF4?o9s=C#YizJb$hW$TV$OmTMl3228UqdaXhL5+Qt=1GZa0IX+PoJ+EJcpFM`S-^k2YmY4K951@4w(~&)?LJQU&WFBCx?Iey}D)DT;26I?%cTp zHJdqTj$b5PqRNpcM%Yp_a0?p`Jc&eMW{a3pphL}I5qtKB^r$+``UxLew_pA&2F>y% zP!%~mJgj6Hb@m-k>x7i!yimm^Ma$04&e)%6xLJBcfuN;u6!Tx?|FR%P`Y0k^Y*v*+fUIaFj=E290A3e`pY!!gM0vurM z+uD)+a*>~3sqYpPM?$CT&1VX~@Ej4&&{^(2g9#~6yR&`N*4Eb1(b3e@1R6NUAowHe zv&KTK6dbGow6h;rYezlajMUWDlEt7=jM<1rXh%fo&nG5C>TEdh(x^N-J_~cs80qka z8oD?-DzBV7cg|8Rr0B2yABBRl%E~Q}h5}hUslHQS$HY*us+55WFV*wwVxd9q?pr`1 z(06euV|Io1I;1`z^!Fh-kI2)s)|s_ci*H%SH7&+x%5SM93>OAwu&U;O9e3klEdgiG z`o)J%V^=f!fgTv)$=a<~{{Q#WV0>(cU~eAqDQ*+4`-* z9xy!kP!_mw5&ajGfB}UFI{ft#)^fKYqOz*$kB0(I!B_>+K0pL1d@yIe1f#`T+S+YE z4fz8oDRD404NX;LC8%2TUKA4(i;j-gWL{04|Mh+FKhP(O-HsZubLFt%IYC8P*%dXafBTj*F7|mc;%N`%dHH?N zu201UQWx*#<)P5z#Kg;zl2i3w@z!9tW@pEHzv1M<)J>FM>I|LgM-@emyOSy^Zj4L`2# zFJHdA{_*Jy(5dABmetY2zP>&HR|XGsiw`CQ5l zJWNl^8!U;@9WS-%Dga_nO-+RXjr6lzx92K>Ko?GUB45qB1hs>FE&)-n!e}J3A7sb8~b0 zQ%Sv)aV(!&T3TQTeA;g*55$|#Y_DF$ADqXP_UN#`e?P8ix6@oOHa1o|AvrmRQr+(Q zB}>&C?=!bW-K*IpL{WFTD$QtDJD+o549Z{=5+*Oi|O8Z!OB#BB7a($EL^&y z+mz2H0{L%rI(Q2%+MR}`7JV=pPq0vaW$aT+uU<7{ebCac`EPTns7Z;YBO(d!VLh-w z<3wIYW?Yo`zoiH_lqPNgCL%D8Y(FEqx&H$>iLAo75pHg70C~Y80c4k?_3v;HQFS1- z{s)AjQUP23@5ucBi<815{Qrr(&a{ew@B?Vq%FD|`jLZ7(8~S?kgs;|{vAj; zaB-yDv{d|e5pAN;&+#no-(P|Ea&`cgs-UpH^Y^dJw*w1b-@4}JGn__Ey$z66Zf#Y5 zdFkKp>nP&NpbmxO10jlxoE+k*myb__@T*%4Y;0_djEv08?ePP-iFxD^y= z)ipE*#slA(H=hp5tyjN)KjOptjP1!Q3@%>lKsduAMyl`z6wC60;S~{_CncFuPyXAn zlrj6?%>o7l1{uE!J(C0+k%OaEbbt*H;L1Q?l!1zROemazi-Ll%AG@!M!KNSn@k9G5 zY&&azf7x9SpygF$0ET3oI2C{(sf&wx6fI7mEOI?x-pI&EOXlLmcx%%MT{E?d0?K-N z$wN!fb^izhFljtG4hubaXeFC)6wBHCyv?dFXt!9qxVXIRmvC5FT`jH$&Qf?oDW0B= zj@KvZz{!bpluD>6I!w6rxXCEHi zzaI?Bi_Ku40#DdCI2gS0`lTlB@D4;fjo(GhZp;h}7M_8_;dM`cjD$OE0-?ugvi{uV zvo2;A=yC*mdR7+|ab3Rr7l;(U+cZ@NEI5I8y}sJjfapKyq$Uk>fWD$n>X?sVtb&UpG*|&#X@jS zG9SbPrQ%4=vv0LcqhPrR66q{hM=_wC!>~O}kUyiq`T>ByeOYPg)y(xD@8e_kK*bh) zupk`b2R{XSb@xzoyscrq>F4LqZA$WUn~H#(s}zLYIJ02~=Ok(8TB7Qv2;+6M5M@TL?Kjw>r?1MNIg-QJ)BYn_lF0n@zv%k98#ufA{?d0vv zGb2jdmt#Yfl zH2$R{CUDEb4n_7b0?CW(O)&|c&GLt0XHJG7JWY9`iSAxqYE+~Gk~`9OekbwziDI|x z8(DRe!!G*c=66qD=RVp$v3>3L+DHEP2M#@5-7LC$8|7v@eD{}{9@~%w5@&fm+XrU2 z3@9aIu&iE42Ekm_`*6omaM{s^bfZ|Cz7K*9{}BK!z)d`e1xsfjV-7EHBYZF2tEVL_ z@9*6mNy2euoON-2#MpCwTRP(Jd+E?guPy}{nO>#f?(7~0i1}v&%?DC8NYL~Mm`jxE z(JlFR&0Ae6*(|Seu2@-rgQ^%v35ILOXO`HY$u*gd8F~_d@wP4ozHnF`k+s z%y+ABSDT0c7!`uOo5q*H8lXqqLi;X z$HHd-QqB4S+XHyCfIpreij5_9k1i<@40xZUeY0?W|lp zMP-;&0IRYGXYnJX9;V2r%|IxJB>;kDxU$PcT^N2qaPTQinrk5xAYU3%I0EU!2XIQ@ zQsiQ;Fy9xKmiC2#U{I#{oJku#ANg9A>%Ty}c>^7`jzH1rUmt_2!&OZJ*PrR&I*t05 z9dv-FJPiXgGh9d}SWcTAulFGx9UXvP)KdN&d%fh+m|iNWYH3Hr2s#w4kvZ2@XTw@d zK2OcfK&BQ6Dkb80;pYpWvH)6G-&n7D`J7f?Sky1kp~PUc?qkOk?i;*>?6l);z^2f& zW*GU#zla}12&AzIAAAA4qICoj$9WlC>s!-`U%p7y^D7vnJSs2dbosUY;EQT7Vasbl z&*2QT;a3X$ZqMY778jgKagrX}yJMEsdSBV=^jBU5cCd9rc_X!a1kPu&LPF$1Xk7LI zH}R2VapP&u;i$x-$3<5Q6AZg)Vt}6y_fFOp5G4&$@!h1J%)IgoH%#}!!HgZ9leT`@ zA`i{f!rs1W?czb3?R$n?PYr1_qHKe;LpIPiee8Br#=ELk7si|fseq|jk8lk#HFN=$P&xxzs zNL&c9u$X~iY(?)?1}hLky3Bg z3~m53N8JJ=@q(Yu_eG(5sgJXo zBrZAkT2J7mQ)k>5(2UbbFiCsDaQ8`Govgj1Rbc-#+sI)=M8v~~55XK9W+GHoRZmV% zE(g;oQJ|PZhy>+#1@c0NFhi{8|A@08p0v{ocBtCvLd%vCu}AYXfoCIO-=wvNY) zY!x=+B@7E|UK`WTJ$@dygKPV`LHlOSB^^#Si;j_4$FFAvS5n(qx?a4vxRn_k^x#`u zN#x34MQSYJmfvv0cg|U?5ku$jTP|aMu6jNU^N@1@{=EYqw6L` zUB_}*Y9Dk+T2r4e)COGb^QpBDu~k(MHde87pE#}j8d4({6UZ)gxk*}l_W9pmnrCqz zxdlURvwRgd{05o`H1O&(M*$ku?x*KkbN7t(1 z{*MvZQ9^6WEnh47UQlXNbD-sH&u!`K$Z|<<=wR#C+}Y-w(c4OzbeasC=N*{~f>-Kz z|Mq@|2+J^rdTVQIOYwA0@iblWAbmY}uK_?TEiElK`D3;KMkb~e zNX7sBf$5g_Qf$%`WMmc<<~nFwC#MpuVtE5Imu<-RS4t34vEQBOn$>aI=l;mJkI%^) z=UmMHl>I*MvdXGxD(?C$nExV=v%Q=9aawlr>AG3!@$r*t_38cTz#m@}q(vD&37-{B z!aPGketr`;TR`HsaL_w2aL@nfu7(PDR)ai5d&nNRhCo()2G{78@HBAgXJ==j6OJ8A zJlh1bn~)PQ=lNaMoU{YLs90^trtoR!Uh`RyJHhYMo`29UJf#Xv^L)ibb>w_UUH)wE zRBEqVb2VFbmh&%Www$cN>)8i2E;oIQ?)pl5RQtU(XgXqp@~N^O|WIN{TD$fUUD2l~z&a?^x(r zPIvp2Izrm2-;KM&+IE8>Hiy=`o`=WF9$zI@q>0=sXpxT%5}78nGF4hOvs zRPM^kIK-+@7F@eV9@L8hQ|ewGV-pj{YuB$kySOB$Q6dGf^jz4tPqTBMtTALQGv*5p zT%Rz^dX!FGkj+xd>+@?RSB*O2Mbl>N>uz6La{JeTnsJ zh?W+yRpZmAtxMZbeW#JJPfQALffycMry^*xHwS*ECLn>36$Exs&?mt1(?QJcGnG`i zsTIVRsHiCYt$32haOcx0q@F#KkmvHDQ$gcXKCA7O3VvhBe^Fo!j_Nk1e#hS6Wsuh` zbuBxqJKYXd-AuDCZta0_rZI|USiQeu4|f_*#6PsDU6~l?*qH? zj)6f2$lmT#=mZrj)nvr2}`}S>! z=AgO-1X2Ej?Ml*F2uzmC>j?Bgo7v**CB4=@J2FJkDFrsE+%7z$Kx~>E@}|*Xc1v}G z9D};x+)q5(7r)kCc6WEwGfC)^rmyA0!)~dV>zB#C?LFXbd=4kjK?T2YZ~^J)0-0?P+;RJlA%72SsouZ;t7CrnIhvdq z8aroT!I^dr+wW6>860@gX*)CUU(eOwzoDieJZPCPx8rPMIiqO#U;m-KscJAcm*om6 zj2aXKLRJSQqkfuM{87v8zfO@Z_}@8naZ36}g!TuTYtyx!beeq^(GeN*BY@GVSc#V7 zDCh@&`KDNLRQCHxpm>>{qBJ!o(?&4yqGo(@vWb|i=%k#ymX(NtOk$(N#R?yr$`G6T z?*6h~j;3zvDoZoPL*`zxLu`y%Zo;KHXK#hi-I<@>ELqA=SY51H{klYk6B84YN(UhT zvbl*ReR@NNC|yKpJnJ<*g-})pN&CZ1(s`6C(tT zwhSlweL~rCcBh3W8}74^Pw<`CZa>|`IpGM6$v`lOQzAliC8P{TA5R)QM|C0l6N|2j zd}84kWT6Q?TIKy$lGB~z|JnP?@swk&Q6*rd=8$kZc2*JBn@{thnq-!D?VHPCKUco0 z8np=0acCPR5V!2_?{92~jE{|tWu427mTLt?_GL3gz38*$<>lft7d29jXSuKHSxMoH zQpQ04O1I=^%quMOe-RvKf|R-;dx2hHYARL15Zpa9Tf$;9-@bp3Vdkp0)-R;@=C#N` zl-vdKaJr|i3i?3R{~nz7EV+tQ1bZDbGc$HD4UETO665I)c-?t50eFd1pFeLw+q!TS zd`#d@X1iH32dHHLY&rm#_k_Zs0)VsJxIHKbJUwTF8T>gY2bM=CCs{u$6K%TQcmpIG zm40<6ZiK@QNYsG<@C6Rv z*>j#efdegl7^o2N`V_S5VUQ6r2o|^BtE(MhPGJ{}6^k!*V>!B+5W6KLC3pAswgrrR~JDbR5s}?iy?ul$I#s@j`t_rl;HN6NGr3mPg$@bGGo3$BAEas`4@1g<$Z6J~*&CdskA9z_EKAxHa){ai_CvryRiB%&Jba zfbJMp$@NuP+1X44WfnuDv{VOxfN>+6ejsrrVrOfc`Znd<*hR}KQu1arvxLYDG;diZ@a(k)WRQqIjJ ze52GvXr6}=gsaDqk$BVyT18?+gWmZq?DU)Y$=eXfkA$q8A#y?my$_P+#~@sa2ixoE zdTsN`!=vAB9$G)tH1~V&BFhmo^0Lp3h}LHwzOQ_cT`Dj?QZ6QidONs$!v5%AjWzM^ zP@RvDZXhO}26J9VE5U?`?Nw2ozTET9R?|l|{egs`f_$(nFlX%W8<;zrfT_s4rEXz8 zL9hdy*qfv3&)ZtGqI`OKx&YTEC9CXCJhZGpPooj~zAY^XN+&+dLszbNuryUI0a&A% zm!?pA5w$4x3>x$zJUmfh3=~iG!oG;e%lC_3xsoS{K5kvq{n28R<*lsT`Y)>+keY^3 zS+e|SY_if36iU&roiwC#Pw>ppsr-;oV(eL)(QXixjyV$BhzAh@5Q{Z8GfV1Gf5G(v z3)?_$t9a0^c-#)XyE?Wv6?mV-YzaD7XU3P@URTo{b#A& zd{X$m{bQW2F1Ny_mI#ccXcEA^a1&Cz&!2n9VfFzue7jFVoXJXu3MBxn5RVBtTR*!p z2=kS08X7S)Iz&{L`!|Cxz4cY`Mu*o0>{em~Lb91?8 zfKwrQvn!mxyMSX^PDDhh`t#?TPtc}@hMMWmadtRw1FF193>~to`NJb4%JgYrU@A4I zl@ylBy9!-#U0=|z;=b@m>$b?^(vtO4xlfzI*ZY|dg_JPLGr>$LDhP&y*Wa;qG!d0a zM^|Pj@q>_i@iR%iXChtU#p-!*)236i?=jX_Refn9h>H^77^hkotC)$U(^#fSoDXA% z?hbAF-1zjgqO`OoV|?rpe0f=9eg-9IMw>PI{~fn4z`8_uaUe?*h!W70%7S9z9X)6R zo^#6iQ1VevTgCZBPJYV_rANGY(~kO}s?_>(&uv4iv2O*I^cbfk zb*MvOLqkJZ8RcKStqr)+U~EN>oKsdQP5{Rc;d(yrv!L1<>c)omj--)Hi=SYn7Jcs-A#olrf7AHcW$apuAg%NY+-Z?0(U>V5G_9KA zXNX5eE9PiJ?zX!p>nqW!kxyY~qLD5bqtaF5ww@qcBz&AzJQRM=e!`A-QE8SR zW`3iGKm0B{{Zl)25gu63b@AP#s=7!KH7@o8a8^QWY>KO@wttxq{`rX1Zo#{M3P zuV*teGmuzAm;J(p3)nk{j(7HP{d@Iii}%=ET7|HQjXuRnh=ao`fRY;KL(gO*L2M>P3NdlN(gqNEez!PjG z)WI-oyB8R{6)Jr@!tKYumtzfI?Csm%aBt(|_V;shgTB;xQox~SZYB*^E@E2Nprc`Fo;(U$#<8l zu8O?ii8z7x z6Mg`W-@JLVPHezgBf?#2RAAC*m-@3pP8EDV2d-jNV#ji+@pu5)D|fP%oozBvxL7Sz z$H>XZppz@6ZD(f(@5bF-Y}-d(ex_eFle@Lg;vy>xi<6TRkW4@ZA^u8vr%E_YDHI0S z(_{JJZO*^SU{@0V5&2vH&MO*Rh5c7h_7@uxl9JqyIB_*|grdVz+kyBDq zl9NA0!UU|_;)i(VWHXY{(NTqhb?{-*M$nyq#m0jm4JEp>dT-J+Mg+h84M%cxEgA3H zNqERNUG~Cw9>nX{uct231kB?Lw|1pCx2i2e#bLEpSe5sA74K+bRAuym8|)S7`l#vV zjdearYQ%YC$bdoAjUx)nx#e4)nPK1k_)(rfWkd8*e8sxN#&RQwN&`FtAj1tav+H6T zZ_Q8c!+;6&Lw(`wG}`KkDbWYe-JDr&$_}L>MUj)=@Gy! ztAyL{CPoLmHvElc3d_DT%*;IAGI%d$m|bMDVbD12W@FPqjo6!V@UBJO@Y*zh7aJ)7 zm?!#zt}6kJ#h!uvgGv>pQ6X2d*mSZEHRSNuub%TgRMq=f7u-$MWv+CMKLDdZV2pSg zA0L0W)Hq7^>~yc${LY>CU;zarh|P(hqd&EdKR$TyIU=%g1#o(m9z3|nn*DNUX|6TF z-qBIO^0gQp>SK91YAG%*4$A4St}f6EF{lNlr!A73L}y>sXj^%n;#pXto~DrhJ9Vx2 zOeX1aRLe z8+kzNM;6V(eu`B-_cJT+K9BJYRh`5o_HC(EA5_8PL0KCoia}k))NEX$S2Ibj!A{r{@mT7e;_XK)ncq`*5kE zDdEDn54oQ(g$)&uI0eaVwW7I#81u$5Kg#&*lB1}K#*#nY`e4qPY>_`zM0#gU{@utM zgJ=tTdY8uy!}IgpuLbtVolW0SO;moz(5YnH3m58g+7;Pvhnu-hDmTEPqyO8*7KRzS zJFainMqGuQTdy!pRf^6r(M;p8uR?xzv#GMu-NwcS_6-aZ@ecTcP6O%$hy+#~8Hhk| znXU&o1gk9w5*u^T+h?cKX8|Bj0JkGS zoF6gSpvDEtLoW&C^&P2ke9p4^dM5orC9tni9^Tv8sjjZ}^z;Ps*E+OSpb6U4lwVQ^ zh)fbJ0XTx$8W}NqfO>=KS~|nb>h^)8^8`A%am?J<7-#@-xTP?1#3?;ObRC<^0_Ure z)J{bP^4)StQK~!3tOzRVHBX$V03I8Svee2urHHA@M%kF!qiwg^&-?-skM-WnHV8*uPqG$*5hQIA@E}1)j({*lAVH}e}wfY0;T@1A;!i` z+KrG{0we=()yfKkZ4C)ry0pl+dJR$E&_H!a`jWQV)}j(F zhuvyUv~Hw%c@1(G+^!FQ{9T#WIvg9SMnxwjWzU6-CgI>;3=o>S(C_P;;GGbL7n-F| zKv|mXZBJAk06iZdc|lFZNE!q-g8rV(K^=6;dj2v$O%lv)H(pW4mx_=2>DVdhZ7ZdzkdA+{ShQEds5EPo;#NsgBs5+Kr_M3 zbz%R5w>BlU&!d@eb-GIPfz1L42`i5Fm3{Nv%bEr z(eTrL4<1@hT6+JRiOYC*7;Fw-*xI1GX#vSFfO%IZ0OeFXTCD5|v4$D4uzK!+iy zJ5!9BZ3z^f!pphIUTNh5RZFwd4df->a3etc7sf)wAK3hM^5@i^ksSfrBz5kMmlAS5 zf8AI=<{MBfGg}nTWVF7qur0%HlQy)pv5AZq7ZMsc5mI7NrL(9Ji*zIzx*`MLD~q1< zYh0@{4xP*8x>e(kQ8SeRp6FOGdRl=L#y=pyFKG*JK}MUxqU`cuU~1q;3EC{ z_4au$Zn9z=Lw664Usz24Q5J(UH$gE8%daTKY+R47f~3$sU_MIlaB~Bhal{R3W^Q5| z3eVQ;OMun*hslu4e!PfdsHyhMXnkeQ%k@($1iw~|hz3Fp!71LiDw^>o@A25LT$yZ? z@@FkUw5q$jrE(Irs|twi0G*5uQ>7=e&&P?^X@*xyjb+9#LI~Tp1T2kF&?^hopr^FJ z$hlZdoZ=;o%T&@Ou-xOquNo$ef9>6e4)Ab25q0kn6A^)IQv31o)5=O}RuC#1eD;j< z_@P6%A^rd28@Ke+>yF-Q@N7hv2t6r92=0=De4;T&%kud@=Lo4o$ER>YEsAPKiE8R1 z9o_$XDwfzA{LyZt4qFDLET3C14;ox%u`c08P~sE}`TY9)9CitC#5P+N^|20~ApG-F z+(b`;T)44gnzW4FkmaSWu z3AY@bDYCRJ!$_IxwI^8ECgBi={bvp?$|kbK=E#U^uIacLl`3 zLiq^DA4QZ7`LQeY%65Ha@!v|(SK+ZGA{x$pqyuG)_l8KgX*1H&FieREJ+6OOZDxZk zTwT6Bh4pzi`RyM3{W0mT$pu;Ga;Ypn55mea6r+zUPeGfuO{Ft=!_srLtX*Nk@xJIQ zgrmX0$oT8`Z_+bX1XrCv3tycM_k_7Hgjqnfnv`Pa&#hs9)qf8( zRaA>`zEv(-2a&F$v-dIMh7P{n_8y&zn8|~Ev%T&Jv_K@PBD{Cuz_upKvTI8cKNDA$ zfFUA?9||v0JexR3rnR_{JMR>5D4Z|XS>TN|YS4dQeovSwLqSOi+H`KV{))(XjB)r8 zQ|moDE_Xd_f`V6uqSOn0Y4Z0+L{WBUzz$tY-yu;7v{yH$eyF&YQE^oq?WJm^OYi2&?EqMAtTa9*B|`~sLX zCyA@`fO4c0dAV;;$ts3PmCjE77>~jJfAlZw*6fH=ew!3xfqXWO$K0Dw^3@kEM zj+Xj2Xqw>sLTO^!kMY8ezZ1P;fus1?@6iXd z+8KA(6huMjn_F9R2BkBl{xpcSdYt?zr0=IHswuuSELHNn!aF=lt$l+HS+$!4ZwchE zLJW9n$*@xW6Sfuc-v@+ZB)sUf1#qisnO0VuyH3HylQeIlOB1WtNQxN*u+_R1>rczVj0r^zfGV zTAh$Vl6KpaH5!;-84;wHL{kueQ$b+Py%O+kiSXBuswXsxfe0rgAdvl#i|mEkRwyTM z34L@)^Pud39u-M0z8H`AB?6Qb9C7(n%`qa|ci)B5D&rrJ-EtL;KcYa?3OgIYYF+AT z{L!d}0RjSsgT}0qi6f}ZX9sHC6m@z*_}C4Rx$*ID-?}z|v2I-JeDCVjtKQzbprrvV zGS@7lX#0n|8|~q=>Qt32z;0MvSn%^e1eV1AUcFrJYjUsOVelKRlbdJ}E9;ksXBK)F zv#u1zQ&n=+d6Q@+zIk(j1TtXthgt1bp*#@MHpZR@-(73}-&1agNl45Gokgd2yEf^PWAa^7E#SJLXI*|EozdT3U zDg#}^G#3<`{dO%a8(mp#Rj-ER}o^s4x-wJXL^I#y!%_$O~I!4cND~{{RA> zFXz`fs`{|+;{Vp?7vpdg+voebaP*%yBJv4Mv>zifk11Av(@ zTCv&|_`Qf6CPSeSK%WO&X?X+|O(fvRNRGTw%Q{VgMJLk@1AJTw}8R$*Qn6rxby14CQF1=X~! zSdoNRb37e3h2SZ5V-}2hnF98ILLgvt5#kR0O*wHuoi3JO{m^$@BFM5IyB%-k@ zkGX0x@TAI67mz%xi#>~P(VpILTkFspCZ7&t&FC5zPB$^bSwcfClXm%VmqQ;Om3xgG zW9Qm++ec3j)o#5l^9u+`pnd{CnFbb>JJ6@RG?xm3C-u29D|DcmkYj8TI0 z3$;HhIzKlD?4fqn5-4z?b#A}w^g59iN_2QQ0JgQw@Uj7l4dXqdlJ!|v6T1cYZyr{F zvYYHzZ#Q*UNXjN~lE`>kS3Dzjh~_9r*`X(er`n=;6|gASL~YbqUiWr8bufIOjAt&T zDUK}?A-y6+ZLh5wAL<;-hZ-k~-4+N2b%OlBE_L+TQ-{9weC#0r!asZGFnV1n4w}GNgf-VbeO>vaN z@)l3kV*b$zfj-YBVh@M{K_=@dx10683VY~mFlg19w@?P>j?O!db7U3%;(6l^DG4XY zlOXZeP%~h|Opw)*$$C#Ou5)(%v)>%E4#(LEB0=+(*HR(ECK=T#P9$}ra zb}fKpXRQL(wr(_)`oMs4aetgc5Fvy-U#>WXLY#c~-f2{cihjB!1I?56!XMQ)_s8nV zV5lDL8pry6sOj5oc$g?FDNUNqdT*zPwmWYjofL94;3ettATx8@r)giOPpn(AS$0a> zvm)vo*?J{G6QxVr%O>CKjT0n3F;V0jkJCDh#etnQ#Q zqeZOO==s0Q%=hLvmnsZKK*Yk1RIt>WCv3eC0>*uiNEB(XMkTNP(jcuS3uo|)KGV=2LTSCyx zz%%^1s>%~E1K_u3d1g65IvV(1YesnU_FwnFfDPf07$<7fRY7W${M1N#3!0kS6fl8i zOSu8MxH*3eBd=fsnq!x^84zRu+wZMgpi;Q#<+TYX_CwPhMs;P5W)fP1VS@W6?Gl!u zGWpn%yHXqpuw}Bh9Y5Tr0WD&|B&p4|$pyAm$7z#YoM6nrtG>NJg8iTpiVm}_%%Cmj z^=iLVYi-m2Pat7P2^A6V<88ll+fN{F8SJ8V;KE&HUO)BYhwunqakhX_tG31A z&7@B?Ax&WZA$fIQiwcllzt+ZSosWlLSF}Ucm8LV&d58DkGUqp`Ovt9jOcEAlYH;@| zT?*nR5hooz1+H=ySp%)bW39zoE_(hPgQ7V7%F)%MAbHaJ$r-5G{ZE~Ye_>&6*eab| zs0%w6*W%Bg)174=C>#Fe$C``KV)7X~fAaF=b9bX&Ys*P~_6Qphv0Zha(t}!#@N&Pgc7n6>-F^{cUW~8;iAgzUqTcLRPcw59A zw45{yW^^le)s_;s2^CP|n((R>j>|W>kc&chhAGBYeNLP8BUx#T8PLA)hwFR?Ke9k? zG0J_s_a$hJ@h*;dIttr%pSJk`_BO@s6&+C9Q8bs0k(sQL+Fnnue6_9+Twg+k>>O?M z>plZd1SaY4ZJWEYwa{>@i(;F{4{N9=`~0%L)%eQC`FQh~+rwfo-y*8#D#q^$F6?dI zC-uKB&%tTOsXPaoU$28loO%s%Hv>tK@ zK`45Pe9uVdCF5&m4<2`cy|Gi4zBN-vE>HzBOr9Ej32QOSD{q%~F>wh@`I3Wyy zX*q0j*EwJ&8g2i8qe1C_-xNKU9@g{}cdvAMqi2x~pyX74 z;5%Vb0MtYf=IQRv6fww*b_AlTN-U7W%+<97T*u`ine~ouMiPNA=jn;4cYe3f ztuKsR2FXT)r5}^TFWC2_%wKS^Elfgf$O)_>-WgPCumK-qVfkq(Hp#2~0sO=c-6tMp z_-t|R@vEI%LLdsNILz({n70wO6^QbB@ZAIj1+A^Ef!S&mOr>!C^Kz$S?0k|Z22_kw zTESNYVk;rs^Y$#7k)tBkdjb+O3b4-z9MUg^1-|Z*i1Y(g~ zq0a&d9JH@s^hTy@l8VeVKq+zK20H^A6g_RNt-<%@F0jt)NKNb-qXvGjKgg!05)E-3 z4Hi4Sh-bow9@74-lPIOzj!1`xhKxJiU2N4qkzaZi`SkWgtGPWw*iqA6NFN2(98|~f ziM1NTt~-p>^uz^qjJVr7!{B|J?M8~4CzheZz zzwizO6FI?TLbyOlVA_k1j)+t!Ss@b7+BD}NbFLWHfG>w4j6c(y1XBbWHq#oKUc7zVt0*NBT?Mhe zVA(-hd2bkZJ3?!moRk!bK<>82h(-Mw+;E5U8{;x1fyGStx|+{1w;sO_993+3HO{im z(i9)?wmUZS`a4wzLAv9%5^xd~;5=HWVx-4f_`r<)MCM6S(in_zgxclh+VZk%sy|rU z!2fFw%Bv&0;6^IARhwih*69yQ?rtjT-i&y;tZks}bh+BG;BrJp9T*y(VGinqd@@?0 zCRo%Dpq&S2*d=ncJDI8h8AVm)m6fJ--iakX-mnk@)@#TA;Ks}EUWIn&=$zBmr8k9z zCqe!!@)gWQ^z|Sfzo4q@gL+&{s(*suIdA?c-tyg!xL>Ez}s)ml11Wp6ZIgKJTp*?Wgn_fLO&ztZy%L zzuCGStc*ubOqz)t14C?toBT~@V)w}|(A?;v4t$b-haikrWa2s4B zR&HJfHN4iyNO(S>^d}%-f_bVyzlY;1{DTl8KQEuiWo=j~ zRlCV3zy+0?mC~9EXJaM-ZkbAEjA(FnX=!Q7_r}IXcnS>H^d1-CQF+R7Lb)3qMmUD3 z+X&n6N1%l)*ph5k>nC*FAgmg%bmC4qa!At4rUXc%R5;_?DkP{Pdqp2%vkA#QTipqh zatDrLJcWfInVJm5UccK}?y@t@lk?`}KYrZBsc<~EsBTG_XK0`aeBWT@MpD)w*+k2- z4c{;@JZv{!e!>-4K1C#zfMZ|_AO`S8z|ID`ojlr?UOyKZSXja?RjMxZowEA_c)@VQ zs@^Ahit|atwzlbxES-SRmPuGn#5TwIvtZ~vqr;Bf^?#*meG6B@!MCC9rt-5-=C?ro zF-!zKw~hKyjdYN4HdE>uVdx|Gsocjxws*7`5xfz*))=0G4y?AZwFOO#3oI+Zu|TJ4 z=Puv`qB*dFWkPB%vO#s3?jcNC4>#@8Cr?81{b7_GDB={ZD>%aZi{ib>Fzyd`q1(ab zrag|#K%B48{h!lvfoD6jugwHXSCHg-f}{?*gw3nBy-sY@`7q9Luvx<%a<(Yww<*Y| z<~?1HF>PMj|4c_vK8jZM{5^pLmHl+Y9)2LBNA$U`?8vHnbs-85BCKf9Sl)t9qypg( zPXW(>aM-kgYVgy4e~H|z4U`GnH}_!6Id|^d#o=N4RRskF*soqyHtdHB_ClJ)gWL^W z)>D>e@Y9e69haE+XzqnLANz?YI*7t}294Lt{o>+cP{4rAR zenFE2I)PNk;jDrfZ{tT;k4Krwo4awvfuWX&1&~Xq*Ro^9EIdk_3=EZ7MFZau1dukj z9n_HYO;+UK6~=+52XWRwV6f{UpZTY&fuu$i<^Y(W4j~yUup0Cn0QbKGhRAg%s!AfkD31FRV8#qL&B?iv^e^LSB!nrJ>J4|AZQ zeb=axM)%6NSn-rZKm@T=0?>-Pt1ndR0Qk}+AJnK#U^s|n+ioquoIia=CRD{|O>|l+ z;qBYgkuwsIOaH5Kd=Jjz`C3pK!i^{~S!N>

f)Jo)C@gkf^zE-!NO8EMl8~q5@}X zVltuaBO}* zmEni9XJjuwdmJXoSaBNNgiqU!eXb}a6@2Ip1NTw_dq6Ouz2e^clpsQ)(&21Y7Tt3q zqpzgUWPjR%42!TKQWUV`| zE7AJ@2!kDGHg41CJ1=A4$VCgJ9T!;7UvBx0XAIoP$oN?LeQIkfy#{~oc}aumP|1`cnV#xqzCbuttjff4U2OGnHa+WIFk^aWEZOF#C;P zjzoa)KbDG}544H2wBq+1GunIp)@>yQ*=A;}?UR)wQa;bDZcPK#_=AuHLextg)D&e{ z&E9IQhR*}l=kjDZk;K`dcl(`E?IAKQnK1IH`gZ+4R|=uG^+Jjp;?%h$u;%Y0vQWDK23{rxgeHu6Dx?7*3u zrU74ASf)a?L%Tjl7l=L$kMTI3L-)20LGf<}ZE2WI@lL#>cZM?jsUC>Up~c4^O>g_O z1HMj`AkIyV)05BpTlu{!U_$Bl$1AR@ zd(1qn92_$*p(fVG2)(?#oP{_?L-3Rr_lybqF{n+`s5&aHv`wey0B}CJcJ_)nH~BJ4 z?iSi*cSBr_m+Qo0oowI;lvTXM7Hy=0#&259zWKxx{A(6gwR$(__W@UV4yS|Me)zxW zfq)TSVCY|antqAF9>m{ciaaclolId$`RXE&n(%4Jd-)KtBl6y2JU$g|rkFE6Y1bh9 z@fC^1og1td+}xR#>DounoADOc{}%ue`xt6hy>e(&;n&`5@UN?`_HuUqy;mk)hq-k1 zs!8-*2F&MhqQ806Bv%qBI5!Rv@Q`_yTb0K@fyT>Jr6_ zD-5$TH67o*9}pt30;h32w-39Ot1H9bzU%+}PJH|0AT%l{;x^dl?l?G*AQc7REr>_X z1occai>&fY+wK)Q3RXDrI5>#W7uUkP94suz`S$>qhoepp!ftdD7s%P7DxNzqlZvXy z>dbh1!P|0l@uV|rbB(jRt)?2^M2if$axo(rsrs4;WG!B3$%-6kZRY+PFxSuP%9{=e z_aef=;7MHgI!-M6`(Y;9+HC?SAu+*8!!Joa$9ewWOG`P0Os;x?v%Q!Av*JG>QU31o z4BmchnVC0&Jx43N3i39JulXk!8W{9JkUl83!KG0IAuf=I1A9M+>Qfb!lz#sFsidgb zUk*VdM@GlS#vq!nH}K+fes$2vvKa_Cfs_ZZ?_kzMJ#^$Ln@A`JSm(Lme18;eV)u-n zY;?TP>O;x|<_}o2MD?ioaZ!yR(U5Ga~9XRxuG+l7|yka>nk z_rshl5Txm_#x9g0$w53&7tPIsX+7Nq+9Sg?WFFQz9cLb&?E6qKm(@cZdCIlQe^-%n z9nx{MUO;8SEp3z|xmi*o#TRS%4h%+~QX*z`;4(twK8μE9e9uW#?{_yC)cDD4ppPsbOKzjQ8Uu)-;=Hp!aip>FdLY(v5o8 zx(xC+(2bSmOtJ z?JK`5B^p;)X`@oLX;-dLE{B6M3#uq!_BMdSd+8=4W8k>3;g+Weo`GcWFYU0ks|?0O z+AxU@>t4aG1_q0ZiMV#Ja-W)=`vdz+ibg z^A=wYGhSoF=oc|>8*lJmqe5JQjPf6T}w6Y#9z9<;O*MtKPL<&Rhwmg7&$ga$6 z!B3`EowHzA{BnP^)&m(ZF*q<~0?k4ttZxXkn1b9dMdt}M$gSxdPHaClaUo3(yrWYw z!P@9SD@pqkqE%a5=OLrjG7#Y9Kxt!vw-ybPus6KSeBgDywkf z~*u_P19_*w9mp)um0TvTZhiQxN^k0|ul}w+&gFg@QfT${MTFa)eJGQnb z=`2IU5BoScL=HhwKO@U5p!C{|r@f=2cb$Ed ztaT3Z&aDYj1E(M=iWUDXj;Z*x+0pJ;dKa5t!$Q)usd(Mf=C5gN?C*YEZrIjUH*i{y z5Z^eukXExx6PJS>>YcsF1JpsPA>J-fB!Qh=)$zgQgN{lJY8ljWFHEGbOFfqR+0ptn zNbMMI%692}za~mXcqltEAZmPHjHAcGNW`;?Ol3yFapbLv!!OaKaBq=DolBk=)0Bc>gk>TvL|(XM3Cs8xulI8VX+8_ft10T>2*d zmz%`h1D;Lbl&dj+Jy2)MvKtN|txP__w&|WYu~j>A-{{8Hg|-V9-WwSFG-cXL^SbtQ z-=?>~Z73%pr84%3+Cfc9it_TOLLo6_@$npkqyN7Xsjuu?|CAj1;1^~kIq*F{Z+5Qq z_Eo1=faB_RrW#8@{<>~TzPPjm#ph=w6yKNrgLI6XaOg&o#$I~-;1gyut?yp{aTQ8W zmw^9z59KMenZ3s4wZ4Fq;g3@vHLpk+5@NQvzw8p-FRdQFdjiFG=O8J8FW-|$-56h7 zys?;3HDLJSbWZ#{k=U_X_G)$2ZZ9&40YzW_KOd=v-&6;^^1nqcUU>D{jel>}{~E0& dqr5h}PsbE@A)cRkdlLo!bT#$QzEw91{(sE@E3N Date: Wed, 20 Nov 2019 10:59:27 -0800 Subject: [PATCH 32/45] add register script path --- ml_service/util/env_variables.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ml_service/util/env_variables.py b/ml_service/util/env_variables.py index 9fe6d061..f83a9fbd 100644 --- a/ml_service/util/env_variables.py +++ b/ml_service/util/env_variables.py @@ -31,6 +31,7 @@ def __init__(self): self._sources_directory_train = os.environ.get("SOURCES_DIR_TRAIN") self._train_script_path = os.environ.get("TRAIN_SCRIPT_PATH") self._evaluate_script_path = os.environ.get("EVALUATE_SCRIPT_PATH") + self._register_script_path = os.environ.get("REGISTER_SCRIPT_PATH") self._model_name = os.environ.get("MODEL_NAME") self._experiment_name = os.environ.get("EXPERIMENT_NAME") self._model_version = os.environ.get('MODEL_VERSION') @@ -94,6 +95,10 @@ def train_script_path(self): def evaluate_script_path(self): return self._evaluate_script_path + @property + def register_script_path(self): + return self._register_script_path + @property def model_name(self): return self._model_name From 06396463b3ee03e05de9a19f2548e9c530aefe09 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 20 Nov 2019 11:23:56 -0800 Subject: [PATCH 33/45] fix firstRegistration var --- code/evaluate/evaluate_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index c4cf1d9c..99a78b61 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -24,6 +24,7 @@ POSSIBILITY OF SUCH DAMAGE. """ import os +import sys from azureml.core import Model, Run, Workspace, Experiment import argparse from azureml.core.authentication import ServicePrincipalAuthentication @@ -107,6 +108,7 @@ # newly trained model and compare mse production_model_run = Run(exp, run_id=production_model_run_id) new_model_run = run.parent + firstRegistration = False if (production_model_run.id == new_model_run.id): print("Production and new model are same.") firstRegistration = True @@ -138,3 +140,4 @@ except Exception as e: print(e) print("Something went wrong trying to evaluate. Exiting.") + sys.exit(1) From 9bfa9c23021841b05d9ff4e627cf4900ad04733e Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 20 Nov 2019 12:17:02 -0800 Subject: [PATCH 34/45] use public AzureML task --- .pipelines/azdo-ci-build-train.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-build-train.yml b/.pipelines/azdo-ci-build-train.yml index 19c254ba..1e729357 100644 --- a/.pipelines/azdo-ci-build-train.yml +++ b/.pipelines/azdo-ci-build-train.yml @@ -60,7 +60,7 @@ stages: variables: AMLPIPELINE_ID: $[ dependencies.Get_Pipeline_ID.outputs['getpipelineid.AMLPIPELINEID'] ] steps: - - task: ms-air-aiagility.private-vss-services-azureml.azureml-restApi-task.MLPublishedPipelineRestAPITask@0 + - task: ms-air-aiagility.vss-services-azureml.azureml-restApi-task.MLPublishedPipelineRestAPITask@0 displayName: 'Invoke ML pipeline' inputs: azureSubscription: '$(WORKSPACE_SVC_CONNECTION)' From 60705db3a67ac43156399d941cbe3ed26c2007ed Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 20 Nov 2019 12:23:32 -0800 Subject: [PATCH 35/45] Fix trigger to delete environment --- environment_setup/iac-remove-environment.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/environment_setup/iac-remove-environment.yml b/environment_setup/iac-remove-environment.yml index 67626223..2892bf5c 100644 --- a/environment_setup/iac-remove-environment.yml +++ b/environment_setup/iac-remove-environment.yml @@ -1,10 +1,5 @@ -trigger: - branches: - include: - - master - paths: - include: - - environment_setup/arm-templates/* +pr: none +trigger: none pool: vmImage: 'ubuntu-latest' From 2b8edd506dcfc4076b668b6d8bba435204bbf071 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 16:19:20 -0800 Subject: [PATCH 36/45] debugging --- code/evaluate/evaluate_model.py | 84 ++++++++++++++++----------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 99a78b61..9f4c8a20 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -93,51 +93,51 @@ # Paramaterize the matrices on which the models should be compared # Add golden data set on which all the model performance can be evaluated -try: - model_list = Model.list(ws) - production_model = next( - filter( - lambda x: x.created_time == max( - model.created_time for model in model_list), - model_list, - ) +# try: +model_list = Model.list(ws) +production_model = next( + filter( + lambda x: x.created_time == max( + model.created_time for model in model_list), + model_list, ) - production_model_run_id = production_model.run_id +) +production_model_run_id = production_model.run_id - # Get the run history for both production model and - # newly trained model and compare mse - production_model_run = Run(exp, run_id=production_model_run_id) - new_model_run = run.parent - firstRegistration = False - if (production_model_run.id == new_model_run.id): - print("Production and new model are same.") - firstRegistration = True - else: - print("Production model run is", production_model_run) +# Get the run history for both production model and +# newly trained model and compare mse +production_model_run = Run(exp, run_id=production_model_run_id) +new_model_run = run.parent +firstRegistration = False +if (production_model_run.id == new_model_run.id): + print("Production and new model are same.") + firstRegistration = True +else: + print("Production model run is", production_model_run) - production_model_mse = production_model_run.get_metrics().get(metric_eval) - new_model_mse = new_model_run.get_metrics().get(metric_eval) - if (production_model_mse is None or new_model_mse is None): - print("Unable to find", metric_eval, "metrics, " - "exiting evaluation") - run.parent.cancel() - else: - print( - "Current Production model mse: {}, " - "New trained model mse: {}".format( - production_model_mse, new_model_mse - ) +production_model_mse = production_model_run.get_metrics().get(metric_eval) +new_model_mse = new_model_run.get_metrics().get(metric_eval) +if (production_model_mse is None or new_model_mse is None): + print("Unable to find", metric_eval, "metrics, " + "exiting evaluation") + run.parent.cancel() +else: + print( + "Current Production model mse: {}, " + "New trained model mse: {}".format( + production_model_mse, new_model_mse ) + ) - if (new_model_mse < production_model_mse or firstRegistration): - print("New trained model performs better, " - "thus it should be registered") - else: - print("New trained model metric is less than or equal to " - "production model so skipping model registration.") - run.parent.cancel() +if (new_model_mse < production_model_mse or firstRegistration): + print("New trained model performs better, " + "thus it should be registered") +else: + print("New trained model metric is less than or equal to " + "production model so skipping model registration.") + run.parent.cancel() -except Exception as e: - print(e) - print("Something went wrong trying to evaluate. Exiting.") - sys.exit(1) +# except Exception as e: +# print(e) +# print("Something went wrong trying to evaluate. Exiting.") +# sys.exit(1) From 9619bd9fa9c30cd7faf0769b0160c489f38741af Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 16:23:04 -0800 Subject: [PATCH 37/45] debugging --- .pipelines/azdo-base-pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/azdo-base-pipeline.yml b/.pipelines/azdo-base-pipeline.yml index 926b404f..20be062b 100644 --- a/.pipelines/azdo-base-pipeline.yml +++ b/.pipelines/azdo-base-pipeline.yml @@ -7,12 +7,12 @@ steps: flake8 --output-file=$(Build.BinariesDirectory)/lint-testresults.xml --format junit-xml workingDirectory: '$(Build.SourcesDirectory)' displayName: 'Run code quality tests' - enabled: 'true' + enabled: 'false' - script: | pytest --junitxml=$(Build.BinariesDirectory)/unit-testresults.xml $(Build.SourcesDirectory)/tests/unit displayName: 'Run unit tests' - enabled: 'true' + enabled: 'false' env: SP_APP_SECRET: '$(SP_APP_SECRET)' From f64f2cabab6adc80ecc2cde104931b0b7d9ce0c0 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 16:40:54 -0800 Subject: [PATCH 38/45] Debugging --- code/evaluate/evaluate_model.py | 76 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 9f4c8a20..99bfeca6 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -95,47 +95,53 @@ # Add golden data set on which all the model performance can be evaluated # try: model_list = Model.list(ws) -production_model = next( - filter( - lambda x: x.created_time == max( - model.created_time for model in model_list), - model_list, +if (len(model_list) > 0): + production_model = next( + filter( + lambda x: x.created_time == max( + model.created_time for model in model_list), + model_list, + ) ) -) -production_model_run_id = production_model.run_id + production_model_run_id = production_model.run_id -# Get the run history for both production model and -# newly trained model and compare mse -production_model_run = Run(exp, run_id=production_model_run_id) -new_model_run = run.parent -firstRegistration = False -if (production_model_run.id == new_model_run.id): - print("Production and new model are same.") - firstRegistration = True -else: - print("Production model run is", production_model_run) + # Get the run history for both production model and + # newly trained model and compare mse + production_model_run = Run(exp, run_id=production_model_run_id) + new_model_run = run.parent + firstRegistration = False + if (production_model_run.id == new_model_run.id): + print("Production and new model are same.") + firstRegistration = True + else: + print("Production model run is", production_model_run) -production_model_mse = production_model_run.get_metrics().get(metric_eval) -new_model_mse = new_model_run.get_metrics().get(metric_eval) -if (production_model_mse is None or new_model_mse is None): - print("Unable to find", metric_eval, "metrics, " - "exiting evaluation") - run.parent.cancel() -else: - print( - "Current Production model mse: {}, " - "New trained model mse: {}".format( - production_model_mse, new_model_mse + production_model_mse = production_model_run.get_metrics().get(metric_eval) + new_model_mse = new_model_run.get_metrics().get(metric_eval) + if (production_model_mse is None or new_model_mse is None): + print("Unable to find", metric_eval, "metrics, " + "exiting evaluation") + run.parent.cancel() + else: + print( + "Current Production model mse: {}, " + "New trained model mse: {}".format( + production_model_mse, new_model_mse + ) ) - ) -if (new_model_mse < production_model_mse or firstRegistration): - print("New trained model performs better, " - "thus it should be registered") + if (new_model_mse < production_model_mse or firstRegistration): + print("New trained model performs better, " + "thus it should be registered") + else: + print("New trained model metric is less than or equal to " + "production model so skipping model registration.") + run.parent.cancel() else: - print("New trained model metric is less than or equal to " - "production model so skipping model registration.") - run.parent.cancel() + print("This is the first model, " + "thus it should be registered") + + # except Exception as e: # print(e) From bf323891e702155989b6db3cdd1c5bbd129a84dd Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 17:25:14 -0800 Subject: [PATCH 39/45] Debugging --- code/evaluate/evaluate_model.py | 15 ++++----------- ml_service/pipelines/run_train_pipeline.py | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 99bfeca6..9c269792 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -109,12 +109,7 @@ # newly trained model and compare mse production_model_run = Run(exp, run_id=production_model_run_id) new_model_run = run.parent - firstRegistration = False - if (production_model_run.id == new_model_run.id): - print("Production and new model are same.") - firstRegistration = True - else: - print("Production model run is", production_model_run) + print("Production model run is", production_model_run) production_model_mse = production_model_run.get_metrics().get(metric_eval) new_model_mse = new_model_run.get_metrics().get(metric_eval) @@ -130,7 +125,7 @@ ) ) - if (new_model_mse < production_model_mse or firstRegistration): + if (new_model_mse < production_model_mse): print("New trained model performs better, " "thus it should be registered") else: @@ -139,11 +134,9 @@ run.parent.cancel() else: print("This is the first model, " - "thus it should be registered") - - - + "thus it should be registered") # except Exception as e: # print(e) # print("Something went wrong trying to evaluate. Exiting.") # sys.exit(1) +# run.parent.fail() diff --git a/ml_service/pipelines/run_train_pipeline.py b/ml_service/pipelines/run_train_pipeline.py index 39cca02d..65316007 100644 --- a/ml_service/pipelines/run_train_pipeline.py +++ b/ml_service/pipelines/run_train_pipeline.py @@ -47,7 +47,7 @@ def main(): # Set this to True for local development or # if not using Azure DevOps pipeline execution task - skip_train_execution = False + skip_train_execution = True if(skip_train_execution is False): pipeline_parameters = {"model_name": e.model_name} response = published_pipeline.submit( From e0c65bdf1a0f0b029465cae028fe3e1584df2990 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 17:48:50 -0800 Subject: [PATCH 40/45] debugging --- code/evaluate/evaluate_model.py | 82 ++++++++++---------- ml_service/pipelines/build_train_pipeline.py | 1 + 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 9c269792..5f8a1b1e 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -28,6 +28,7 @@ from azureml.core import Model, Run, Workspace, Experiment import argparse from azureml.core.authentication import ServicePrincipalAuthentication +import traceback run = Run.get_context() if (run.id.startswith('OfflineRun')): @@ -93,50 +94,49 @@ # Paramaterize the matrices on which the models should be compared # Add golden data set on which all the model performance can be evaluated -# try: -model_list = Model.list(ws) -if (len(model_list) > 0): - production_model = next( - filter( - lambda x: x.created_time == max( - model.created_time for model in model_list), - model_list, +try: + model_list = Model.list(ws) + if (len(model_list) > 0): + production_model = next( + filter( + lambda x: x.created_time == max( + model.created_time for model in model_list), + model_list, + ) ) - ) - production_model_run_id = production_model.run_id + production_model_run_id = production_model.run_id + 1 - # Get the run history for both production model and - # newly trained model and compare mse - production_model_run = Run(exp, run_id=production_model_run_id) - new_model_run = run.parent - print("Production model run is", production_model_run) + # Get the run history for both production model and + # newly trained model and compare mse + production_model_run = Run(exp, run_id=production_model_run_id) + new_model_run = run.parent + print("Production model run is", production_model_run) - production_model_mse = production_model_run.get_metrics().get(metric_eval) - new_model_mse = new_model_run.get_metrics().get(metric_eval) - if (production_model_mse is None or new_model_mse is None): - print("Unable to find", metric_eval, "metrics, " - "exiting evaluation") - run.parent.cancel() - else: - print( - "Current Production model mse: {}, " - "New trained model mse: {}".format( - production_model_mse, new_model_mse + production_model_mse = production_model_run.get_metrics().get(metric_eval) + new_model_mse = new_model_run.get_metrics().get(metric_eval) + if (production_model_mse is None or new_model_mse is None): + print("Unable to find", metric_eval, "metrics, " + "exiting evaluation") + run.parent.cancel() + else: + print( + "Current Production model mse: {}, " + "New trained model mse: {}".format( + production_model_mse, new_model_mse + ) ) - ) - if (new_model_mse < production_model_mse): - print("New trained model performs better, " - "thus it should be registered") + if (new_model_mse < production_model_mse): + print("New trained model performs better, " + "thus it should be registered") + else: + print("New trained model metric is less than or equal to " + "production model so skipping model registration.") + run.parent.cancel() else: - print("New trained model metric is less than or equal to " - "production model so skipping model registration.") - run.parent.cancel() -else: - print("This is the first model, " - "thus it should be registered") -# except Exception as e: -# print(e) -# print("Something went wrong trying to evaluate. Exiting.") -# sys.exit(1) -# run.parent.fail() + print("This is the first model, " + "thus it should be registered") +except Exception as e: + traceback.print_exc(limit=None, file=None, chain=True) + print("Something went wrong trying to evaluate. Exiting.") + run.parent.fail() diff --git a/ml_service/pipelines/build_train_pipeline.py b/ml_service/pipelines/build_train_pipeline.py index 1374d3d5..fa21b515 100644 --- a/ml_service/pipelines/build_train_pipeline.py +++ b/ml_service/pipelines/build_train_pipeline.py @@ -94,6 +94,7 @@ def main(): steps = [train_step, evaluate_step, register_step] train_pipeline = Pipeline(workspace=aml_workspace, steps=steps) + train_pipeline._set_experiment_name train_pipeline.validate() published_pipeline = train_pipeline.publish( name=e.pipeline_name, From cd1da4fee161958bbadebf430fc94767a7e12396 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 18:10:24 -0800 Subject: [PATCH 41/45] debugging --- code/evaluate/evaluate_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 5f8a1b1e..959b8c90 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -139,4 +139,5 @@ except Exception as e: traceback.print_exc(limit=None, file=None, chain=True) print("Something went wrong trying to evaluate. Exiting.") - run.parent.fail() + raise + From 53f9aadbc138c6774d64e28348fba7e3ac888fa5 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 18:26:05 -0800 Subject: [PATCH 42/45] Debugging --- .pipelines/azdo-base-pipeline.yml | 4 ++-- code/evaluate/evaluate_model.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pipelines/azdo-base-pipeline.yml b/.pipelines/azdo-base-pipeline.yml index 20be062b..926b404f 100644 --- a/.pipelines/azdo-base-pipeline.yml +++ b/.pipelines/azdo-base-pipeline.yml @@ -7,12 +7,12 @@ steps: flake8 --output-file=$(Build.BinariesDirectory)/lint-testresults.xml --format junit-xml workingDirectory: '$(Build.SourcesDirectory)' displayName: 'Run code quality tests' - enabled: 'false' + enabled: 'true' - script: | pytest --junitxml=$(Build.BinariesDirectory)/unit-testresults.xml $(Build.SourcesDirectory)/tests/unit displayName: 'Run unit tests' - enabled: 'false' + enabled: 'true' env: SP_APP_SECRET: '$(SP_APP_SECRET)' diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index 959b8c90..df32b530 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -104,7 +104,7 @@ model_list, ) ) - production_model_run_id = production_model.run_id + 1 + production_model_run_id = production_model.run_id # Get the run history for both production model and # newly trained model and compare mse From eff94832974e658d3b86af92b8c6aba01cd5dbd5 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 20 Nov 2019 18:32:21 -0800 Subject: [PATCH 43/45] Linting --- code/evaluate/evaluate_model.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/code/evaluate/evaluate_model.py b/code/evaluate/evaluate_model.py index df32b530..0959d36b 100644 --- a/code/evaluate/evaluate_model.py +++ b/code/evaluate/evaluate_model.py @@ -24,7 +24,6 @@ POSSIBILITY OF SUCH DAMAGE. """ import os -import sys from azureml.core import Model, Run, Workspace, Experiment import argparse from azureml.core.authentication import ServicePrincipalAuthentication @@ -94,7 +93,7 @@ # Paramaterize the matrices on which the models should be compared # Add golden data set on which all the model performance can be evaluated -try: +try: model_list = Model.list(ws) if (len(model_list) > 0): production_model = next( @@ -112,11 +111,12 @@ new_model_run = run.parent print("Production model run is", production_model_run) - production_model_mse = production_model_run.get_metrics().get(metric_eval) + production_model_mse = \ + production_model_run.get_metrics().get(metric_eval) new_model_mse = new_model_run.get_metrics().get(metric_eval) if (production_model_mse is None or new_model_mse is None): print("Unable to find", metric_eval, "metrics, " - "exiting evaluation") + "exiting evaluation") run.parent.cancel() else: print( @@ -128,16 +128,15 @@ if (new_model_mse < production_model_mse): print("New trained model performs better, " - "thus it should be registered") + "thus it should be registered") else: print("New trained model metric is less than or equal to " - "production model so skipping model registration.") + "production model so skipping model registration.") run.parent.cancel() else: print("This is the first model, " - "thus it should be registered") -except Exception as e: + "thus it should be registered") +except Exception: traceback.print_exc(limit=None, file=None, chain=True) print("Something went wrong trying to evaluate. Exiting.") raise - From eada62fab11217db8c922d19fdcc930853b5df80 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Thu, 21 Nov 2019 08:54:18 -0800 Subject: [PATCH 44/45] Switch build status badge to aidemos AzDO --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 264643ea..e622ba75 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ description: "Code which demonstrates how to set up and operationalize an MLOps # MLOps with Azure ML -[![Build Status](https://dev.azure.com/customai/DevopsForAI-AML/_apis/build/status/Build%20%26%20Train?branchName=master)](https://dev.azure.com/customai/DevopsForAI-AML/_build/latest?definitionId=34&branchName=master) +[![Build Status](https://aidemos.visualstudio.com/MLOps/_apis/build/status/microsoft.MLOpsPython-CI?branchName=master)](https://aidemos.visualstudio.com/MLOps/_build/latest?definitionId=127&branchName=master) MLOps will help you to understand how to build the Continuous Integration and Continuous Delivery pipeline for a ML/AI project. We will be using the Azure DevOps Project for build and release/deployment pipelines along with Azure ML services for model retraining pipeline, model management and operationalization. From d652f8703905e64ffee441b3bbacba4cf5622cf3 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Thu, 21 Nov 2019 09:39:48 -0800 Subject: [PATCH 45/45] consistent exception handling --- code/register/register_model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/register/register_model.py b/code/register/register_model.py index ab1e6d76..17388d62 100644 --- a/code/register/register_model.py +++ b/code/register/register_model.py @@ -26,6 +26,7 @@ import os import sys import argparse +import traceback from azureml.core import Run, Experiment, Workspace from azureml.core.model import Model as AMLModel from azureml.core.authentication import ServicePrincipalAuthentication @@ -160,10 +161,10 @@ def register_aml_model(model_name, exp, run_id, build_id: str = 'none'): model.name, model.description, model.version ) ) - except Exception as e: - print(e) + except Exception: + traceback.print_exc(limit=None, file=None, chain=True) print("Model registration failed") - sys.exit(1) + raise if __name__ == '__main__':