From afef1e274aa0560c75382eb9094e3173e9d2da31 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 31 Mar 2022 09:00:59 +0900 Subject: [PATCH 01/93] modules/silicon_design: use deeplearning images - replace container base image with deeplearning image - provision eda tooling using conda - add daisy-based workflow to build compute engine image - add compute binding for cloudbuild service account - reduce image build time to ~15min --- modules/silicon_design/main.tf | 28 +++++- modules/silicon_design/scripts/build/build.sh | 9 +- .../scripts/build/cloudbuild.yaml | 51 +++++------ .../containers/openlane-jupyterlab/Dockerfile | 36 -------- .../scripts/build/images/Dockerfile | 7 ++ .../build/images/compute_image.wf.json | 89 +++++++++++++++++++ .../scripts/build/images/provision.sh | 40 +++++++++ .../scripts/build/images/provision/env.tcl | 9 ++ .../build/images/provision/environment.yml | 23 +++++ .../scripts/build/images/provision/profile.sh | 2 + modules/silicon_design/variables.tf | 6 ++ 11 files changed, 232 insertions(+), 68 deletions(-) delete mode 100644 modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile create mode 100644 modules/silicon_design/scripts/build/images/Dockerfile create mode 100644 modules/silicon_design/scripts/build/images/compute_image.wf.json create mode 100644 modules/silicon_design/scripts/build/images/provision.sh create mode 100644 modules/silicon_design/scripts/build/images/provision/env.tcl create mode 100644 modules/silicon_design/scripts/build/images/provision/environment.yml create mode 100644 modules/silicon_design/scripts/build/images/provision/profile.sh diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 4daa90d4..57a9f18d 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -42,6 +42,12 @@ locals { "roles/storage.objectViewer", ] + cloudbuild_sa_project_roles = [ + "roles/compute.instanceAdmin", + "roles/compute.storageAdmin", + "roles/iam.serviceAccountUser", + ] + project_services = var.enable_services ? [ "compute.googleapis.com", "notebooks.googleapis.com", @@ -65,6 +71,10 @@ data "google_project" "existing_project" { project_id = var.project_name } +data "google_project" "project" { + project_id = var.project_name +} + module "project_radlab_silicon_design" { count = var.create_project ? 1 : 0 source = "terraform-google-modules/project-factory/google" @@ -157,6 +167,13 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { role = each.value } +resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { + for_each = toset(local.cloudbuild_sa_project_roles) + project = local.project.project_id + member = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com" + role = each.value +} + resource "google_service_account_iam_member" "sa_ai_notebook_user_iam" { for_each = var.trusted_users member = each.value @@ -186,7 +203,7 @@ resource "google_notebooks_instance" "ai_notebook" { machine_type = var.machine_type container_image { - repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/openlane-jupyterlab" + repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}" tag = "latest" } @@ -246,17 +263,22 @@ resource "null_resource" "build_and_push_image" { triggers = { cloudbuild_yaml_sha = filesha1("${path.module}/scripts/build/cloudbuild.yaml") build_script_sha = filesha1("${path.module}/scripts/build/build.sh") - dockerfile_sha = filesha1("${path.module}/scripts/build/containers/openlane-jupyterlab/Dockerfile") + workflow_sha = filesha1("${path.module}/scripts/build/images/compute_image.wf.json") + dockerfile_sha = filesha1("${path.module}/scripts/build/images/Dockerfile") + environment_sha = filesha1("${path.module}/scripts/build/images/provision/environment.yml") + env_sha = filesha1("${path.module}/scripts/build/images/provision/env.tcl") + profile_sha = filesha1("${path.module}/scripts/build/images/provision/profile.sh") notebook_sha = filesha1("${path.module}/scripts/build/notebooks/inverter.md") } provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${google_artifact_registry_repository.containers_repo.location} ${google_artifact_registry_repository.containers_repo.repository_id} ${google_storage_bucket.notebooks_bucket.name}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name}" } depends_on = [ google_artifact_registry_repository.containers_repo, google_storage_bucket.notebooks_bucket, + google_project_iam_member.sa_p_cloudbuild_permissions, ] } diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index 4f272242..b9de69f5 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -17,9 +17,10 @@ set -ex PROJECT_ID=$1 -REPOSITORY_LOCATION=$2 -REPOSITORY_ID=$3 -NOTEBOOKS_BUCKET=$4 +ZONE=$2 +COMPUTE_IMAGE=$3 +CONTAINER_IMAGE=$4 +NOTEBOOKS_BUCKET=$5 gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_REPOSITORY_LOCATION=${REPOSITORY_LOCATION},_REPOSITORY_ID=${REPOSITORY_ID},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET}" +gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET}" diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 93fe61fc..1c9918dd 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -timeout: 7200s +timeout: 3600s substitutions: - _OPENLANE_VERSION: 2022.02.01_02.19.58 - _REPOSITORY_LOCATION: $LOCATION - _REPOSITORY_ID: gcr.io - _NOTEBOOKS_BUCKET: $PROJECT_ID-silicon-design-notebooks + _ZONE: 'asia-northeast1-a' + _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' + _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' + _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' options: logging: CLOUD_LOGGING_ONLY steps: -- name: 'python' +- id: 'notebooks-build' + name: 'python' entrypoint: '/bin/bash' args: - '-c' @@ -30,29 +31,29 @@ steps: env-jupytext/bin/python -m pip install jupytext env-jupytext/bin/jupytext --to notebook scripts/build/notebooks/*.md echo 'gsutil cp gs://$_NOTEBOOKS_BUCKET/*.ipynb /home/jupyter/' > scripts/build/notebooks/copy-notebooks.sh -- name: 'gcr.io/cloud-builders/git' - args: ['clone', '-b', $_OPENLANE_VERSION, 'https://github.com/The-OpenROAD-Project/OpenLane'] -- name: 'gcr.io/cloud-builders/docker' - entrypoint: '/bin/bash' - env: - - EXTERNAL_PDK_INSTALLATION=0 - - NO_PDKS=0 + waitFor: ['-'] +- id: 'compute-image-build' + name: 'gcr.io/cloud-builders/gcloud' + entrypoint: '/bin/bash' args: - '-c' - |- - apt-get update && apt install -yq python3-venv - python3 -m venv env-openlane/ - env-openlane/bin/python -m pip install pyyaml click - source env-openlane/bin/activate - docker pull $_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-pdk:$_OPENLANE_VERSION || make -C OpenLane OPENLANE_IMAGE_NAME=$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-pdk:$_OPENLANE_VERSION openlane -- name: 'gcr.io/cloud-builders/docker' - args: ['build', '-t', '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:$BUILD_ID', '--build-arg', 'REPOSITORY_LOCATION=$_REPOSITORY_LOCATION', '--build-arg', 'PROJECT_ID=$PROJECT_ID', '--build-arg', 'REPOSITORY_ID=$_REPOSITORY_ID', '--build-arg', 'OPENLANE_VERSION=$_OPENLANE_VERSION', '-f', './scripts/build/containers/openlane-jupyterlab/Dockerfile', '.'] -- name: 'gcr.io/cloud-builders/docker' - args: ['tag', '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:$BUILD_ID', '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:latest'] + cd scripts/build/images/ + gsutil cp gs://compute-image-tools/release/linux/daisy . + chmod +x daisy + ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID compute_image.wf.json + waitFor: ['-'] +- id: 'container-image-build' + name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', '$_CONTAINER_IMAGE:$BUILD_ID', './scripts/build/images'] + waitFor: ['-'] +- id: 'container-image-tag' + name: 'gcr.io/cloud-builders/docker' + args: ['tag', '$_CONTAINER_IMAGE:$BUILD_ID', '$_CONTAINER_IMAGE:latest'] + waitFor: ['container-image-build'] images: -- '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-pdk:$_OPENLANE_VERSION' -- '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:$BUILD_ID' -- '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:latest' +- '$_CONTAINER_IMAGE:$BUILD_ID' +- '$_CONTAINER_IMAGE:latest' artifacts: objects: location: gs://$_NOTEBOOKS_BUCKET/ diff --git a/modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile b/modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile deleted file mode 100644 index b5b5e070..00000000 --- a/modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -ARG REPOSITORY_LOCATION -ARG PROJECT_ID -ARG REPOSITORY_ID -ARG OPENLANE_VERSION -FROM $REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_ID/openlane-pdk:$OPENLANE_VERSION - -RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && bash Miniconda3-latest-Linux-x86_64.sh -b -f -p /conda-env && rm Miniconda3-latest-Linux-x86_64.sh - -# install openlane dependencies in conda environment -RUN /conda-env/bin/conda install -c conda-forge -y python pip -# install openlane dependencies in conda environment -RUN /conda-env/bin/python -m pip install click pyyaml matplotlib "jinja2<3.0.0" pandas install XlsxWriter -RUN /conda-env/bin/conda install -c conda-forge -y jupyterlab gdstk iverilog - -RUN groupadd --gid 1001 jupyter -RUN useradd --uid 1000 --gid 1001 jupyter -USER jupyter -EXPOSE 8080 -ENV JUPYTER_PORT 8080 - -WORKDIR /home/jupyter -ENTRYPOINT ["/bin/bash", "-c", "source /conda-env/bin/activate && jupyter lab --ip 0.0.0.0 --allow-root --ServerApp.token='' --ServerApp.allow_origin_pat='^https?://.*\\.notebooks\\.googleusercontent\\.com' --ServerApp.allow_remote_access=True"] diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile new file mode 100644 index 00000000..c0c67e17 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -0,0 +1,7 @@ +FROM gcr.io/deeplearning-platform-release/base-cpu +RUN apt-get update && apt-get -yq install locales locales-all +COPY provision.sh /tmp/provision.sh +COPY provision/ /tmp/provision/ +RUN bash -x /tmp/provision.sh +ENV OPENLANE_ROOT=/OpenLane +ENV PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json new file mode 100644 index 00000000..a76a4b97 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -0,0 +1,89 @@ +{ + "Name": "silicon-design", + "Project": "${PROJECT}", + "Zone": "${ZONE}", + "Vars": { + "source_image": { + "Description": "source image path", + "Value": "projects/deeplearning-platform-release/global/images/family/common-cpu-ubuntu-2004" + }, + "image_name": { + "Description": "image name prefix", + "Value": "silicon-design-ubuntu-2004" + }, + "image_tag": { + "Description": "image name suffix", + "Value": "${ID}" + } + }, + "Sources": { + "provision": "./provision", + "provision.sh": "./provision.sh" + }, + "Steps": { + "create-disk": { + "CreateDisks": [ + { + "Name": "disk", + "SourceImage": "${source_image}", + "SizeGb": "100", + "Type": "pd-ssd" + } + ] + }, + "create-instance": { + "CreateInstances": [ + { + "Name": "instance", + "Disks": [ + {"Source": "disk"} + ], + "MachineType": "n1-standard-4", + "StartupScript": "provision.sh" + } + ] + }, + "wait-for-script": { + "WaitForInstancesSignal": [ + { + "Name": "instance", + "SerialOutput": { + "Port": 1, + "SuccessMatch": "DaisySuccess:", + "FailureMatch": "DaisyFailure:", + "StatusMatch": "DaisyStatus:" + } + } + ], + "Timeout": "30m" + }, + "stop-instance": { + "StopInstances": { + "Instances":["instance"] + } + }, + "create-image": { + "CreateImages": [ + { + "Name": "image", + "SourceDisk": "disk", + "NoCleanup": true, + "RealName": "${image_name}-${image_tag}" + } + ] + }, + "cleanup": { + "DeleteResources": { + "Instances": ["instance"], + "Disks": ["disk"] + } + } + }, + "Dependencies": { + "create-instance": ["create-disk"], + "wait-for-script": ["create-instance"], + "stop-instance": ["wait-for-script"], + "create-image": ["stop-instance"], + "cleanup": ["create-image"] + } +} diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh new file mode 100644 index 00000000..583ac495 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -ex + +env +OPENLANE_VERSION=master +PROVISION_DIR=/tmp/provision + +SYSTEM_NAME=$(dmidecode -s system-product-name || true) + +if [ -n "$(echo ${SYSTEM_NAME} | grep 'Google Compute Engine')" ]; then +echo "DaisyStatus: fetching provisioning script" +DAISY_SOURCES_PATH=$(curl -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/daisy-sources-path) +mkdir -p ${PROVISION_DIR} +gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true +fi + +echo "DaisyStatus: installing conda-eda environment" +/opt/conda/bin/conda install --yes --prefix /opt/conda/ mamba +/opt/conda/bin/mamba env update --prefix /opt/conda/ --file ${PROVISION_DIR}/environment.yml + +echo "DaisyStatus: installing OpenLane" +git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane + +echo "DaisyStatus: patching OpenLane" +mkdir -p /OpenLane/install/build/versions +cp ${PROVISION_DIR}/env.tcl /OpenLane/install/ +for tool in yosys netgen +do + /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} +done +# https://github.com/The-OpenROAD-Project/OpenLane/pull/978 +# https://github.com/RTimothyEdwards/open_pdks/commit/098c3b0e934e8d1b8d8b71074df8837c58c00405 +sed -i -z 's/}\n\ \ \ \ "/},\n "/' /opt/conda/share/pdk/sky130A/.config/nodeinfo.json +# https://github.com/The-OpenROAD-Project/OpenLane/pull/978 +curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project/OpenLane/pull/1027.patch | patch -d /OpenLane -p1 + +echo "DaisyStatus: adding profile hook" +cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh + +echo "DaisySuccess: done" diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/env.tcl new file mode 100644 index 00000000..803c36c4 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/env.tcl @@ -0,0 +1,9 @@ +set ::env(PDK_ROOT) "$::env(CONDA_PREFIX)/share/pdk" +set ::env(TCLLIBPATH) "$::env(CONDA_PREFIX)/opt/conda/lib/tcllib1.20" +set ::env(OL_INSTALL_DIR) "$::env(OPENLANE_ROOT)/install" +set ::env(OPENLANE_LOCAL_INSTALL) 1 +set ::env(MISMATCHES_OK) 1 +set ::env(RUN_CVC) 0 +set ::env(RUN_KLAYOUT_XOR) 0 +set ::env(RUN_KLAYOUT_DRC) 0 +set ::env(RUN_KLAYOUT) 0 diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml new file mode 100644 index 00000000..0fe930a8 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -0,0 +1,23 @@ +channels: + - litex-hub + - conda-forge +dependencies: + # https://github.com/The-OpenROAD-Project/OpenLane/pull/978 + - open_pdks.sky130a=1.0.290 + - magic + - openroad + - netgen + - yosys>=0.15 + - gdstk + - ngspice-lib + - python + - pip + - tcllib + - iverilog + - pip: + - pyyaml + - click + - pandas + - pyspice + - gdsfactory + - klayout diff --git a/modules/silicon_design/scripts/build/images/provision/profile.sh b/modules/silicon_design/scripts/build/images/provision/profile.sh new file mode 100644 index 00000000..85cd6b47 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/profile.sh @@ -0,0 +1,2 @@ +export OPENLANE_ROOT=/OpenLane +export PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/variables.tf b/modules/silicon_design/variables.tf index 26c0bed2..bd55fcbe 100644 --- a/modules/silicon_design/variables.tf +++ b/modules/silicon_design/variables.tf @@ -137,3 +137,9 @@ variable "zone" { type = string default = "us-east4-c" } + +variable "image_name" { + description = "Basename for for the compute and container image." + type = string + default = "silicon-design-ubuntu-2004" +} From 458b19d271592860f98e4dc16e04430f93862eee Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 31 Mar 2022 21:52:30 +0900 Subject: [PATCH 02/93] modules/silicon_design: add missing license headers --- modules/silicon_design/scripts/build/build.sh | 4 ++-- .../silicon_design/scripts/build/cloudbuild.yaml | 1 + .../scripts/build/images/Dockerfile | 15 +++++++++++++++ .../scripts/build/images/provision.sh | 15 +++++++++++++++ .../scripts/build/images/provision/env.tcl | 15 +++++++++++++++ .../build/images/provision/environment.yml | 14 ++++++++++++++ .../scripts/build/images/provision/profile.sh | 15 +++++++++++++++ .../scripts/usage/ai-notebook-desktop-script.sh | 2 +- 8 files changed, 78 insertions(+), 3 deletions(-) diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index b9de69f5..03eaf370 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -1,5 +1,5 @@ #!/bin/bash - +# # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,7 +12,7 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the Lpicense. +# limitations under the License. set -ex diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 1c9918dd..61f434bd 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -1,3 +1,4 @@ +# # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile index c0c67e17..2e3f4a3a 100644 --- a/modules/silicon_design/scripts/build/images/Dockerfile +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -1,3 +1,18 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + FROM gcr.io/deeplearning-platform-release/base-cpu RUN apt-get update && apt-get -yq install locales locales-all COPY provision.sh /tmp/provision.sh diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 583ac495..5d61ba5b 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -1,4 +1,19 @@ #!/bin/bash +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + set -ex env diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/env.tcl index 803c36c4..e0424a70 100644 --- a/modules/silicon_design/scripts/build/images/provision/env.tcl +++ b/modules/silicon_design/scripts/build/images/provision/env.tcl @@ -1,3 +1,18 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + set ::env(PDK_ROOT) "$::env(CONDA_PREFIX)/share/pdk" set ::env(TCLLIBPATH) "$::env(CONDA_PREFIX)/opt/conda/lib/tcllib1.20" set ::env(OL_INSTALL_DIR) "$::env(OPENLANE_ROOT)/install" diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index 0fe930a8..bac81d3b 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -1,3 +1,17 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. channels: - litex-hub - conda-forge diff --git a/modules/silicon_design/scripts/build/images/provision/profile.sh b/modules/silicon_design/scripts/build/images/provision/profile.sh index 85cd6b47..465c2079 100644 --- a/modules/silicon_design/scripts/build/images/provision/profile.sh +++ b/modules/silicon_design/scripts/build/images/provision/profile.sh @@ -1,2 +1,17 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + export OPENLANE_ROOT=/OpenLane export PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh b/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh index 2726cc6b..59299edf 100755 --- a/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh +++ b/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh @@ -1,5 +1,5 @@ #!/bin/bash - +# # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); From 62e67298eba4a2cbd613e711b7f2b39c5a21ccdf Mon Sep 17 00:00:00 2001 From: Mukul Gupta Date: Wed, 6 Apr 2022 15:49:06 -0700 Subject: [PATCH 03/93] Spinning silicon design module in new GCP project. --- modules/silicon_design/main.tf | 10 +++------- modules/silicon_design/variables.tf | 1 + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 57a9f18d..48be8b86 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -67,12 +67,8 @@ resource "random_id" "default" { ############################ data "google_project" "existing_project" { - count = var.create_project ? 0 : 1 - project_id = var.project_name -} - -data "google_project" "project" { - project_id = var.project_name + count = var.create_project ? 0 : 1 + project_id = var.project_name } module "project_radlab_silicon_design" { @@ -170,7 +166,7 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { for_each = toset(local.cloudbuild_sa_project_roles) project = local.project.project_id - member = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com" + member = var.create_project ? "serviceAccount:${local.project.project_number}@cloudbuild.gserviceaccount.com" : "serviceAccount:${local.project.number}@cloudbuild.gserviceaccount.com" role = each.value } diff --git a/modules/silicon_design/variables.tf b/modules/silicon_design/variables.tf index bd55fcbe..6c247840 100644 --- a/modules/silicon_design/variables.tf +++ b/modules/silicon_design/variables.tf @@ -96,6 +96,7 @@ variable "project_name" { type = string default = "radlab-silicon-design" } + variable "random_id" { description = "Adds a suffix of 4 random characters to the `project_id`" type = string From 51459bbf0c01b9074d20fe655492548bd64052b8 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:17:55 +0900 Subject: [PATCH 04/93] modules/silicon_design: add local.project_number --- modules/silicon_design/main.tf | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 48be8b86..21d6c3a7 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -19,7 +19,11 @@ locals { project = (var.create_project ? try(module.project_radlab_silicon_design.0, null) : try(data.google_project.existing_project.0, null) - ) + ) + project_number = (var.create_project + ? try(module.project_radlab_silicon_design.0.project_number, null) + : try(data.google_project.existing_project.0.number, null) + ) region = join("-", [split("-", var.zone)[0], split("-", var.zone)[1]]) network = ( @@ -166,7 +170,7 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { for_each = toset(local.cloudbuild_sa_project_roles) project = local.project.project_id - member = var.create_project ? "serviceAccount:${local.project.project_number}@cloudbuild.gserviceaccount.com" : "serviceAccount:${local.project.number}@cloudbuild.gserviceaccount.com" + member = "serviceAccount:${local.project_number}@cloudbuild.gserviceaccount.com" role = each.value } From bc5a8fb2cc8ab8195426a08e601dc7806be0d648 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:19:24 +0900 Subject: [PATCH 05/93] docs/images/V1_Silicon: add gcs --- docs/images/V1_Silicon.png | Bin 77067 -> 75776 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/V1_Silicon.png b/docs/images/V1_Silicon.png index ddf028e4bcf56590678c8f4d1c8c74d068c9cddd..60dca91d10dd2caae1945fd6fda3dfe2d4327290 100644 GIT binary patch literal 75776 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 Zxf4u9R(5T*-2(rqDrzd^p1pYEe*mm<((wQQ literal 77067 zcmeFZWn5HS`#-D*igbe@B?!_Yr8FuctuzcnN_WH1jUXZ&(hWmM=YW#Z-6b6Z4BZ3I z9?yNw@0|O--#qW0=l}n`fzRHv_S)CF`ueVG?T_yiq;Rn)v2NYEg)9B`wbHFy=mxiL zp*3RO1@2@|Jh%Y<-F8rtdU>m)|KaAXTXeUiUrVUC>TJ*5b5)s0yxCtMaSGjlCC$xo3mf8NpSgy~b+b{^s7Cd^AX28df3Uk|uv z6Gpvz(8ZHA)O)&S_q4Ycw(iwW>b4et+SuvPn${oC=6eeblg{U#e+V3I<4ef(hS&Y` z_U}=@&@l~M2?)e*-A4b%AK&Nq3EJB3C-waMWWZDC+`@PM<0U@g$-q=g=zT{2nCFja z{U}oZZGO}xodPhGo!{EC|D5cW1kS*Jp_YL8wqL??35N&&Bo;N94_%PSzb^u~9};~_ zLM}PI4(C7FB2LiC<0DRt>1eex)I^-YRBLa^IaORG8$&%Xb&Oca; z4y0GVeb8O+LuQFT>bi8XwtFKlJ+kWWL7FB4X15X|8im6x#7xfm7P&>LGF46#4I}p4 zXu#TqUYmn*H3 z$HF>O3uyc0CTQ2gI&yja`e0zGbo=k{u&DPWHC^nnYn`^yy+=@08dKAE9$= znhhgtdIlY!cOV7AZJUwsA;r?3!Tk|>SbdX9r-E#C@A{U$=qkC)f@|4Z6PfIBO`18L z`O^4_So8Iv!50#r=FXUz&#U(J^2gJmWdj=7X{IrEaSKnm(^w1X`FyI;#W(ma8F)0PVr75g` zi4SVA*AXh+bt`)3E2B)@g<2QYjkbmng*xu>rt*5;<^&>cBc+c|-H`DmfuU?v3GL#D zi7{#2UN2rlUeDcF9|%GO89}T(W$^#M;>7ePvWI<)VeFai|oHi0wEkRFuN?a+~SdKj#3TQ(2Y z`5B=TS<+uDe6chl2{}r|sp{QR(VmYRo<*Kk(ED*z)Ikz-Q?wwCwR9_m)Ru|zXZaR# z>UbxcBhe#;vmnvNfx zx|3Z!V>ZT&AH-2wTGDkdTy|u%EKo|bvs+iV^UY-7BL7M^r#qz-9F@B-;#m(LdKs(MgM*+!xQubBsj7&$7O;ezcOnxZ zHnQnUiKU3dYP;zXw?5lu(1DSDf@-zr;-rKVt zQ~iRY?Lws;LCVp#c~NqMD}?)FQcrHyd)#0xH*GybPs1WDLuH+Jmpv|VW$DXn@4d*; zr=oJW=RA{MjON|ARb(5ONrMf`b=c~rhSl@(pu7abrSlv^A9?I`zfcjiY1r=d7AjG* zeBpU8HV5@?cr|yoy6RMHPqyAN04nf;pYv0hL&6*zlIlG{h6^ z;+BCXWZgOPpyN~Mz(G96Klv@LisE-sp`Ny4n&`f3Mv-~@%P6tA)C?r^D^%A>X3)N zdD8VxtbzJtV$q^!i@SDSa~BgY zVBV^r8XV0&de&<7phSNUPSwAoOkCn|eo!GD2QD-Vq#oNFv0YF}Fn*JC>S3!%nQGkQ(rauVQ7VnEEKyuakEgdqF+X=$Jgp`Km3P3 zxrHW8(N_8M3Ft)MQ_%9G!r8aSeCl}nn};{-_P(E}`7pQzJ|Fd5*v+15yl;2@F-99B z*!+nsh&3+zHBnnYsrpRo*%WiwEcwAj$A`2CV*+Ap0?=i9!!TD?Bs%xv{`F}i5w)n(Zu|kbvm0WoAt?toEaV6|-{E*L z?Z&v9SaJNo+r(_=a*BWJZ7Ss1&SZWsoF->33+m*G!ZGKqPp9Z zEHnDcO*VeIaQuK-qV(=y*h$3YGd^#SIGGP$^`qR9^-gDt^3a4WRl)JANmc`PM`+EX zTqR7iWqfs2O+#IahrRz+D7;gpWARG}wtlM)my4i*T+VV91_sDJfY8r_snqwKX zAbEkTa?6<$i&l->wo5#%Xn!L>$BA9OFzXT1?9Iw$DOp2qqjSKWh|YL5Fxm&*3mznm zdMt5Qf~DI)M5(mPsZmdIORyE8)q43(Lu=<)tH;GkHt&SDF?>-kTovyG<7&Xg*t9e9 zi`NlkEVp7@tljx*=t_PuZO^A4q7x+NVme&To=)4QKT-aF_Gt?OQlw&?r0tm;c?7VJ z5zc2A`+gBCauE!>SnNG=@T0(HVW+!KTV2#!sKI&<6x%tNmohO2k60&Zc=L0yHO@~{ zEXk(P)mhfUy|3}cQ7Q=fq+Ev=7uVpCo0sJTGj^xtd9+nDwP^0q@#c@Pkq;7!OEMw` zg$rLnNh%y64~D1M+^<>15Rt+>jrabZN)fyz^);q#3uyi&TGyy@qwTgE^=0n{ZX==} zhrk=BXq)^n6Mq$UaXwofwIk)fweSP(e6M^Bc487<9I`n+y&PBFyFfYELj5DuIIlW- zxE<*US#2#Zj;t6d7Wg=z>J;!Q7BH^mYOdbuzypRbnOP+wxS0LOljfPB4pv|Ob&R=q zSGc>uq8wP_QwmpOKu42LbH$7ND7~L`pLQ3=nuS%j(XfPEtgEG(JxhajKC*Hs-al2n zfAdr`wzE~h_z6K2+;aqKrTLt26Uaa*ueo{zU09Nc0E zy(SMiW``W@;=md?f5mYH;zMK3*K#(e-5;$u7nEbMV81P)+)@WBi>5=FvYB>hhJ+mB zKqah`5@&{Ni>$+=RxE$FPDj(|3mB|o1N;FdoXR?A;<@P4SgicsAp{)a&F zRnw>LaaUIik@|Z;esw*fhGrNrwCbv@mRai#BE_!wRNE3hFR-0rIGT&4K}*W40!&J| z#;D+T$_2l4+~s!Ob!R@~GyJv?NWrflv>kSS+=?s@DyS}M*8x=v<#)!#sIlJ^*vL84 zKug?gA@$I7H%B|oEDdb!-IOo6*!_py&d4&ZG3d!%u#b{1f#LeYg&w%G^w!)sG%8(t zAf_`u)(URbIl(FIp#P&rr_c?$R$r*d{``F0XtSg{%SFh>e~t5DTR_^H-_nj_tWesHD#4s+Y%o_&rjaIa$YiKGTr< zO`Xj2IAfsl)~XYVa=6E8Chr7JZT;6B-VdrGt@RLa?T3M~@OHmSTf>D8bnZxDG5!;a zpvyVoNe9==r`h<`7}~FlurYQ*j;3l(rw`aJ-3n%LzBYURvrItoG7ANU3FarO-f42LV@#UIXU^xKdkK!Mx|?i zhaa;tFuW?(s){nXJlV$0?FGEomrKYysIlKKOI(nM!vJN zD?{@YC*k^0@Lv!m^cFPHG{w!$%>xoLvLH+8DAm??gspe{{&VKn>fc!nwrN3`EB7Ot zUaQAf(El4e23%HQ6144^{ueRY5djh==8c#5PiP6i&F=muVgI+_X8+0OHfbL^CWS%m zyZ@;SbRmlm{)_o6F^UrqGn;by1Cs?{^e4Z#bNBD*J8z`8nwlChxNf@~om;+ppq)^h zjuZpaK*?^r&g3tU$@euOU5Io{q8=)b{cjj#z61abcA^1Pz2yebLz1ty*5rSYNS6VC zmPF=)6exU_ng|6}gTH;Fr%p+ZPf8F_i+$E6=@IXec{|}Vjm5~FA2IE%$8=KPk zZv(g~E1;B{^Zrkb7ehKg1W(*Sr9^kR=4c!fNwpP!l)1{A4pU0zK*%5lz)O2UX zY)I(<@;HLI-8k^?UVs$v0xF4Szmv`<xV+sy~jSxd-0$^KQc z!wR|L!Yb0CCnb}~(3|r=JYVgyVz`N&VBIOCTIn*Aw!^{a{mXrdD*@X#jAM@WJSg{o zMBB$ScF6#EH|x%whpAdE(rY_YGvJ`9a+D*dcxr zq~!5W2%f}_t!AGiWn^We2iC(}lN$;D=G;1fpvn6td;sOX`ZVaqB|c$LXY7Dfjv9hV zCJ!P>FjdA13vOi3pFPIb$1`yrT}EC@QZ*=y9}#ZWJNZd*RAkJDbFsE& zbJ`hh$52j8G$JGWK;7G;{QiAez2ol6btB7X!_El!US!tRU`dL-14y8qpqA-RnGc!* zdh=#vUjkTsx*JxTUrEo)mWlJz%4a3A#?a(Eci`Rh*&&#M!COlc&AwjT zcL(IOgv3AE_&AibU|x=`Kbt2!ythl^dH$@x&_Z#lfYR=vsQv5G0XJo+da0_NbeUkK zO=+r#d%l%*3Dv@4t5l`Kgf+E@v$oYt_0UeuZ2mR!qT52~`Qz{U`dvv*@Z;0Y>kPGG zt?R+Sx^> zy6ryR5&Nd=GVdij+L9hL8yu*FO~yICEwHv#4>d7wIV;0Ge~$RedXlt#=yJRAd~TzZ za-THBj6m5|CQ$6W+OHw5XJS95aVK;|9{Y((68*xSMGi|^RGtxjSG10iRQ9_k`7MSR z7w}=6zPS~3>6|gS2WB)PPvw(DC$o6HTZ^~7bQ6*FWSm-s;!pGU4Ldjdx`@Zh4DaRS z<8`P&ivg@jdKwBO=f@#Ap# z^3Xcxe!gc|(2glJCVG`+yA14X7%Hgj`X-v`(?mNtIR9j4Iz@TMfL`9qtLe(C*}lAj zm?@y_9@DhMrz*Fpawn*c>$xhmkh6AlN5n|2o#7~xfV!F4b;aW7^_>!}4mr+d%X$k_ zIf9WDu@;;^bE>2|pg`kE6Ay|2QYu~*IyH$;r@eRGYCNO__dg5IPgfV5JXx{Gc(?l) z>*%Z`W=^cpxohqX9mVkMF^$N9^}D>Vfq6~mddD5p2x|)XdxfdOoRH5~%6UgCVrgTA zV-{nj3vTuBYS3uLtIIdR?AA3oQA8MbZo9!mjXq>gxN*W?86s*VRmp5M(%;YMuXD1p z+H>XQ%_7V73l_LHK5G>2Pp|MT3lK)Xnu7 z$@Y~rhP#&(y)Km1M#S||HbR7JM~uuq_JOlTX~MYX@K}~`y?M422g5dDgZqFE@_2~Z zapfl=*zP>1emg1*Os8~?iD*(*#w6S&H1zHE^}jc#L@i_@@x#o>#HhfchLcj*)~DWS zr|{^ge6O`XKFwQ()@mxV4^+K5S!P&5uF!C@!<(+FdkmlH45VEA((V@qZa6hZ28B=8 z-1y8WH8`v*Y)!!$KBqmWhw41w_Q=Ph=^e?6g|r=Q49x}7G&m#d8pLAhdACqNACQ)K>-JJ=iWeUFxZDy5tZj<1J`P=DGYzU2VZ$=HuhXUOM76f0v^g_?emvwW zL(771XkU>%pu078CQ5(acwDm!zFdTeTt|4<(>y($_iT8BN2~VO_FHITija%CN~Q`; zBgSKJgQGJk@AyyW-Nu?KCpbUGD+%@e_mr5m4#ykweHg-If*Q6CTca$n^ZrY%ddozG z6js|<9&1^|yLTM+TBgHocJ;uL&4(U!hFi>C;6e_IT`ElwUNzp)R*KYEL9-{LlI@Zu zN~>#Br7js^9N1U_@SV}RXwfTM|Fx#`>Y1*eEi@4mAhM21v#bJgh4FLZPBw-t^ZSvo zCi^i;k>lP&$`=Ot@Cqee4Ef0%2*C){1b=mvdKm7oFgoo7kByGri4)NLX1}t*J#jW! zX!Qn%YOCWWzF9Q4u{HRZEA2cR`)iAomxQku^6Nswg6LT+7b_0~_!9r&;2~F2rqzr~ zZIg)ef?A59uR=rhPNmoK8=z3TR{b&cFaJmP27r*b{Z=bcqRGU1Eflh!ti`gBd%40g zt+^Rzbzi9@hUaxgrSky^ZH$7=7v?Xe?;y+=4Od|=QxI(KidC>LMFPFc*e|upIkJ(= z!sU8Q+EoUz+~l|i8x`d4V~-dfZjW-p3|7tiAX^VkgA_fNXw)Ao)!i<|U>^{<$%IEM zo!d?vy7D433n+z6(=ALbRGF=&E64K}TgNgC?@^0D(7HM#;UDA z99oU9ZF)U>0@?lV;X5{PP3;e@aa`CU->6IT9Cr8cS@%BK5wjiSD4VKsuc>$5Em~|1 zuK)gnRZXo_Gmn^AAs^gOU4Ohr1-|wEMaccGER!_v_d+f^N&_?^yyk7T>bC(kcCI0T zIziRy_b)8?KJ0yGMcP_wACj{V_mnp6qGdmb*Cl$-_i66cF08n-*0*^SJrB1bWPDPmdg;uTp|tiTrQ zbI~Tk)9&oq@#=w9x`^zOVYvrm(uT zvE_{WYg;&kASdVXvj;h4hHvW9v%oX_)_TEsGi1|;mBu@QDHC}SCCw+S#hRBH^B$Mr znv`nfb)zV3)}9{w;k^6J9T;-{(mXQSL4m;G2IgW4ucn3=oltS+*Ig$;M=MNRs9)HR zS~Txe$4_zRVd3u@tenrX{7!bpLVGiv^xp*C6d2;!$MDGa>!%Mw10R(O$jrP33ex`C< z#ASZ>B^CCdU&fc2_RZ1=(cMp)NihcAZgvfT0Re%R@goq0qvRNCP`gC0Sy7^?*|&C! zXj+OJ3y9F`Y#`YX+!Xtj}p1d@7iJrJpdr$4VxDS6N{hC%vo3XBY-x{2X z>$N>w&G>WPhH#iqy>AeR^p$oaQc2IwJ?3oZ#zz9% zoC%h7g~0R5Y^-tknm-9PF0nC$=5>wS&EvB*@E_Pf+63ZtpNI%cX_fP75FVa+4zF|V zZ)Hj-O;5??bU366yAAIoeNcUL=i+n($jmj?OQcU8FHml4k%k{L)GgLqXxHk?>T^E# z&a|#yoMz1k;)|)uEkiVkEvVMv(C~^^=%&FhvS%G8tT=u0JuIi{`;LpsM4$1L)Q)n9 zm3Mz;4xlKWZu#cwU~=ulIw-a^;af66T~<>DI2@5stUx!}kwl^WcSS zTw;0U#_;ML$jVUXSed8$oacHmWU0%<9LJzYeSnMRT34O^`1oFPwq#iRR!+PLJW7^x zcuZaA{c0n>RJHKtzUGZ1(uyoUdB%u&8X=F}mByTNkNkm}dAjAPUW1&b zq+F?v2pIEZl2~z5d|twH2-4=bsl8~h+kAFx12+9Y%FU);GA@?p`Gb$DdJlR~z2H1Q zRoXYSEs!5#))qlR`!CGg7aiCok@k_+!$bvVVN~99CU%i69GnKN=Pf|?VI7@+j2T}{ zHg1fZhw|-!YnZbaV2^iRK%A$P6@rd_;Y4&CpNux}c9l3i6I<}K(=B(bYZNVI`b?W= zXDHQRPB6nXh4uiO%JSpzl2K2U!Z^-E3lnxqk=ve8yu!p6i92&qxwaAH(B!i8c4w#QMk6Rt zI2QG%2-aJb0v!P~+?yp*y$h?SmyV~@HFeb4+lMEbz#jEn3>lYB?&e6|=2C&OHs4@w zAIK4rQd2khbKYW^k9a0&hSh;RWU9d>?fRgmbF5xfcFuERXKOsiK{Fyj?`<$X?Kne( zXvUoT$v0k>VOpm7V8F?vjNtxbaf0Y~BR}*|fb}FcDvP2y%kr3MW(y2uyKB=xpR#*^ zL2d2+^%=5wtYi#ZG6;G?@%Z*_> z3>D>ASfH1O2hh^Gwc7E$D)3D8)=W)=pz{-Tg9G2HIaimYx&?MbP#424Pa0ApoOh(){mNe_==#l4n=C+sK?jOfzSc5^?~W{XS(vyyB(FDa8do$P z^zkaW_qkn%`E#1>?XFN&=<2@f+;A3m)*BUc?zHO_7-vEv55fv|IPY=1IX8mwl3xe9 zq*<}{3U+wyQDqeky+qW3cBZR_W)O{~>bl;o7-~Ew+Pluqf{th3W{kR9Xl7Vvo#mgE z=B0V|tKx(|d@(!cZ*}T<;h3?;rIDV~qgFrvf)^gXZQ|qdXPsz(0{q53o>HRf#5J@b zJOkmpiLT~N(aqkt(6aU5;bSd3F<88Ff&H`TF^V?VFn8AFGb5#^m&xv&oVaEK+z|%~ zLE2AYQrTm7~p(h7RLK=Y{k0yvjD!+r@*CS)DN{ zu?5@P<>%i-`2!`}w+5S1utV54$}W+e%8CtTS`id8{6bt@ilqadva}_-2vG~9-Sz_^ z$aVsDW6@2xm4sF2kH%L?#{(vn78BO$NCOGEADd`ZKl_KumYG)+58+VV>&9o#c)%4a zl{qJm(%caw*eljHqq3AoA{37PX~b*k4p$WFNFwfze`rAq?pm7&oUZInyo zApl}e)_1e>ldISBp3N?Ou3>&UFpWuBZy_G>&RET|x%ZMAi!JzFE|8FNe59BgS~08h z>+LDTm%vs;x9Hg#>>L|> z%K=D+>VLJwNozXw)Y@f%qjj*s2M2g&$J6*m;daGD?bpGczkIwXxh?atqqn9ByE@Ec zACrBtoUH8v->ejPkS9+?<r&DMowk2nRFaUAH(YZtE3R3zoN-&rEzS$lT2H2j zQ;7|CT`9}R9ya)xALU1GO;^bVhKEl$mzM+OqC6D{0puKzQOp`@7isA-s8`vlrn3%I zMXSj^)`HU#&&nQTF+0i48B->c61kd6C~~Xu{+|+hO8|hW=!uv?s06r8mTpVpQy`)&JzN)j1CIN9**JZ0`m$ipA2nIE zwXbt>9&_$VE6PGm)j+T`nKQw~0nJJM88B$m*SCak^$!b2AJM#6C&vMpO}W zuu5%g+jvePm?2w~J2| zR=?$6RB8LDX&33yrB_xuk{_suK7w_2h;n)0{mF`uYM>UVlzc&pLeYPrm0=oaBfAr= zv|z}Hi>`j8;5*idu$&TxVY&`}>aD?-^^h1Tb(~XHw!Sz7<8yoT)`~5H8SbuÐ{y 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 z z3%35dxY21A_WwH>8Tj6qGC+5@ zfNEwcBx)1xT^rMnN1P2BTthzyBsJ6iT zmWNdYN%joJBZ(XX-2m*FqzLGP62S@Admi9?QuJ<|fMj7Y7m;9x@LLvkesLM0uvj{O z_G*<>IkGy>9E4C#+{no8vw|i_wFydJimUZDv|i?v0|f%_`YxfDhDvtQ=lL+!e-8|t zV7j|(Y|yV}om~4&VAnZs`uaKLRS3xMNAd6`dsjakth+p-@^-%WS$A1N0I6yrlttze zcEK3COT~BX%?|lm$jWlD&4>^NK&Kc26jGg!VKl}}NUP}6?qHnMcopV}RVYIRNFYS9N5+$i^t}A;s_X-y+xZi?L2DtziRJamfFy+YgW*!bkLYnif34YoCvf zKrM9%QBn&mrk6gw#iP>N#rU?+vm#VwQrkdfaVP`KiobvA9|*ZMz1#49)9EuNJmU~r z>#C8QE9X=JKF(HZaT!e=6*JWj zCOSaq^~6HL^1;|h#9owNk*C-S-EX`vicbPk!)Q&uh==<;YyiV0cb~E-3TjtjVmWcT z4H=A5%t#;`(6JdlZ2f!qaJQm=>~Z#7=W0{aHPIDM**A#m=eW<_i(@DvJLyXKmomV(YOD>Lu=gdSO zZ<2A8g7(crhED~XQ_;ZO^z8sQy%&`i-Ff}s#GwHUQ)?h%=hCUdoOJ@{>Nm}>W; z5~^F=a_UD{4Y(}n!dGEJZg$m>{?1V^;on_JVsrcKSxSr@vZ<*sZkNH~XZX@b~{Z@WeZI+0H z#G$kz`=@(aXdkGtyHZ5SjI$Uj+kbd=GaGLD9@bPq@3|e=Sb7*+_19FNaMp^wIJU=$ zRnS-`{3_%hr=fm9g8)9E3V1@}wohmxK8IE28uszNvMh*%`S)nQ+NGk^eIZ{8iE%Fc zD3by~@>g4ipT25O1;REug+Z@abw9wU-a1-fL%0`Ym!kD>$Vs4Vwm!fYkS_H>uIk&5 zJ*PSHb^4>R$|Z>#_^x$wbxEx?rE18(1G357LEgpDN)UI4{U%xlyI?oe2h;;5)XEA7 zlsykSyjc+yM5e|r0J!`BU~I;{oewrUhaP;Gx3?rb*tH#}PG_9ZQi%-eH9PQidQ7>+ zx-NlX+@ib_|IKrI@&`7TYgTb5*7b&rb8lZ8#RJ+*ulX9*Y`ldhx~KXP!`FlkHko8> zv9+zmV+n|3R2tTO;T0AV5Dy-7%T8pWtyBLkt6a$T+w6rJFbowsXXCZrQSmCPCU}nE#7NFU#@=AuTEF%H=*lZ5PeLjYUN+4) zHy0i~h_7So`aW{~oSjL~vfs*^a+n<9f^qD-WpG(+*Q;tO_56YJQo_uW+ERkKAAI3T zKSy^~d#Xb`{L=Nq=6-}+RmLNzrq=_fjZPiPGh~IXYll2i!hUupb8ZYi@R_Q;>0TNV zf0Yr{ou!$-eAoh--xe8VI)~sjX9T)A-q@Oa;U^ZL5AwU+#?Lm}gSkjkKp1LKKPpJS z^T7%qD=uOJ_~`Z@2v5~?4MpQV#pASJrlse~C67^Z?sx^&4V$g8niY_mii->}A;UTY z61#{a1wW^3*8#%2e3rh4mZ8HOVaLtQozBHgsgO%jw$R43OI=Sl`*sVME1+nXn7@lk z95nt$Uwd7_G*#q}KHD6m{BTUe`pkK{zlLeRd8j3eqo_ji9UDcdg-(Muf-rsx6gF44 zls!5xcyc!a3d!8Oc z#T_cSk%XMm96Y;KtxOXC8fILtF`yu}r=3Z-VfKgxKVd3}SmlI{;pD2ZC}Hwhrp5dE z78ZrFFijS-a(T{EKn{BBYF4MV-v5vT#ZaLN&v%kXd^A(~>#h~<`TA!N^Eb~Bpyk6s zb)zKU(LRdVQl_qo+B;Iu^laW*W=S>{C!R}Tv*FQNAs0! z9R9jY!w*kM@qcPg{cN(sy^P#k;Jvi?IsEv3igd+DmaWCcC%h)~rUnLaNQ;%Wsv1(; z8C6g|`p5eORAC4(ED;pumc1e6VgA{u`RT`?YcKp|o=v#V>;#kI^>=7qLS-TcU|_P1 z4+=`b|6f1)@23O9?2#+q4w!80wWG(4QE*Z3`%@JIx#VNSD4hB<_E;$QjKz4$wDVqVKQ0{&$+$sH?PE6gno|`^ibVpr_$43OpV}m~TCX8l8HN zU^4!QI0A#H;TvNg)wH#D@Adg=^|IQhV%Zj8LpbZYi_i~UOf4{0rF@CP%^NwtSl?<7 zg&uziZF-Um3m80P?jH%tZadmQz?m?qXsKFO3YqqZcu$O!Ua3F{JvVuDMp=e z7F4^*IIOb?z?P468<6>{Jo@@=p(j!?)~lB?BQ+r2LTtTV+Og_v`QvZWxDyPr9F@hLXObnec760gPrpqZy zp5q&;`R7j|S9RI!zXt3tjIIQVhgDGjzD{FAl34Fn=;JWaKa)N8t^UNLH0uUtzbhZF z3-eUJ6H_QSIZ?1}kNs{IfnD20P8LlDy?HWQqb#Q@3N5O?Rm6%_ zoN38)N_y>QWaBN8T#SG7hLdOJWvIxPU1uhnGg5E*EDo3y+V2Kgu){?TorFBbU~1MY zpe(_JfHw6WxlPA|cT_Eh*H8e=3@N)aV8|gQ4fe!1uva-hGkn-?vIV%K{Eh9c8eH#T zUAn;yD40J_cmI9}aIzf37s!KI)w~<8Qy#0LGJ1dQHF?txiZ3UNK2oG#H2a=hyd~^V z-F}B0$NJi^`klsG-lv>2oW5#UwzV%;`;lYYnee4HiQ4rott^6nWk(Ms@e)J-$O-qo*;YT&9Z*-Y`}1^_Umj0eGj{b zF1+s^jAKM6H=Yd6zW$t6O-2!2N^!^4SRg;WB9v%a?Oi5tM5x~`nq8*AIrN@Am@(QO z0`N@(xmob7J4VU}(%Ml`USePB3pOO&ie-`9d>q@!T`_SrP+F+DGCa_$j*|}2g z4$ooRBkG5W*z-540$&>E?H`EqTCAd6lvTQH6hTaUc2m}-Ki9&f!t=oJrO5*xh9yP8 z7<;JRl!ENDVN*E!^VmUY(9;tP3l9ek597>H?NCYUI*Qe|w0_6^eAX8F=Uyn&B3`8v z4lmC6&a*|ZN$L3Lh!Vy=c0+@rHxKw~&?j`R1?fR^seZ^VB7u*x1Nzk)#|;dwA{CsP zwdr%c`68z~9T=bvy2a9G!H_rJR%dAyilezlMrIviJ9t(qK#&trWK&$<2L@-mH z^ZX0Ni83$+I5o2^w_i|SEiSoImj=RVhj@s)3YnqQ`!8$jEk@kj{<@hp`Z-=s?hC8E zlwVn?>N&Si*C<;Td{{nH=tN)Fy+Q6iie@lh$3~=;?6`Jl@zDcIDwI3i?|=r&#B|4IOxn>7YKg!vNHo|6@@bJFQ+&lwNeAsj$UJ{ z9DGNl)G+b(`q%pt-La?Ldvw*_g4&BAxf-?|RQwG}j=nqeidKG=4svY%v2W1)OIX6J zT*HNWE`PfJAcBb`K!t);=y@h}Vm*y4HKP6b=9Q0umZ%%PbQQ22di7H0h96?1u+v-S8H%iX(nq`)R`X_`oS0zRZdU;G>*$-C?UMeCQBFS31Y>*>T=THQ1#h4OJV0;Cww zRQy+r-+jz@$%ky$jz?Rd3i{bgap|pP)XSb81DhXO3$}TjEz{GI5>>Y6zGxO4K__vJ zSZU;z^<8#H?tb!ViWKds8?M1^c#>H>6mK+}LT&H1tCR$E&>Gfz!p&Ck@j>ruEDzGUO#Sx;#tYg%F^}F&+yb(n?hZth@0s> z-q50jIcy0oJ-queKIX8$M&0373k9(-+iU7^b)}(KsxVqm1WC$UU+4v~O?E`Iw7?3j zy>qMP0jAR!(=p1}huqruBzEoC2-Yk!o-3z7Sno0w*FYe{;7u9#dxs8v4dh(i;#PDt zN|8625|gFRL(UJkO#=koSRSqGR=zUpGaKv2TJt~&_PI2u=*Az9U4*)?0ILM_fT64N zNW*2?zM)8vm3Ztr1#aHY)k#>YcaMIquw&PWDJ}5dy$vg)*u8&!5-)fO6M2%JE?b(Z zcM%l&533Q*hq5GroX@pam&SfVunesZJ2srYg>X~FA%U783m0L8p7Fm z_JEFdx#x5k;7+uA?MNxH_;_%16PUyJj8VoAS<2Sb!xnZb^Y;igr(si6ns%Ln<#GiO z3cC!~u0eU271%GwJuEX?cdKDcv4Lu1?XXf40k1T2G($ZnB^b(K(?>BEGwdU5Th)D% z0a}nH#1)$2Z7yn`A*ubqC@1;qHwDc2ny}QHMj4d%qeut09&}Fr z45SS(04hZrhJbFq1_?ZgPw$~oL#s{n;d|Hs^_{JKF?`!>-!F2@DDn7=8FBKv39@2i z1GweCYVJ-zDv1JO%C?d0p`;G&qS$x@oPr45{G@%DtW>Cm5K3$R=JTor*i9VOvWi zk}$QgufIUIWCB_p@hA4l`~}L@YruMPsU<|Q?y02>`L0Ws>1x2E zDr!1*$qt3Pf5R;-mKsCAURLr=yv`S^B&s+j9c^3GLi-)|36b3L2?05f8$i$&2?i!b z4kUpX!Df$Je(M)K*kXDVNP-E`P~PN~8xycgvJ!R5s9}FVqK^0JkIiqsS@o4ZQAZ)v&zs@m4?@TTvvEr zXUO7bQjrn10B>k){1yV0>7bD*;lZdD2!O&Ji7kta=kXQ*aM-BpeZz~&VCR)Zr2#J$ z3atsFP?pw-7#k^!L&m+(j(uuQwh^{>wGu_Rb3AN^7!P~RsNy^PYFJseQscyUe9dv`hW;zJtL_6%fP%z0S#Hug0pFh=g$rfXJ?L0SB-5Ib?n*i zA+8bC*)qbbZ~toXth8U<(u8^}11TY5PuZerwQItlaH!2ujMdopE(ow~zD^D3+cmZ@ zz7Pd9IoXk{hFnoz!jN`Q1rON_qW#7s_&aH$$(?&NSR*ycpE;UO`}PqF4yo8zK1lX5 zk*3}`z^xpd$)o1Ru2EneCH;;vVK>jnjiZat!#ookh3=`EsLm@(7NWK4GFH*~MPpl6;`cztd zVm!tQW7IJ{`9{!j7f<5GVMVtH<)vdHsJet4LY$6=H|sOgKP;mhLXpO_6{*qU5G@2Z{k0=0DpZwzT@n3L%}6cPZyUO z0}o>2@~*`9W~jAyhGUU3oT`?EJu2dRRh*t~oOCvisH}fZ2I$)6!M7crDq16Cfibs7 zwCwOcm0ZEB^8`=`WT;lAxgB1w!DWmrA>seX+D6M_TMen!N;wzuhXNy0_c7a}p{8~}@o)xga z(R6GtJF=e@RewpoY`t%G=`|kWDARwEt)2%(uF!%2@{<4&EP`g9=;RR}=M#%*SCpH{_^S`_TVPakKU{lt zJL8#-Bq($^h4J0yW!}sJp~Cg|r1Zz_y@9yv7+o}Xi8oE%bIaX=Cw8ZsYO+jU;ud)) zPO&3u+n1F7zVjqZMfQu_)r0+5-n}E_Li)Wo6@|jS4=xdX*YT(`NENC>Fw4ttx&$v^zM@<{V3o?34XYyLj?s=qR(_RS=DyWFHJPEMc9?d0>6k zgz^C)94$tN8=bG0AH$B__S_XwcdZz`uQ8|kR|6NxH8agTzKpegd|e@>BmI`5a%nsT zlqL?3vCY`EU*i@QriF;49sqv9riJ!zK{lu!kQpbu#CpK(A=5c*8BorX<8d|Zj3Zrt z3C&nf_T_whna}e~dXzdoK0Y(Ly_!aT)djf4JLvheGAktI8l{uv4y$Ozkz4`Ck5uvt zUH&+y26#Y;I?)yXp*paI&8s$>KQDnll$zyYSGiN-B~g_oUOgMMpN?(Y_rTe2GCP+( z3~x<0)g9dVRnoA*dvV|t;xy4&cXEDoLa%rE&3pB)d?4~fDF7mpbo=&9 zTORaYjDv$xFl4Ww4&#*WScI^-|Hjw*F`Qx!Lz@lXn#l6*3^SLU54xi(kGs)3WyqT> zHC5tER2lk-vQEz7sQ4zc*kl0$mPgfAGYyh=Kjo2XAJ;C8eQ!x>dlezia~V8+|1FaG zB6372&gB{&XQ4X`d9bm{mFI^hg%&P$iz8;cqlk^I+}`HVX!$6p3aDwYNl8J|N-g9v zPrwhNPWl?y3I0|yl4BNiPNV?ediAQ<2H~=`rWoq$>Jn*Il~-O4Nu6{y-HxyKXUt2G zh%A2HoEVWtGy82%Xl1^9NZwgvg^Y-{3=J!H0Rj@kx70Kb5IG7&f4^%7rZoW?TNKUK*f(foYbF0j zV)bKoT(E3V?^6Zqc+en{jZa9(8kKRQRx6OC7Nik05Bk2`Nc5c(?P%nz6}73DtiiTb zu@nb+%YIdxeIbTo5!tS4Ey%K7Swx8h$Gjv!h+8u6Lh;=>P6>?UgHwIPtlKCAN_n;3 z8aPG1GEraKt=9E84wQfG((KLWIlWtqfNua9RN8da$iYcL<;9Xq3yxtd>z5I)JtpL; zMG*bVzp=J{tkk!dahN^BKHn^OtPeB(m8CHw>@Fkmhhbk%B*^woAJuCy zcC7gxHdw#kLHF|N{-*=NdMsXzVT_2W=>&mW$@WF6Vm3=2!<#j*{>X`I4=O(U^9Knn$1S6URRvuI3E{vq@pmTXkD#Kq8>8CLj-X@}O|z_H9lmIP1m{^I zNWe%CAAtqK@wHKnifE3zrnOi}KJ++Ln~a)+GIGOh7h81G5KrjW&IY==cd#8`6B2nx zIfU@!%C^KKK8L_A;^iUKJY=44pvv%2uum51orociBEXt@MfKQJ3v5-R_vSb@Vv`>V zszuozEaT29Dq^5L{FN?B_Oo;Z3yz#k(#fCQJ(ev2O#Rg1R)mpTQ9^FT{*SjZj~NH~ z8ADSZH#~pmm`OB|B`f4z&?3Nq_b(Ortauik>DQrRYIV>k0=mc1*6R-9VTf=V%>T<4 zR#jkQeg5MMMU6eJ z4VA<{UT!6kv?InwL$AnqXBQJABkUf+`$Xl(1^{9WfhRfts^DXF3s7GQ(>y31mS|2C zy^TxR+=2o2_=h!<5ff~U1zk^2u{;vvkEqs4heL1YzyDGJRqw?$39*W;h5C|$j4BdD z)c%xS`-`#tBZvT1(5;?LM#H)J-Y#37`t^)P^IV0uY@s&Qz$+STDoOlUJGg8xeC)YTzT6GhEwR}r zAlQbARJBgx6I!}`nj0N?a&)=)V`tU@utTg@C)JXHrxMlnK){PCK~&KRok zyKIbq#b$5{$`J~DsxBRooqg0Gen!dAj+cZhGAt8zJ$)DAi9x z4d!kD2@|jtO^2eVq&RM3LIeO<=7{p07&LByO^iT8jO9@0N=IJR+dn>6Q4B$-UFmlU zXr&}uQ}$CLDIzJM{W`%V`WX<6=y_DgcVM}iqgQ?9LA%6&FG8IKCAJXF+lpnt?ZCIv zU}JQV!3a3+KkON-^f-)p#}E3aF}Z5;a_MOS#X|2^4E3Z%#6gR45k2h5a0R6%VhqXZ zi&q$n*~gcmn{2rzQ((*e5SRMHa|dsPq6Tz zUueI1Wp}3Y{)WQX2dS0eg}(CscisR+(1!T{*sk4||9H*0j5s$iLH@_+<54BopcnBz z0>xtnNnxe-zMQ{!Z4GlxXzF1^tKg0_$ z1={I0%qD2x^0)rpJFpQ-HcrnhRc=e1|FASWV`W@$i2NI+`9^Zr+(PNLr2bLYaFLO#s=cN`!+k(~WM2!wRVh$Z?wg&mD({ja){&e$lm*c4DxMms1 zxX1aYg3-Uu`I6!)O9-4Ipb=g|BhzPh_^TghJOS5|)A!a6zT&trS3?h5%FpZIQaE`` zf#}Kx*KDGtKs0rFd*u4hTRsDrd&_jOa`mZaR-L(7XyZ&_s zC3t0&gDgLWIy_EMKOLDKd!)qHkk2@0<7M4_S!(dbz)T z=-EYyTWI3@_3xiQNWwJY{`rre!-V4hrqSlK{(pHI)mkOgmFjKK&{l1M|1?x|@VQDS GulzsvS>DJ1 From ebb0a23001f9b1665a09d1a3457e8cebec788446 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:20:31 +0900 Subject: [PATCH 06/93] modules/silicon_design: add roles/serviceusage.serviceUsageAdmin to cloud build sa --- modules/silicon_design/main.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 21d6c3a7..0e646efb 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -49,7 +49,8 @@ locals { cloudbuild_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", - "roles/iam.serviceAccountUser", + "roles/iam.serviceAccountUser", + "roles/serviceusage.serviceUsageAdmin", ] project_services = var.enable_services ? [ From c6cd9374addbe3c2b27a7cef5e26886321c8872e Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 2 May 2022 23:50:12 +0900 Subject: [PATCH 07/93] modules/silicon_design: use google_project_service_identity for cloud build sa --- modules/silicon_design/main.tf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 0e646efb..eb21bf88 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -168,10 +168,16 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { role = each.value } +resource "google_project_service_identity" "sa_cloudbuild_identity" { + provider = google-beta + project = local.project.project_id + service = "cloudbuild.googleapis.com" +} + resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { for_each = toset(local.cloudbuild_sa_project_roles) project = local.project.project_id - member = "serviceAccount:${local.project_number}@cloudbuild.gserviceaccount.com" + member = "serviceAccount:${google_project_service_identity.sa_cloudbuild_identity.email}" role = each.value } From dd9f9d6b1fb64ab5697ec0d0c2804df9618363c8 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 12 May 2022 14:13:02 +0900 Subject: [PATCH 08/93] modules/silicon: pass network down to daisy tooling --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/build.sh | 3 ++- modules/silicon_design/scripts/build/cloudbuild.yaml | 3 ++- .../scripts/build/images/compute_image.wf.json | 9 ++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index eb21bf88..fa7a2652 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -280,7 +280,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.self_link}" } depends_on = [ diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index 03eaf370..1e8b2a3b 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -21,6 +21,7 @@ ZONE=$2 COMPUTE_IMAGE=$3 CONTAINER_IMAGE=$4 NOTEBOOKS_BUCKET=$5 +COMPUTE_NETWORK=$6 gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET}" +gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK}" diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 61f434bd..33b5cf2b 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -19,6 +19,7 @@ substitutions: _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' + _COMPUTE_NETWORK: 'global/networks/default' options: logging: CLOUD_LOGGING_ONLY steps: @@ -42,7 +43,7 @@ steps: cd scripts/build/images/ gsutil cp gs://compute-image-tools/release/linux/daisy . chmod +x daisy - ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID compute_image.wf.json + ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK compute_image.wf.json waitFor: ['-'] - id: 'container-image-build' name: 'gcr.io/cloud-builders/docker' diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json index a76a4b97..6104a2e1 100644 --- a/modules/silicon_design/scripts/build/images/compute_image.wf.json +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -14,6 +14,10 @@ "image_tag": { "Description": "image name suffix", "Value": "${ID}" + }, + "network": { + "Description": "compute network", + "Value": "global/networks/default" } }, "Sources": { @@ -39,7 +43,10 @@ {"Source": "disk"} ], "MachineType": "n1-standard-4", - "StartupScript": "provision.sh" + "StartupScript": "provision.sh", + "NetworkInterfaces": [{ + "network": "${network}" + }] } ] }, From 52fbbbf4d3d5a0a5a28d7fb2ad921af20128ba07 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 24 May 2022 17:29:40 +0900 Subject: [PATCH 09/93] modules/silicon_design: clarify cloud build permissions --- modules/silicon_design/README.md | 1 + modules/silicon_design/main.tf | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/README.md b/modules/silicon_design/README.md index 8d642dc2..0c1c4c62 100644 --- a/modules/silicon_design/README.md +++ b/modules/silicon_design/README.md @@ -44,6 +44,7 @@ When deploying in an existing project, ensure the identity has the following per - `roles/resourcemanager.projectIamAdmin` - `roles/iam.serviceAccountAdmin` - `roles/iam.serviceAccountUser` +- `serviceusage.serviceUsageConsumer` NOTE: Additional [permissions](./radlab-launcher/README.md#iam-permissions-prerequisites) are required when deploying the RAD Lab modules via [RAD Lab Launcher](./radlab-launcher) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index fa7a2652..e8ba985a 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -49,8 +49,7 @@ locals { cloudbuild_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", - "roles/iam.serviceAccountUser", - "roles/serviceusage.serviceUsageAdmin", + "roles/iam.serviceAccountUser" ] project_services = var.enable_services ? [ From c8ba8a36e5aeced97a0a65e2cb9b85bac4030967 Mon Sep 17 00:00:00 2001 From: Mukul Gupta Date: Tue, 24 May 2022 14:08:24 -0700 Subject: [PATCH 10/93] Update radlab.py Setting the account config by running gcloud config set account ACCOUNT command and making sure that same user account is set in account config with which RAD Lab launcher has been authenticated --- radlab-launcher/radlab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radlab-launcher/radlab.py b/radlab-launcher/radlab.py index 7f6b1f3a..6adc8cef 100644 --- a/radlab-launcher/radlab.py +++ b/radlab-launcher/radlab.py @@ -116,7 +116,8 @@ def radlabauth(currentusr): r = requests.get('https://www.googleapis.com/oauth2/v3/tokeninfo?access_token='+token) currentusr = r.json()["email"] - print("\nUser to deploy RAD Lab Modules (Selected) : " + Fore.GREEN + Style.BRIGHT + currentusr + Style.RESET_ALL ) + print("\nUser to deploy RAD Lab Modules (Selected) : " + Fore.GREEN + Style.BRIGHT + currentusr + Style.RESET_ALL ) + os.system("gcloud config set account " + currentusr) return currentusr def set_proj(projid): From f38ee00548106b5f1932468137954f4d60ef37a9 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 25 May 2022 07:02:34 +0900 Subject: [PATCH 11/93] modules/silicon_design: use partial url for network --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index e8ba985a..b3b514c3 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -279,7 +279,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.self_link}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${replace(local.network.self_link, "https://www.googleapis.com/compute/v1/", "")}" } depends_on = [ From e405abf17e23fc51a806afedd863ff8bdd694983 Mon Sep 17 00:00:00 2001 From: Mukul Gupta Date: Tue, 24 May 2022 16:48:19 -0700 Subject: [PATCH 12/93] Revert "Update radlab.py" This reverts commit c8ba8a36e5aeced97a0a65e2cb9b85bac4030967. --- radlab-launcher/radlab.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/radlab-launcher/radlab.py b/radlab-launcher/radlab.py index 6adc8cef..7f6b1f3a 100644 --- a/radlab-launcher/radlab.py +++ b/radlab-launcher/radlab.py @@ -116,8 +116,7 @@ def radlabauth(currentusr): r = requests.get('https://www.googleapis.com/oauth2/v3/tokeninfo?access_token='+token) currentusr = r.json()["email"] - print("\nUser to deploy RAD Lab Modules (Selected) : " + Fore.GREEN + Style.BRIGHT + currentusr + Style.RESET_ALL ) - os.system("gcloud config set account " + currentusr) + print("\nUser to deploy RAD Lab Modules (Selected) : " + Fore.GREEN + Style.BRIGHT + currentusr + Style.RESET_ALL ) return currentusr def set_proj(projid): From 21edab42fa73fd08a32e02c8ef7895f1d11842b0 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 31 Mar 2022 09:00:59 +0900 Subject: [PATCH 13/93] modules/silicon_design: use deeplearning images - replace container base image with deeplearning image - provision eda tooling using conda - add daisy-based workflow to build compute engine image - add compute binding for cloudbuild service account - reduce image build time to ~15min --- modules/silicon_design/main.tf | 28 +++++- modules/silicon_design/scripts/build/build.sh | 9 +- .../scripts/build/cloudbuild.yaml | 51 +++++------ .../containers/openlane-jupyterlab/Dockerfile | 36 -------- .../scripts/build/images/Dockerfile | 7 ++ .../build/images/compute_image.wf.json | 89 +++++++++++++++++++ .../scripts/build/images/provision.sh | 40 +++++++++ .../scripts/build/images/provision/env.tcl | 9 ++ .../build/images/provision/environment.yml | 23 +++++ .../scripts/build/images/provision/profile.sh | 2 + modules/silicon_design/variables.tf | 6 ++ 11 files changed, 232 insertions(+), 68 deletions(-) delete mode 100644 modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile create mode 100644 modules/silicon_design/scripts/build/images/Dockerfile create mode 100644 modules/silicon_design/scripts/build/images/compute_image.wf.json create mode 100644 modules/silicon_design/scripts/build/images/provision.sh create mode 100644 modules/silicon_design/scripts/build/images/provision/env.tcl create mode 100644 modules/silicon_design/scripts/build/images/provision/environment.yml create mode 100644 modules/silicon_design/scripts/build/images/provision/profile.sh diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 4daa90d4..57a9f18d 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -42,6 +42,12 @@ locals { "roles/storage.objectViewer", ] + cloudbuild_sa_project_roles = [ + "roles/compute.instanceAdmin", + "roles/compute.storageAdmin", + "roles/iam.serviceAccountUser", + ] + project_services = var.enable_services ? [ "compute.googleapis.com", "notebooks.googleapis.com", @@ -65,6 +71,10 @@ data "google_project" "existing_project" { project_id = var.project_name } +data "google_project" "project" { + project_id = var.project_name +} + module "project_radlab_silicon_design" { count = var.create_project ? 1 : 0 source = "terraform-google-modules/project-factory/google" @@ -157,6 +167,13 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { role = each.value } +resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { + for_each = toset(local.cloudbuild_sa_project_roles) + project = local.project.project_id + member = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com" + role = each.value +} + resource "google_service_account_iam_member" "sa_ai_notebook_user_iam" { for_each = var.trusted_users member = each.value @@ -186,7 +203,7 @@ resource "google_notebooks_instance" "ai_notebook" { machine_type = var.machine_type container_image { - repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/openlane-jupyterlab" + repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}" tag = "latest" } @@ -246,17 +263,22 @@ resource "null_resource" "build_and_push_image" { triggers = { cloudbuild_yaml_sha = filesha1("${path.module}/scripts/build/cloudbuild.yaml") build_script_sha = filesha1("${path.module}/scripts/build/build.sh") - dockerfile_sha = filesha1("${path.module}/scripts/build/containers/openlane-jupyterlab/Dockerfile") + workflow_sha = filesha1("${path.module}/scripts/build/images/compute_image.wf.json") + dockerfile_sha = filesha1("${path.module}/scripts/build/images/Dockerfile") + environment_sha = filesha1("${path.module}/scripts/build/images/provision/environment.yml") + env_sha = filesha1("${path.module}/scripts/build/images/provision/env.tcl") + profile_sha = filesha1("${path.module}/scripts/build/images/provision/profile.sh") notebook_sha = filesha1("${path.module}/scripts/build/notebooks/inverter.md") } provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${google_artifact_registry_repository.containers_repo.location} ${google_artifact_registry_repository.containers_repo.repository_id} ${google_storage_bucket.notebooks_bucket.name}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name}" } depends_on = [ google_artifact_registry_repository.containers_repo, google_storage_bucket.notebooks_bucket, + google_project_iam_member.sa_p_cloudbuild_permissions, ] } diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index 4f272242..b9de69f5 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -17,9 +17,10 @@ set -ex PROJECT_ID=$1 -REPOSITORY_LOCATION=$2 -REPOSITORY_ID=$3 -NOTEBOOKS_BUCKET=$4 +ZONE=$2 +COMPUTE_IMAGE=$3 +CONTAINER_IMAGE=$4 +NOTEBOOKS_BUCKET=$5 gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_REPOSITORY_LOCATION=${REPOSITORY_LOCATION},_REPOSITORY_ID=${REPOSITORY_ID},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET}" +gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET}" diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 93fe61fc..1c9918dd 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -timeout: 7200s +timeout: 3600s substitutions: - _OPENLANE_VERSION: 2022.02.01_02.19.58 - _REPOSITORY_LOCATION: $LOCATION - _REPOSITORY_ID: gcr.io - _NOTEBOOKS_BUCKET: $PROJECT_ID-silicon-design-notebooks + _ZONE: 'asia-northeast1-a' + _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' + _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' + _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' options: logging: CLOUD_LOGGING_ONLY steps: -- name: 'python' +- id: 'notebooks-build' + name: 'python' entrypoint: '/bin/bash' args: - '-c' @@ -30,29 +31,29 @@ steps: env-jupytext/bin/python -m pip install jupytext env-jupytext/bin/jupytext --to notebook scripts/build/notebooks/*.md echo 'gsutil cp gs://$_NOTEBOOKS_BUCKET/*.ipynb /home/jupyter/' > scripts/build/notebooks/copy-notebooks.sh -- name: 'gcr.io/cloud-builders/git' - args: ['clone', '-b', $_OPENLANE_VERSION, 'https://github.com/The-OpenROAD-Project/OpenLane'] -- name: 'gcr.io/cloud-builders/docker' - entrypoint: '/bin/bash' - env: - - EXTERNAL_PDK_INSTALLATION=0 - - NO_PDKS=0 + waitFor: ['-'] +- id: 'compute-image-build' + name: 'gcr.io/cloud-builders/gcloud' + entrypoint: '/bin/bash' args: - '-c' - |- - apt-get update && apt install -yq python3-venv - python3 -m venv env-openlane/ - env-openlane/bin/python -m pip install pyyaml click - source env-openlane/bin/activate - docker pull $_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-pdk:$_OPENLANE_VERSION || make -C OpenLane OPENLANE_IMAGE_NAME=$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-pdk:$_OPENLANE_VERSION openlane -- name: 'gcr.io/cloud-builders/docker' - args: ['build', '-t', '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:$BUILD_ID', '--build-arg', 'REPOSITORY_LOCATION=$_REPOSITORY_LOCATION', '--build-arg', 'PROJECT_ID=$PROJECT_ID', '--build-arg', 'REPOSITORY_ID=$_REPOSITORY_ID', '--build-arg', 'OPENLANE_VERSION=$_OPENLANE_VERSION', '-f', './scripts/build/containers/openlane-jupyterlab/Dockerfile', '.'] -- name: 'gcr.io/cloud-builders/docker' - args: ['tag', '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:$BUILD_ID', '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:latest'] + cd scripts/build/images/ + gsutil cp gs://compute-image-tools/release/linux/daisy . + chmod +x daisy + ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID compute_image.wf.json + waitFor: ['-'] +- id: 'container-image-build' + name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', '$_CONTAINER_IMAGE:$BUILD_ID', './scripts/build/images'] + waitFor: ['-'] +- id: 'container-image-tag' + name: 'gcr.io/cloud-builders/docker' + args: ['tag', '$_CONTAINER_IMAGE:$BUILD_ID', '$_CONTAINER_IMAGE:latest'] + waitFor: ['container-image-build'] images: -- '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-pdk:$_OPENLANE_VERSION' -- '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:$BUILD_ID' -- '$_REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_ID/openlane-jupyterlab:latest' +- '$_CONTAINER_IMAGE:$BUILD_ID' +- '$_CONTAINER_IMAGE:latest' artifacts: objects: location: gs://$_NOTEBOOKS_BUCKET/ diff --git a/modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile b/modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile deleted file mode 100644 index b5b5e070..00000000 --- a/modules/silicon_design/scripts/build/containers/openlane-jupyterlab/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -ARG REPOSITORY_LOCATION -ARG PROJECT_ID -ARG REPOSITORY_ID -ARG OPENLANE_VERSION -FROM $REPOSITORY_LOCATION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY_ID/openlane-pdk:$OPENLANE_VERSION - -RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && bash Miniconda3-latest-Linux-x86_64.sh -b -f -p /conda-env && rm Miniconda3-latest-Linux-x86_64.sh - -# install openlane dependencies in conda environment -RUN /conda-env/bin/conda install -c conda-forge -y python pip -# install openlane dependencies in conda environment -RUN /conda-env/bin/python -m pip install click pyyaml matplotlib "jinja2<3.0.0" pandas install XlsxWriter -RUN /conda-env/bin/conda install -c conda-forge -y jupyterlab gdstk iverilog - -RUN groupadd --gid 1001 jupyter -RUN useradd --uid 1000 --gid 1001 jupyter -USER jupyter -EXPOSE 8080 -ENV JUPYTER_PORT 8080 - -WORKDIR /home/jupyter -ENTRYPOINT ["/bin/bash", "-c", "source /conda-env/bin/activate && jupyter lab --ip 0.0.0.0 --allow-root --ServerApp.token='' --ServerApp.allow_origin_pat='^https?://.*\\.notebooks\\.googleusercontent\\.com' --ServerApp.allow_remote_access=True"] diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile new file mode 100644 index 00000000..c0c67e17 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -0,0 +1,7 @@ +FROM gcr.io/deeplearning-platform-release/base-cpu +RUN apt-get update && apt-get -yq install locales locales-all +COPY provision.sh /tmp/provision.sh +COPY provision/ /tmp/provision/ +RUN bash -x /tmp/provision.sh +ENV OPENLANE_ROOT=/OpenLane +ENV PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json new file mode 100644 index 00000000..a76a4b97 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -0,0 +1,89 @@ +{ + "Name": "silicon-design", + "Project": "${PROJECT}", + "Zone": "${ZONE}", + "Vars": { + "source_image": { + "Description": "source image path", + "Value": "projects/deeplearning-platform-release/global/images/family/common-cpu-ubuntu-2004" + }, + "image_name": { + "Description": "image name prefix", + "Value": "silicon-design-ubuntu-2004" + }, + "image_tag": { + "Description": "image name suffix", + "Value": "${ID}" + } + }, + "Sources": { + "provision": "./provision", + "provision.sh": "./provision.sh" + }, + "Steps": { + "create-disk": { + "CreateDisks": [ + { + "Name": "disk", + "SourceImage": "${source_image}", + "SizeGb": "100", + "Type": "pd-ssd" + } + ] + }, + "create-instance": { + "CreateInstances": [ + { + "Name": "instance", + "Disks": [ + {"Source": "disk"} + ], + "MachineType": "n1-standard-4", + "StartupScript": "provision.sh" + } + ] + }, + "wait-for-script": { + "WaitForInstancesSignal": [ + { + "Name": "instance", + "SerialOutput": { + "Port": 1, + "SuccessMatch": "DaisySuccess:", + "FailureMatch": "DaisyFailure:", + "StatusMatch": "DaisyStatus:" + } + } + ], + "Timeout": "30m" + }, + "stop-instance": { + "StopInstances": { + "Instances":["instance"] + } + }, + "create-image": { + "CreateImages": [ + { + "Name": "image", + "SourceDisk": "disk", + "NoCleanup": true, + "RealName": "${image_name}-${image_tag}" + } + ] + }, + "cleanup": { + "DeleteResources": { + "Instances": ["instance"], + "Disks": ["disk"] + } + } + }, + "Dependencies": { + "create-instance": ["create-disk"], + "wait-for-script": ["create-instance"], + "stop-instance": ["wait-for-script"], + "create-image": ["stop-instance"], + "cleanup": ["create-image"] + } +} diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh new file mode 100644 index 00000000..583ac495 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -ex + +env +OPENLANE_VERSION=master +PROVISION_DIR=/tmp/provision + +SYSTEM_NAME=$(dmidecode -s system-product-name || true) + +if [ -n "$(echo ${SYSTEM_NAME} | grep 'Google Compute Engine')" ]; then +echo "DaisyStatus: fetching provisioning script" +DAISY_SOURCES_PATH=$(curl -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/daisy-sources-path) +mkdir -p ${PROVISION_DIR} +gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true +fi + +echo "DaisyStatus: installing conda-eda environment" +/opt/conda/bin/conda install --yes --prefix /opt/conda/ mamba +/opt/conda/bin/mamba env update --prefix /opt/conda/ --file ${PROVISION_DIR}/environment.yml + +echo "DaisyStatus: installing OpenLane" +git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane + +echo "DaisyStatus: patching OpenLane" +mkdir -p /OpenLane/install/build/versions +cp ${PROVISION_DIR}/env.tcl /OpenLane/install/ +for tool in yosys netgen +do + /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} +done +# https://github.com/The-OpenROAD-Project/OpenLane/pull/978 +# https://github.com/RTimothyEdwards/open_pdks/commit/098c3b0e934e8d1b8d8b71074df8837c58c00405 +sed -i -z 's/}\n\ \ \ \ "/},\n "/' /opt/conda/share/pdk/sky130A/.config/nodeinfo.json +# https://github.com/The-OpenROAD-Project/OpenLane/pull/978 +curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project/OpenLane/pull/1027.patch | patch -d /OpenLane -p1 + +echo "DaisyStatus: adding profile hook" +cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh + +echo "DaisySuccess: done" diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/env.tcl new file mode 100644 index 00000000..803c36c4 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/env.tcl @@ -0,0 +1,9 @@ +set ::env(PDK_ROOT) "$::env(CONDA_PREFIX)/share/pdk" +set ::env(TCLLIBPATH) "$::env(CONDA_PREFIX)/opt/conda/lib/tcllib1.20" +set ::env(OL_INSTALL_DIR) "$::env(OPENLANE_ROOT)/install" +set ::env(OPENLANE_LOCAL_INSTALL) 1 +set ::env(MISMATCHES_OK) 1 +set ::env(RUN_CVC) 0 +set ::env(RUN_KLAYOUT_XOR) 0 +set ::env(RUN_KLAYOUT_DRC) 0 +set ::env(RUN_KLAYOUT) 0 diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml new file mode 100644 index 00000000..0fe930a8 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -0,0 +1,23 @@ +channels: + - litex-hub + - conda-forge +dependencies: + # https://github.com/The-OpenROAD-Project/OpenLane/pull/978 + - open_pdks.sky130a=1.0.290 + - magic + - openroad + - netgen + - yosys>=0.15 + - gdstk + - ngspice-lib + - python + - pip + - tcllib + - iverilog + - pip: + - pyyaml + - click + - pandas + - pyspice + - gdsfactory + - klayout diff --git a/modules/silicon_design/scripts/build/images/provision/profile.sh b/modules/silicon_design/scripts/build/images/provision/profile.sh new file mode 100644 index 00000000..85cd6b47 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/profile.sh @@ -0,0 +1,2 @@ +export OPENLANE_ROOT=/OpenLane +export PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/variables.tf b/modules/silicon_design/variables.tf index 26c0bed2..bd55fcbe 100644 --- a/modules/silicon_design/variables.tf +++ b/modules/silicon_design/variables.tf @@ -137,3 +137,9 @@ variable "zone" { type = string default = "us-east4-c" } + +variable "image_name" { + description = "Basename for for the compute and container image." + type = string + default = "silicon-design-ubuntu-2004" +} From 17bb142b99013dfbc693c55fbbb6614d37679848 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 31 Mar 2022 21:52:30 +0900 Subject: [PATCH 14/93] modules/silicon_design: add missing license headers --- modules/silicon_design/scripts/build/build.sh | 4 ++-- .../silicon_design/scripts/build/cloudbuild.yaml | 1 + .../scripts/build/images/Dockerfile | 15 +++++++++++++++ .../scripts/build/images/provision.sh | 15 +++++++++++++++ .../scripts/build/images/provision/env.tcl | 15 +++++++++++++++ .../build/images/provision/environment.yml | 14 ++++++++++++++ .../scripts/build/images/provision/profile.sh | 15 +++++++++++++++ .../scripts/usage/ai-notebook-desktop-script.sh | 2 +- 8 files changed, 78 insertions(+), 3 deletions(-) diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index b9de69f5..03eaf370 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -1,5 +1,5 @@ #!/bin/bash - +# # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,7 +12,7 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the Lpicense. +# limitations under the License. set -ex diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 1c9918dd..61f434bd 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -1,3 +1,4 @@ +# # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile index c0c67e17..2e3f4a3a 100644 --- a/modules/silicon_design/scripts/build/images/Dockerfile +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -1,3 +1,18 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + FROM gcr.io/deeplearning-platform-release/base-cpu RUN apt-get update && apt-get -yq install locales locales-all COPY provision.sh /tmp/provision.sh diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 583ac495..5d61ba5b 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -1,4 +1,19 @@ #!/bin/bash +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + set -ex env diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/env.tcl index 803c36c4..e0424a70 100644 --- a/modules/silicon_design/scripts/build/images/provision/env.tcl +++ b/modules/silicon_design/scripts/build/images/provision/env.tcl @@ -1,3 +1,18 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + set ::env(PDK_ROOT) "$::env(CONDA_PREFIX)/share/pdk" set ::env(TCLLIBPATH) "$::env(CONDA_PREFIX)/opt/conda/lib/tcllib1.20" set ::env(OL_INSTALL_DIR) "$::env(OPENLANE_ROOT)/install" diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index 0fe930a8..bac81d3b 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -1,3 +1,17 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. channels: - litex-hub - conda-forge diff --git a/modules/silicon_design/scripts/build/images/provision/profile.sh b/modules/silicon_design/scripts/build/images/provision/profile.sh index 85cd6b47..465c2079 100644 --- a/modules/silicon_design/scripts/build/images/provision/profile.sh +++ b/modules/silicon_design/scripts/build/images/provision/profile.sh @@ -1,2 +1,17 @@ +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + export OPENLANE_ROOT=/OpenLane export PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh b/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh index 2726cc6b..59299edf 100755 --- a/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh +++ b/modules/silicon_design/scripts/usage/ai-notebook-desktop-script.sh @@ -1,5 +1,5 @@ #!/bin/bash - +# # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); From 8aabdf40e2e3b5e19c83fbdd0bc2b9d995fba01e Mon Sep 17 00:00:00 2001 From: Mukul Gupta Date: Wed, 6 Apr 2022 15:49:06 -0700 Subject: [PATCH 15/93] Spinning silicon design module in new GCP project. --- modules/silicon_design/main.tf | 10 +++------- modules/silicon_design/variables.tf | 1 + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 57a9f18d..48be8b86 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -67,12 +67,8 @@ resource "random_id" "default" { ############################ data "google_project" "existing_project" { - count = var.create_project ? 0 : 1 - project_id = var.project_name -} - -data "google_project" "project" { - project_id = var.project_name + count = var.create_project ? 0 : 1 + project_id = var.project_name } module "project_radlab_silicon_design" { @@ -170,7 +166,7 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { for_each = toset(local.cloudbuild_sa_project_roles) project = local.project.project_id - member = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com" + member = var.create_project ? "serviceAccount:${local.project.project_number}@cloudbuild.gserviceaccount.com" : "serviceAccount:${local.project.number}@cloudbuild.gserviceaccount.com" role = each.value } diff --git a/modules/silicon_design/variables.tf b/modules/silicon_design/variables.tf index bd55fcbe..6c247840 100644 --- a/modules/silicon_design/variables.tf +++ b/modules/silicon_design/variables.tf @@ -96,6 +96,7 @@ variable "project_name" { type = string default = "radlab-silicon-design" } + variable "random_id" { description = "Adds a suffix of 4 random characters to the `project_id`" type = string From dfb28d33b07087b958331d1cbb51cf78181b68ba Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:17:55 +0900 Subject: [PATCH 16/93] modules/silicon_design: add local.project_number --- modules/silicon_design/main.tf | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 48be8b86..21d6c3a7 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -19,7 +19,11 @@ locals { project = (var.create_project ? try(module.project_radlab_silicon_design.0, null) : try(data.google_project.existing_project.0, null) - ) + ) + project_number = (var.create_project + ? try(module.project_radlab_silicon_design.0.project_number, null) + : try(data.google_project.existing_project.0.number, null) + ) region = join("-", [split("-", var.zone)[0], split("-", var.zone)[1]]) network = ( @@ -166,7 +170,7 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { for_each = toset(local.cloudbuild_sa_project_roles) project = local.project.project_id - member = var.create_project ? "serviceAccount:${local.project.project_number}@cloudbuild.gserviceaccount.com" : "serviceAccount:${local.project.number}@cloudbuild.gserviceaccount.com" + member = "serviceAccount:${local.project_number}@cloudbuild.gserviceaccount.com" role = each.value } From eca8f44a308fe56cac0547ce114c727a3f5c943a Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:19:24 +0900 Subject: [PATCH 17/93] docs/images/V1_Silicon: add gcs --- docs/images/V1_Silicon.png | Bin 77067 -> 75776 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/V1_Silicon.png b/docs/images/V1_Silicon.png index ddf028e4bcf56590678c8f4d1c8c74d068c9cddd..60dca91d10dd2caae1945fd6fda3dfe2d4327290 100644 GIT binary patch literal 75776 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 Zxf4u9R(5T*-2(rqDrzd^p1pYEe*mm<((wQQ literal 77067 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Ð{y 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 z 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 GulzsvS>DJ1 From c42b55a2a1865346102ebb7e428d0e027e9de101 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:20:31 +0900 Subject: [PATCH 18/93] modules/silicon_design: add roles/serviceusage.serviceUsageAdmin to cloud build sa --- modules/silicon_design/main.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 21d6c3a7..0e646efb 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -49,7 +49,8 @@ locals { cloudbuild_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", - "roles/iam.serviceAccountUser", + "roles/iam.serviceAccountUser", + "roles/serviceusage.serviceUsageAdmin", ] project_services = var.enable_services ? [ From 237d1c52ba76a34f3c4015a236166ce7862cffbb Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 2 May 2022 23:50:12 +0900 Subject: [PATCH 19/93] modules/silicon_design: use google_project_service_identity for cloud build sa --- modules/silicon_design/main.tf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 0e646efb..eb21bf88 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -168,10 +168,16 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { role = each.value } +resource "google_project_service_identity" "sa_cloudbuild_identity" { + provider = google-beta + project = local.project.project_id + service = "cloudbuild.googleapis.com" +} + resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { for_each = toset(local.cloudbuild_sa_project_roles) project = local.project.project_id - member = "serviceAccount:${local.project_number}@cloudbuild.gserviceaccount.com" + member = "serviceAccount:${google_project_service_identity.sa_cloudbuild_identity.email}" role = each.value } From 1a28d0b57754d6c0dd0f5b687bea984856332441 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 12 May 2022 14:13:02 +0900 Subject: [PATCH 20/93] modules/silicon: pass network down to daisy tooling --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/build.sh | 3 ++- modules/silicon_design/scripts/build/cloudbuild.yaml | 3 ++- .../scripts/build/images/compute_image.wf.json | 9 ++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index eb21bf88..fa7a2652 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -280,7 +280,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.self_link}" } depends_on = [ diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index 03eaf370..1e8b2a3b 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -21,6 +21,7 @@ ZONE=$2 COMPUTE_IMAGE=$3 CONTAINER_IMAGE=$4 NOTEBOOKS_BUCKET=$5 +COMPUTE_NETWORK=$6 gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET}" +gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK}" diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 61f434bd..33b5cf2b 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -19,6 +19,7 @@ substitutions: _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' + _COMPUTE_NETWORK: 'global/networks/default' options: logging: CLOUD_LOGGING_ONLY steps: @@ -42,7 +43,7 @@ steps: cd scripts/build/images/ gsutil cp gs://compute-image-tools/release/linux/daisy . chmod +x daisy - ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID compute_image.wf.json + ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK compute_image.wf.json waitFor: ['-'] - id: 'container-image-build' name: 'gcr.io/cloud-builders/docker' diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json index a76a4b97..6104a2e1 100644 --- a/modules/silicon_design/scripts/build/images/compute_image.wf.json +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -14,6 +14,10 @@ "image_tag": { "Description": "image name suffix", "Value": "${ID}" + }, + "network": { + "Description": "compute network", + "Value": "global/networks/default" } }, "Sources": { @@ -39,7 +43,10 @@ {"Source": "disk"} ], "MachineType": "n1-standard-4", - "StartupScript": "provision.sh" + "StartupScript": "provision.sh", + "NetworkInterfaces": [{ + "network": "${network}" + }] } ] }, From 24aaa60fa0bb82d4e452a3c0a090df2a62fa9a5a Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 24 May 2022 17:29:40 +0900 Subject: [PATCH 21/93] modules/silicon_design: clarify cloud build permissions --- modules/silicon_design/README.md | 1 + modules/silicon_design/main.tf | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/README.md b/modules/silicon_design/README.md index 8d642dc2..0c1c4c62 100644 --- a/modules/silicon_design/README.md +++ b/modules/silicon_design/README.md @@ -44,6 +44,7 @@ When deploying in an existing project, ensure the identity has the following per - `roles/resourcemanager.projectIamAdmin` - `roles/iam.serviceAccountAdmin` - `roles/iam.serviceAccountUser` +- `serviceusage.serviceUsageConsumer` NOTE: Additional [permissions](./radlab-launcher/README.md#iam-permissions-prerequisites) are required when deploying the RAD Lab modules via [RAD Lab Launcher](./radlab-launcher) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index fa7a2652..e8ba985a 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -49,8 +49,7 @@ locals { cloudbuild_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", - "roles/iam.serviceAccountUser", - "roles/serviceusage.serviceUsageAdmin", + "roles/iam.serviceAccountUser" ] project_services = var.enable_services ? [ From fa9578713985e47a733589c6e6e0ff79dd940d83 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 25 May 2022 07:02:34 +0900 Subject: [PATCH 22/93] modules/silicon_design: use partial url for network --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index e8ba985a..b3b514c3 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -279,7 +279,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.self_link}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${replace(local.network.self_link, "https://www.googleapis.com/compute/v1/", "")}" } depends_on = [ From 0bbbec483e407f516aa98fca4ef39c8b11fe2f03 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 25 May 2022 09:34:49 +0900 Subject: [PATCH 23/93] modules/silicon_design: pass subnetwork down to daisy --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/build.sh | 3 ++- modules/silicon_design/scripts/build/cloudbuild.yaml | 5 +++-- .../scripts/build/images/compute_image.wf.json | 7 ++++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index b3b514c3..77e0bd73 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -279,7 +279,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${replace(local.network.self_link, "https://www.googleapis.com/compute/v1/", "")}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id}" } depends_on = [ diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index 1e8b2a3b..721c0132 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -22,6 +22,7 @@ COMPUTE_IMAGE=$3 CONTAINER_IMAGE=$4 NOTEBOOKS_BUCKET=$5 COMPUTE_NETWORK=$6 +COMPUTE_SUBNET=$7 gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK}" +gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK},_COMPUTE_SUBNET=${COMPUTE_SUBNET}" diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 33b5cf2b..deadd1e9 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -19,7 +19,8 @@ substitutions: _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' - _COMPUTE_NETWORK: 'global/networks/default' + _COMPUTE_NETWORK: 'global/networks/default' + _COMPUTE_SUBNET: '' options: logging: CLOUD_LOGGING_ONLY steps: @@ -43,7 +44,7 @@ steps: cd scripts/build/images/ gsutil cp gs://compute-image-tools/release/linux/daisy . chmod +x daisy - ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK compute_image.wf.json + ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET compute_image.wf.json waitFor: ['-'] - id: 'container-image-build' name: 'gcr.io/cloud-builders/docker' diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json index 6104a2e1..3c44f396 100644 --- a/modules/silicon_design/scripts/build/images/compute_image.wf.json +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -18,6 +18,10 @@ "network": { "Description": "compute network", "Value": "global/networks/default" + }, + "subnet": { + "Description": "compute subnet", + "Value": "" } }, "Sources": { @@ -45,7 +49,8 @@ "MachineType": "n1-standard-4", "StartupScript": "provision.sh", "NetworkInterfaces": [{ - "network": "${network}" + "Network": "${network}", + "Subnetwork": "${subnet}" }] } ] From 3fe6b3b9319e86d3d4453e8918aed3ddfbdfc5cb Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 26 May 2022 01:11:22 +0900 Subject: [PATCH 24/93] modules/silicon_design: drop obsolete patches --- modules/silicon_design/scripts/build/images/provision.sh | 5 +---- .../scripts/build/images/provision/environment.yml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 5d61ba5b..10a95eb9 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -43,10 +43,7 @@ for tool in yosys netgen do /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} done -# https://github.com/The-OpenROAD-Project/OpenLane/pull/978 -# https://github.com/RTimothyEdwards/open_pdks/commit/098c3b0e934e8d1b8d8b71074df8837c58c00405 -sed -i -z 's/}\n\ \ \ \ "/},\n "/' /opt/conda/share/pdk/sky130A/.config/nodeinfo.json -# https://github.com/The-OpenROAD-Project/OpenLane/pull/978 +# https://github.com/The-OpenROAD-Project/OpenLane/pull/1027 curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project/OpenLane/pull/1027.patch | patch -d /OpenLane -p1 echo "DaisyStatus: adding profile hook" diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index bac81d3b..8d4b66be 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -17,7 +17,7 @@ channels: - conda-forge dependencies: # https://github.com/The-OpenROAD-Project/OpenLane/pull/978 - - open_pdks.sky130a=1.0.290 + - open_pdks.sky130a - magic - openroad - netgen From f51211f77250520d7e06ca902ac3b4346f1a57ce Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 26 May 2022 01:12:26 +0900 Subject: [PATCH 25/93] modules/silicon_design: bump compute image timeout --- .../silicon_design/scripts/build/images/compute_image.wf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json index 3c44f396..45771cab 100644 --- a/modules/silicon_design/scripts/build/images/compute_image.wf.json +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -67,7 +67,7 @@ } } ], - "Timeout": "30m" + "Timeout": "45m" }, "stop-instance": { "StopInstances": { From 0860142747ccbfd411e213e987fd2c5b390569df Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 02:10:23 +0900 Subject: [PATCH 26/93] modules/silicon_design/build/provision: notify daisy on errors --- modules/silicon_design/scripts/build/images/provision.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 10a95eb9..b7a05e7b 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -15,6 +15,7 @@ # limitations under the License. set -ex +trap "echo DaisyFailure: trapped error" ERR env OPENLANE_VERSION=master From 47dad1b8fa64b055360b069e0c073740118598be Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 03:36:23 +0900 Subject: [PATCH 27/93] modules/silicon_design/scripts: remove -x --- modules/silicon_design/scripts/build/build.sh | 2 +- modules/silicon_design/scripts/build/images/provision.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index 721c0132..a6d409c3 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -ex +set -e PROJECT_ID=$1 ZONE=$2 diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index b7a05e7b..d21800ed 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -ex +set -e trap "echo DaisyFailure: trapped error" ERR env From 6e3778d4a65303296326d30ccd2474d16b6a2f0b Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 03:47:54 +0900 Subject: [PATCH 28/93] modules/silicon_design: add storage scope to cloud build sa --- modules/silicon_design/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 77e0bd73..924b79c3 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -49,6 +49,7 @@ locals { cloudbuild_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", + "roles/storage.admin", "roles/iam.serviceAccountUser" ] From d7df945d010b0f43178611f68f6bdb34c063c4b6 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 14 Jun 2022 19:40:35 +0900 Subject: [PATCH 29/93] modules/silicon_design: use cloud build sa with daisy --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/build.sh | 3 ++- modules/silicon_design/scripts/build/cloudbuild.yaml | 5 +++-- .../scripts/build/images/compute_image.wf.json | 10 +++++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 924b79c3..268103d7 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -280,7 +280,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_project_service_identity.sa_cloudbuild_identity.email}" } depends_on = [ diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh index a6d409c3..042866e3 100755 --- a/modules/silicon_design/scripts/build/build.sh +++ b/modules/silicon_design/scripts/build/build.sh @@ -23,6 +23,7 @@ CONTAINER_IMAGE=$4 NOTEBOOKS_BUCKET=$5 COMPUTE_NETWORK=$6 COMPUTE_SUBNET=$7 +CLOUD_BUILD_SA=$8 gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK},_COMPUTE_SUBNET=${COMPUTE_SUBNET}" +gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK},_COMPUTE_SUBNET=${COMPUTE_SUBNET},_CLOUD_BUILD_SA=${CLOUD_BUILD_SA}" diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index deadd1e9..026063d3 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -20,7 +20,8 @@ substitutions: _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' _COMPUTE_NETWORK: 'global/networks/default' - _COMPUTE_SUBNET: '' + _COMPUTE_SUBNET: '' + _CLOUD_BUILD_SA: '' options: logging: CLOUD_LOGGING_ONLY steps: @@ -44,7 +45,7 @@ steps: cd scripts/build/images/ gsutil cp gs://compute-image-tools/release/linux/daisy . chmod +x daisy - ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET compute_image.wf.json + ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET,service_account=$_CLOUD_BUILD_SA compute_image.wf.json waitFor: ['-'] - id: 'container-image-build' name: 'gcr.io/cloud-builders/docker' diff --git a/modules/silicon_design/scripts/build/images/compute_image.wf.json b/modules/silicon_design/scripts/build/images/compute_image.wf.json index 45771cab..2a84f14a 100644 --- a/modules/silicon_design/scripts/build/images/compute_image.wf.json +++ b/modules/silicon_design/scripts/build/images/compute_image.wf.json @@ -22,6 +22,10 @@ "subnet": { "Description": "compute subnet", "Value": "" + }, + "service_account": { + "Description": "compute sevice account", + "Value": "" } }, "Sources": { @@ -51,7 +55,11 @@ "NetworkInterfaces": [{ "Network": "${network}", "Subnetwork": "${subnet}" - }] + }], + "ServiceAccounts": [{ + "email": "${service_account}", + "scopes": ["https://www.googleapis.com/auth/devstorage.read_only"] + }] } ] }, From 8ac47227e35b38a208c02432acf53c9c2db73598 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 15 Jun 2022 21:32:01 +0900 Subject: [PATCH 30/93] modules/silicon: add image builder service account --- modules/silicon_design/main.tf | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 268103d7..d70b5de1 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -46,11 +46,10 @@ locals { "roles/storage.objectViewer", ] - cloudbuild_sa_project_roles = [ + image_builder_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", "roles/storage.admin", - "roles/iam.serviceAccountUser" ] project_services = var.enable_services ? [ @@ -168,16 +167,15 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { role = each.value } -resource "google_project_service_identity" "sa_cloudbuild_identity" { - provider = google-beta - project = local.project.project_id - service = "cloudbuild.googleapis.com" +resource "google_service_account" "sa_image_builder_identity" { + project = local.project.project_id + account_id = "sa-image-builder-identity" } -resource "google_project_iam_member" "sa_p_cloudbuild_permissions" { - for_each = toset(local.cloudbuild_sa_project_roles) +resource "google_project_iam_member" "sa_image_builder_permissions" { + for_each = toset(local.image_builder_sa_project_roles) project = local.project.project_id - member = "serviceAccount:${google_project_service_identity.sa_cloudbuild_identity.email}" + member = "serviceAccount:${google_service_account.sa_image_builder_identity.email}" role = each.value } @@ -280,12 +278,12 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_project_service_identity.sa_cloudbuild_identity.email}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_service_account.sa_image_builder_identity.email}" } depends_on = [ google_artifact_registry_repository.containers_repo, google_storage_bucket.notebooks_bucket, - google_project_iam_member.sa_p_cloudbuild_permissions, + google_project_iam_member.sa_image_builder_permissions, ] } From d62ca1a2dbeafd99b8d18e463a9e304377bb935e Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 16 Jun 2022 03:13:51 +0900 Subject: [PATCH 31/93] modules/silicon: grant cloudbuild sa image_builder access --- modules/silicon_design/main.tf | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index d70b5de1..bbc1924b 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -46,6 +46,11 @@ locals { "roles/storage.objectViewer", ] + cloudbuild_sa_project_roles = [ + "roles/compute.admin", + "roles/storage.admin", + ] + image_builder_sa_project_roles = [ "roles/compute.instanceAdmin", "roles/compute.storageAdmin", @@ -167,6 +172,25 @@ resource "google_project_iam_member" "sa_p_notebook_permissions" { role = each.value } +resource "google_project_service_identity" "sa_cloudbuild_identity" { + provider = google-beta + project = local.project.project_id + service = "cloudbuild.googleapis.com" +} + +resource "google_project_iam_member" "sa_cloudbuild_permissions" { + for_each = toset(local.cloudbuild_sa_project_roles) + member = "serviceAccount:${google_project_service_identity.sa_cloudbuild_identity.email}" + project = local.project.project_id + role = each.value +} + +resource "google_service_account_iam_member" "sa_cloudbuild_image_builder_access" { + member = "serviceAccount:${google_project_service_identity.sa_cloudbuild_identity.email}" + role = "roles/iam.serviceAccountUser" + service_account_id = google_service_account.sa_image_builder_identity.id +} + resource "google_service_account" "sa_image_builder_identity" { project = local.project.project_id account_id = "sa-image-builder-identity" @@ -285,5 +309,7 @@ resource "null_resource" "build_and_push_image" { google_artifact_registry_repository.containers_repo, google_storage_bucket.notebooks_bucket, google_project_iam_member.sa_image_builder_permissions, + google_project_iam_member.sa_cloudbuild_permissions, + google_service_account_iam_member.sa_cloudbuild_image_builder_access, ] } From 946416f98fcd28b0e269b461770e01b2b9f4a9fe Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 22 Jun 2022 16:35:48 +0900 Subject: [PATCH 32/93] modules/silicon_design: fix env and add container test --- modules/silicon_design/scripts/build/cloudbuild.yaml | 4 ++++ modules/silicon_design/scripts/build/images/provision/env.tcl | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 026063d3..6e653b91 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -55,6 +55,10 @@ steps: name: 'gcr.io/cloud-builders/docker' args: ['tag', '$_CONTAINER_IMAGE:$BUILD_ID', '$_CONTAINER_IMAGE:latest'] waitFor: ['container-image-build'] +- id: 'container-image-test' + name: 'gcr.io/cloud-builders/docker' + args: ['run', '$_CONTAINER_IMAGE:$BUILD_ID', 'flow.tcl', '-design', 'inverter'] + waitFor: ['container-image-tag'] images: - '$_CONTAINER_IMAGE:$BUILD_ID' - '$_CONTAINER_IMAGE:latest' diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/env.tcl index e0424a70..1efb9c49 100644 --- a/modules/silicon_design/scripts/build/images/provision/env.tcl +++ b/modules/silicon_design/scripts/build/images/provision/env.tcl @@ -17,7 +17,7 @@ set ::env(PDK_ROOT) "$::env(CONDA_PREFIX)/share/pdk" set ::env(TCLLIBPATH) "$::env(CONDA_PREFIX)/opt/conda/lib/tcllib1.20" set ::env(OL_INSTALL_DIR) "$::env(OPENLANE_ROOT)/install" set ::env(OPENLANE_LOCAL_INSTALL) 1 -set ::env(MISMATCHES_OK) 1 +set ::env(TEST_MISMATCHES) none set ::env(RUN_CVC) 0 set ::env(RUN_KLAYOUT_XOR) 0 set ::env(RUN_KLAYOUT_DRC) 0 From 5eda48126857f4c9e50b4c864e20e7e3afade08b Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 13 Jul 2022 22:49:18 +0900 Subject: [PATCH 33/93] modules/silicon_design/environment: drop yosys constraint --- .../scripts/build/images/provision/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index 8d4b66be..8595cafe 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -21,7 +21,7 @@ dependencies: - magic - openroad - netgen - - yosys>=0.15 + - yosys - gdstk - ngspice-lib - python From f460c9e0b97663d31ba8680a87c8d60aba7a4c88 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 13 Jul 2022 22:49:36 +0900 Subject: [PATCH 34/93] modules/silicon_design/environment: add xls --- .../scripts/build/images/provision/environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index 8595cafe..fde67cf7 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -28,6 +28,7 @@ dependencies: - pip - tcllib - iverilog + - xls - pip: - pyyaml - click From 344f11679bd3d7f0f2bf170cdf77d06be5393bd0 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 13 Jul 2022 22:50:44 +0900 Subject: [PATCH 35/93] modules/silicon_design: workaround OpenLane/issues/1195 --- modules/silicon_design/scripts/build/images/provision/env.tcl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/env.tcl index 1efb9c49..f00087ad 100644 --- a/modules/silicon_design/scripts/build/images/provision/env.tcl +++ b/modules/silicon_design/scripts/build/images/provision/env.tcl @@ -22,3 +22,6 @@ set ::env(RUN_CVC) 0 set ::env(RUN_KLAYOUT_XOR) 0 set ::env(RUN_KLAYOUT_DRC) 0 set ::env(RUN_KLAYOUT) 0 +# https://github.com/The-OpenROAD-Project/OpenLane/issues/1195 +set ::env(DIODE_INSERTION_STRATEGY) 0 +set ::env(USE_ARC_ANTENNA_CHECK) 0 From 61024a161c977236845e7d904835fc5818b03609 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 13 Jul 2022 22:51:00 +0900 Subject: [PATCH 36/93] modules/silicon_design: use separate environment --- modules/silicon_design/scripts/build/images/provision.sh | 7 +++++-- .../scripts/build/images/provision/profile.sh | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index d21800ed..a0911729 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -31,8 +31,8 @@ gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true fi echo "DaisyStatus: installing conda-eda environment" -/opt/conda/bin/conda install --yes --prefix /opt/conda/ mamba -/opt/conda/bin/mamba env update --prefix /opt/conda/ --file ${PROVISION_DIR}/environment.yml +curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -C /usr/local -xvj bin/micromamba +micromamba create --yes -r /opt/conda -n silicon --file ${PROVISION_DIR}/environment.yml echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane @@ -50,4 +50,7 @@ curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh +echo "DaisyStatus: patch entrypoint" +sed -i -e 's/conda activate base/conda activate base\nconda activate silicon/' /entrypoint.sh + echo "DaisySuccess: done" diff --git a/modules/silicon_design/scripts/build/images/provision/profile.sh b/modules/silicon_design/scripts/build/images/provision/profile.sh index 465c2079..58b6b8f8 100644 --- a/modules/silicon_design/scripts/build/images/provision/profile.sh +++ b/modules/silicon_design/scripts/build/images/provision/profile.sh @@ -15,3 +15,6 @@ export OPENLANE_ROOT=/OpenLane export PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH +. /opt/conda/etc/profile.d/conda.sh +conda activate base +conda activate silicon From 9919aca7fbe5b8c07e23c6ceccf0ef8f147e31cd Mon Sep 17 00:00:00 2001 From: Mukul Gupta Date: Mon, 18 Jul 2022 15:27:01 -0700 Subject: [PATCH 37/93] Added bash to run script command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added bash to run script command to avoid below error: ``` â•· │ Error: local-exec provisioner error │ │ with null_resource.build_and_push_image, │ on main.tf line 303, in resource "null_resource" "build_and_push_image": │ 303: provisioner "local-exec" { │ │ Error running command 'scripts/build/build.sh radlab-silicon-design-*** │ us-east4-c silicon-design-ubuntu-2004 │ us-east4-docker.pkg.dev/radlab-silicon-design-***/containers/silicon-design-ubuntu-2004 │ radlab-silicon-design-***-silicon-design-notebooks │ projects/radlab-silicon-design-***/global/networks/ai-notebook │ projects/radlab-silicon-design-***/regions/us-east4/subnetworks/subnet-ai-notebook │ sa-image-builder-identity@radlab-silicon-design-****.iam.gserviceaccount.com': │ exit status 126. Output: /bin/sh: line 1: scripts/build/build.sh: Permission │ denied ``` --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index bbc1924b..d2ce48bf 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -302,7 +302,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_service_account.sa_image_builder_identity.email}" + command = "bash scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_service_account.sa_image_builder_identity.email}" } depends_on = [ From 5847d7ae349d0fc9787f752fb9000a3631b9f14e Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 17:07:34 +0900 Subject: [PATCH 38/93] modules/silicon_design: use name prefix --- modules/silicon_design/main.tf | 21 ++++++++++++--------- modules/silicon_design/variables.tf | 9 +++++++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index d2ce48bf..fde53d00 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -3,7 +3,7 @@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at +nnn * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -26,6 +26,9 @@ locals { ) region = join("-", [split("-", var.zone)[0], split("-", var.zone)[1]]) + network_name = var.network_name != null ? var.network_name : "${var.name}-network" + subnet_name = var.subnet_name != null ? var.subnet_name : "${var.name}-subnet" + network = ( var.create_network ? try(module.vpc_ai_notebook.0.network.network, null) @@ -34,7 +37,7 @@ locals { subnet = ( var.create_network - ? try(module.vpc_ai_notebook.0.subnets["${local.region}/${var.subnet_name}"], null) + ? try(module.vpc_ai_notebook.0.subnets["${local.region}/${local.subnet_name}"], null) : try(data.google_compute_subnetwork.default.0, null) ) @@ -64,7 +67,7 @@ locals { "artifactregistry.googleapis.com", ] : [] - notebook_names = length(var.notebook_names) > 0 ? var.notebook_names : [for i in range(var.notebook_count): "silicon-design-notebook-${i}"] + notebook_names = length(var.notebook_names) > 0 ? var.notebook_names : [for i in range(var.notebook_count): "${var.name}-nodebook-${i}"] } resource "random_id" "default" { @@ -109,13 +112,13 @@ resource "google_project_service" "enabled_services" { data "google_compute_network" "default" { count = var.create_network ? 0 : 1 project = local.project.project_id - name = var.network_name + name = local.network_name } data "google_compute_subnetwork" "default" { count = var.create_network ? 0 : 1 project = local.project.project_id - name = var.subnet_name + name = local.subnet_name region = local.region } @@ -125,13 +128,13 @@ module "vpc_ai_notebook" { version = "~> 3.0" project_id = local.project.project_id - network_name = var.network_name + network_name = local.network_name routing_mode = "GLOBAL" description = "VPC Network created via Terraform" subnets = [ { - subnet_name = var.subnet_name + subnet_name = local.subnet_name subnet_ip = var.ip_cidr_range subnet_region = local.region description = "Subnetwork inside *vpc-silicon-design* VPC network, created via Terraform" @@ -269,7 +272,7 @@ resource "google_artifact_registry_repository" "containers_repo" { project = local.project.project_id location = local.region - repository_id = "containers" + repository_id = "${var.name}-containers" description = "container image repository" format = "DOCKER" @@ -280,7 +283,7 @@ resource "google_artifact_registry_repository" "containers_repo" { resource "google_storage_bucket" "notebooks_bucket" { project = local.project.project_id - name = "${local.project.project_id}-silicon-design-notebooks" + name = "${var.name}-notebooks" location = local.region force_destroy = true uniform_bucket_level_access = true diff --git a/modules/silicon_design/variables.tf b/modules/silicon_design/variables.tf index 6c247840..833b948a 100644 --- a/modules/silicon_design/variables.tf +++ b/modules/silicon_design/variables.tf @@ -14,6 +14,11 @@ * limitations under the License. */ +variable "name" { + description = "Name prefix used for naming child radlab resources" + type = string +} + variable "billing_account_id" { description = "Billing Account associated to the GCP Resources" type = string @@ -70,7 +75,7 @@ variable "machine_type" { variable "network_name" { description = "Name of the network to be created." type = string - default = "ai-notebook" + default = null } variable "notebook_count" { @@ -124,7 +129,7 @@ variable "set_trustedimage_project_policy" { variable "subnet_name" { description = "Name of the subnet where to deploy the Notebooks." type = string - default = "subnet-ai-notebook" + default = null } variable "trusted_users" { From 926bbf2561882643a3414d51bbda2b283d04f621 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 17:54:31 +0900 Subject: [PATCH 39/93] modules/silicon_design: use prefix --- modules/silicon_design/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index fde53d00..c3227041 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -144,7 +144,7 @@ module "vpc_ai_notebook" { firewall_rules = [ { - name = "fw-silicon-design-notebook-allow-internal" + name = "${var.name}-allow-internal" description = "Firewall rule to allow traffic on all ports inside *vpc-silicon-design* VPC network." priority = 65534 ranges = ["10.0.0.0/8"] @@ -164,7 +164,7 @@ module "vpc_ai_notebook" { resource "google_service_account" "sa_p_notebook" { project = local.project.project_id - account_id = format("sa-p-notebook-%s", local.random_id) + account_id = "${var.name}-sa-notebook" display_name = "Notebooks in trusted environment" } From e9f9e1eb4a6e66d2de38f5b25c9efa2dcbe3967b Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 17:57:48 +0900 Subject: [PATCH 40/93] modules/silicon_design: fix sa length --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index c3227041..4c4477bf 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -164,7 +164,7 @@ module "vpc_ai_notebook" { resource "google_service_account" "sa_p_notebook" { project = local.project.project_id - account_id = "${var.name}-sa-notebook" + account_id = "${var.name}-sa" display_name = "Notebooks in trusted environment" } From 070c329f60d972ff192b9dab47706e203d4c328c Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 18:01:19 +0900 Subject: [PATCH 41/93] modules/silicon_design: remove bash command prefix --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 4c4477bf..e23f6c3e 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -305,7 +305,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "bash scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_service_account.sa_image_builder_identity.email}" + command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_service_account.sa_image_builder_identity.email}" } depends_on = [ From 92f08eb3eae814360ee9dab3a47f80340f09dd83 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 20:42:36 +0900 Subject: [PATCH 42/93] modules/silicon_design: remove intermediate script --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/build.sh | 29 ------------------- 2 files changed, 1 insertion(+), 30 deletions(-) delete mode 100755 modules/silicon_design/scripts/build/build.sh diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index e23f6c3e..c2367713 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -305,7 +305,7 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "scripts/build/build.sh ${local.project.project_id} ${var.zone} ${var.image_name} ${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name} ${google_storage_bucket.notebooks_bucket.name} ${local.network.id} ${local.subnet.id} ${google_service_account.sa_image_builder_identity.email}" + command = "gcloud --project=${local.project.project_id} builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions \"_ZONE=${var.zone},_COMPUTE_IMAGE=${var.image_name},_CONTAINER_IMAGE=${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name},_NOTEBOOKS_BUCKET=${google_storage_bucket.notebooks_bucket.name},_COMPUTE_NETWORK=${local.network.id},_COMPUTE_SUBNET=${local.subnet.id},_CLOUD_BUILD_SA=${google_service_account.sa_image_builder_identity.email}\"" } depends_on = [ diff --git a/modules/silicon_design/scripts/build/build.sh b/modules/silicon_design/scripts/build/build.sh deleted file mode 100755 index 042866e3..00000000 --- a/modules/silicon_design/scripts/build/build.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -e - -PROJECT_ID=$1 -ZONE=$2 -COMPUTE_IMAGE=$3 -CONTAINER_IMAGE=$4 -NOTEBOOKS_BUCKET=$5 -COMPUTE_NETWORK=$6 -COMPUTE_SUBNET=$7 -CLOUD_BUILD_SA=$8 - -gcloud config set project ${PROJECT_ID} -gcloud builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions "_ZONE=${ZONE},_COMPUTE_IMAGE=${COMPUTE_IMAGE},_CONTAINER_IMAGE=${CONTAINER_IMAGE},_NOTEBOOKS_BUCKET=${NOTEBOOKS_BUCKET},_COMPUTE_NETWORK=${COMPUTE_NETWORK},_COMPUTE_SUBNET=${COMPUTE_SUBNET},_CLOUD_BUILD_SA=${CLOUD_BUILD_SA}" From c2369c8c5bea786291341263fac5b26a678321a0 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 20:56:28 +0900 Subject: [PATCH 43/93] modules/silicon_design: remove obsolete file --- modules/silicon_design/main.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index c2367713..1a5952cf 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -294,7 +294,6 @@ resource "google_storage_bucket" "notebooks_bucket" { resource "null_resource" "build_and_push_image" { triggers = { cloudbuild_yaml_sha = filesha1("${path.module}/scripts/build/cloudbuild.yaml") - build_script_sha = filesha1("${path.module}/scripts/build/build.sh") workflow_sha = filesha1("${path.module}/scripts/build/images/compute_image.wf.json") dockerfile_sha = filesha1("${path.module}/scripts/build/images/Dockerfile") environment_sha = filesha1("${path.module}/scripts/build/images/provision/environment.yml") From b86edef9a0c60b989795b93e11eb79704504d7f8 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 21:32:00 +0900 Subject: [PATCH 44/93] modules/silicon_design: add install-wide tcl for openlane --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/images/provision.sh | 5 ++--- .../scripts/build/images/provision/{env.tcl => install.tcl} | 0 3 files changed, 3 insertions(+), 4 deletions(-) rename modules/silicon_design/scripts/build/images/provision/{env.tcl => install.tcl} (100%) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 1a5952cf..bde6a127 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -297,7 +297,7 @@ resource "null_resource" "build_and_push_image" { workflow_sha = filesha1("${path.module}/scripts/build/images/compute_image.wf.json") dockerfile_sha = filesha1("${path.module}/scripts/build/images/Dockerfile") environment_sha = filesha1("${path.module}/scripts/build/images/provision/environment.yml") - env_sha = filesha1("${path.module}/scripts/build/images/provision/env.tcl") + env_sha = filesha1("${path.module}/scripts/build/images/provision/install.tcl") profile_sha = filesha1("${path.module}/scripts/build/images/provision/profile.sh") notebook_sha = filesha1("${path.module}/scripts/build/notebooks/inverter.md") } diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index a0911729..a723d501 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -39,13 +39,12 @@ git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Proje echo "DaisyStatus: patching OpenLane" mkdir -p /OpenLane/install/build/versions -cp ${PROVISION_DIR}/env.tcl /OpenLane/install/ +cp ${PROVISION_DIR}/install.tcl /OpenLane/configuration/ for tool in yosys netgen do /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} done -# https://github.com/The-OpenROAD-Project/OpenLane/pull/1027 -curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project/OpenLane/pull/1027.patch | patch -d /OpenLane -p1 +echo 'install.tcl' >> /OpenLane/configuration/load_order.txt echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh diff --git a/modules/silicon_design/scripts/build/images/provision/env.tcl b/modules/silicon_design/scripts/build/images/provision/install.tcl similarity index 100% rename from modules/silicon_design/scripts/build/images/provision/env.tcl rename to modules/silicon_design/scripts/build/images/provision/install.tcl From 7f560306ca7dfac0a8fcd17714784c315a692eaf Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 21:54:29 +0900 Subject: [PATCH 45/93] modules/silicon_design: patch entrypoint --- modules/silicon_design/scripts/build/images/Dockerfile | 1 + modules/silicon_design/scripts/build/images/provision.sh | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile index 2e3f4a3a..4a2ee846 100644 --- a/modules/silicon_design/scripts/build/images/Dockerfile +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -18,5 +18,6 @@ RUN apt-get update && apt-get -yq install locales locales-all COPY provision.sh /tmp/provision.sh COPY provision/ /tmp/provision/ RUN bash -x /tmp/provision.sh +RUN sed -i -e 's/conda activate base/conda activate base\nconda activate silicon/' /entrypoint.sh ENV OPENLANE_ROOT=/OpenLane ENV PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index a723d501..17c809c2 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -49,7 +49,4 @@ echo 'install.tcl' >> /OpenLane/configuration/load_order.txt echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh -echo "DaisyStatus: patch entrypoint" -sed -i -e 's/conda activate base/conda activate base\nconda activate silicon/' /entrypoint.sh - echo "DaisySuccess: done" From 1e645b397a6a5eff6d718d405213588c8c222510 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 22:10:20 +0900 Subject: [PATCH 46/93] modules/silicon_design: fix install.tcl --- modules/silicon_design/scripts/build/images/provision.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 17c809c2..01f5ad6d 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -44,7 +44,7 @@ for tool in yosys netgen do /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} done -echo 'install.tcl' >> /OpenLane/configuration/load_order.txt +echo ' install.tcl' >> /OpenLane/configuration/load_order.txt echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh From 9300029fc0fa40fabde94797eb08cd9368d8ab26 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 23:43:51 +0900 Subject: [PATCH 47/93] modules/silicon_design: refresh inverter config --- .../scripts/build/notebooks/inverter.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/modules/silicon_design/scripts/build/notebooks/inverter.md b/modules/silicon_design/scripts/build/notebooks/inverter.md index d7b78edb..fe3f2aea 100644 --- a/modules/silicon_design/scripts/build/notebooks/inverter.md +++ b/modules/silicon_design/scripts/build/notebooks/inverter.md @@ -37,24 +37,18 @@ See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/Ope ```python %%bash -c 'cat > config.tcl; tclsh config.tcl' +%%bash -c 'cat > config.tcl; tclsh config.tcl' set ::env(DESIGN_NAME) inverter -set script_dir [file dirname [file normalize [info script]]] -set ::env(VERILOG_FILES) "$script_dir/inverter.v" - -set ::env(CLOCK_TREE_SYNTH) 0 -set ::env(CLOCK_PORT) "" - -set ::env(PL_RANDOM_GLB_PLACEMENT) 1 +set ::env(VERILOG_FILES) "inverter.v" set ::env(FP_SIZING) absolute -set ::env(DIE_AREA) "0 0 34.165 54.885" +set ::env(DIE_AREA) "0 0 50 50" set ::env(PL_TARGET_DENSITY) 0.75 -set ::env(FP_PDN_HORIZONTAL_HALO) 6 -set ::env(FP_PDN_VERTICAL_HALO) 6 - -set ::env(DIODE_INSERTION_STRATEGY) 3 +set ::env(CLOCK_TREE_SYNTH) 0 +set ::env(CLOCK_PORT) "" +set ::env(DIODE_INSERTION_STRATEGY) 0 ``` ## Run OpenLane flow From 1ed10e56f73022a2eafd39520051c7516e812561 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 23:44:53 +0900 Subject: [PATCH 48/93] modules/silicon_design: refresh conda environment --- .../build/images/provision/environment.yml | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index fde67cf7..4ea95518 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -16,23 +16,26 @@ channels: - litex-hub - conda-forge dependencies: - # https://github.com/The-OpenROAD-Project/OpenLane/pull/978 - open_pdks.sky130a - magic - openroad - netgen - yosys - gdstk - - ngspice-lib - - python - - pip - tcllib - iverilog - xls + - pyspice + - pyyaml + - click + - gdsfactory + - pandas + - pymeep=*=mpi_mpich_* + - jupyterlab + - python + - pip - pip: - - pyyaml - - click - - pandas - - pyspice - - gdsfactory - klayout + - scrapbook[gcs] + - google-cloud-aiplatform + - cloudml-hypertune From 9efebfae5ee1d961c35a22791c9078a74d1cc086 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 23:45:15 +0900 Subject: [PATCH 49/93] modules/silicon_design: patch openlane --- modules/silicon_design/scripts/build/images/provision.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 01f5ad6d..0571fb3d 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -38,13 +38,14 @@ echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane echo "DaisyStatus: patching OpenLane" -mkdir -p /OpenLane/install/build/versions cp ${PROVISION_DIR}/install.tcl /OpenLane/configuration/ +echo ' install.tcl' >> /OpenLane/configuration/load_order.txt +mkdir -p /OpenLane/install/build/versions for tool in yosys netgen do /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} done -echo ' install.tcl' >> /OpenLane/configuration/load_order.txt +curl -L https://github.com/The-OpenROAD-Project/OpenLane/pull/1229.patch | patch -p1 -d /OpenLane echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh From 5c835456c3b5cd620c015fbaec3c12b8559060b7 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 22 Jul 2022 23:45:35 +0900 Subject: [PATCH 50/93] modules/silicon_design: patch run_jupyter --- modules/silicon_design/scripts/build/images/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile index 4a2ee846..5802f93e 100644 --- a/modules/silicon_design/scripts/build/images/Dockerfile +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -19,5 +19,6 @@ COPY provision.sh /tmp/provision.sh COPY provision/ /tmp/provision/ RUN bash -x /tmp/provision.sh RUN sed -i -e 's/conda activate base/conda activate base\nconda activate silicon/' /entrypoint.sh +RUN sed -i -e 's@/opt/conda@/opt/conda/envs/silicon@' /run_jupyter.sh ENV OPENLANE_ROOT=/OpenLane ENV PATH=$OPENLANE_ROOT:$OPENLANE_ROOT/scripts:$PATH From 7bb398121157b74231087487c95e7fbf12961af8 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Sat, 23 Jul 2022 02:25:34 +0900 Subject: [PATCH 51/93] modules/silicon_design/terraform: make image build name dependent --- modules/silicon_design/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index bde6a127..03bbe71d 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -164,7 +164,7 @@ module "vpc_ai_notebook" { resource "google_service_account" "sa_p_notebook" { project = local.project.project_id - account_id = "${var.name}-sa" + account_id = "${var.name}-n-sa" display_name = "Notebooks in trusted environment" } @@ -196,7 +196,7 @@ resource "google_service_account_iam_member" "sa_cloudbuild_image_builder_access resource "google_service_account" "sa_image_builder_identity" { project = local.project.project_id - account_id = "sa-image-builder-identity" + account_id = "${var.name}-i-sa" } resource "google_project_iam_member" "sa_image_builder_permissions" { From a8b6f9a9968733f027c4fb5224dda54fd7b4688c Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Sat, 23 Jul 2022 02:44:09 +0900 Subject: [PATCH 52/93] modules/silicon_design/terraform: do not disable services --- modules/silicon_design/main.tf | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 03bbe71d..a5423f6a 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -101,8 +101,12 @@ resource "google_project_service" "enabled_services" { for_each = toset(local.project_services) project = local.project.project_id service = each.value - disable_dependent_services = true - disable_on_destroy = true + disable_dependent_services = false + disable_on_destroy = false + + lifecycle { + prevent_destroy = true + } depends_on = [ module.project_radlab_silicon_design From 76f421398ae1dba88e099aa684b87ad1aebcd7ad Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 26 Jul 2022 15:51:01 +0900 Subject: [PATCH 53/93] modules/silicon_design: drop patches --- modules/silicon_design/scripts/build/images/provision.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 0571fb3d..683ed477 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -45,7 +45,6 @@ for tool in yosys netgen do /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} done -curl -L https://github.com/The-OpenROAD-Project/OpenLane/pull/1229.patch | patch -p1 -d /OpenLane echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh From 6cd52058e83e5983e7b31813ef12f3992353eaad Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 15:29:28 +0900 Subject: [PATCH 54/93] bump provider version --- modules/silicon_design/main.tf | 4 ++-- modules/silicon_design/versions.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index a5423f6a..90b6fde2 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -86,7 +86,7 @@ data "google_project" "existing_project" { module "project_radlab_silicon_design" { count = var.create_project ? 1 : 0 source = "terraform-google-modules/project-factory/google" - version = "~> 11.0" + version = "~> 13.0" name = format("%s-%s", var.project_name, local.random_id) random_project_id = false @@ -129,7 +129,7 @@ data "google_compute_subnetwork" "default" { module "vpc_ai_notebook" { count = var.create_network ? 1 : 0 source = "terraform-google-modules/network/google" - version = "~> 3.0" + version = "~> 5.0" project_id = local.project.project_id network_name = local.network_name diff --git a/modules/silicon_design/versions.tf b/modules/silicon_design/versions.tf index 5a19b143..3de89291 100644 --- a/modules/silicon_design/versions.tf +++ b/modules/silicon_design/versions.tf @@ -18,7 +18,7 @@ terraform { required_version = "~> 1.0" required_providers { - google = ">= 3.87.0" - google-beta = ">= 3.87.0" + google = ">= 4.22.0" + google-beta = ">= 4.22.0" } } From f9dfd53325ece30cd3544b342dcdbee4244a631c Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 15:30:06 +0900 Subject: [PATCH 55/93] add bucket variable --- modules/silicon_design/outputs.tf | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/silicon_design/outputs.tf b/modules/silicon_design/outputs.tf index 157ca1e0..0e0902b6 100644 --- a/modules/silicon_design/outputs.tf +++ b/modules/silicon_design/outputs.tf @@ -29,11 +29,6 @@ output "notebooks_instance_names" { value = join(", ", google_notebooks_instance.ai_notebook[*].name) } -output "notebooks_bucket_name" { - description = "Notebooks GCS Bucket Name" - value = google_storage_bucket.notebooks_bucket.name -} - output "artifact_registry_repository_id" { description = "Artifact Registry Repository ID" value = google_artifact_registry_repository.containers_repo.repository_id From 99a50ed92c06fda3b47d3e5d2ad9fb86846b86c7 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 12 Apr 2022 22:05:39 +0900 Subject: [PATCH 56/93] modules/silicon_design: add parameter tuning notebook --- modules/silicon_design/main.tf | 48 ++++- modules/silicon_design/outputs.tf | 7 +- .../scripts/build/cloudbuild.yaml | 16 +- .../scripts/build/images/provision.sh | 9 +- .../build/images/provision/environment.yml | 4 +- .../build/images/provision/install.tcl | 3 - .../build/images/provision/papermill-launcher | 28 +++ .../scripts/build/notebooks/inverter.md | 113 ++++++++-- .../scripts/build/notebooks/openram.md | 113 ++++++++++ .../scripts/build/notebooks/serv.md | 154 ++++++++++++++ .../scripts/build/notebooks/tuning.md | 96 +++++++++ .../scripts/build/notebooks/xls.md | 195 ++++++++++++++++++ modules/silicon_design/variables.tf | 8 +- 13 files changed, 756 insertions(+), 38 deletions(-) create mode 100755 modules/silicon_design/scripts/build/images/provision/papermill-launcher create mode 100644 modules/silicon_design/scripts/build/notebooks/openram.md create mode 100644 modules/silicon_design/scripts/build/notebooks/serv.md create mode 100644 modules/silicon_design/scripts/build/notebooks/tuning.md create mode 100644 modules/silicon_design/scripts/build/notebooks/xls.md diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 90b6fde2..a9a9c68c 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -47,6 +47,7 @@ locals { "roles/compute.instanceAdmin", "roles/iam.serviceAccountUser", "roles/storage.objectViewer", + "roles/aiplatform.admin", ] cloudbuild_sa_project_roles = [ @@ -65,9 +66,11 @@ locals { "notebooks.googleapis.com", "cloudbuild.googleapis.com", "artifactregistry.googleapis.com", + "aiplatform.googleapis.com", ] : [] notebook_names = length(var.notebook_names) > 0 ? var.notebook_names : [for i in range(var.notebook_count): "${var.name}-nodebook-${i}"] + image_tag = var.image_tag != "" ? var.image_tag : formatdate("YYYYMMDDhhmm", timestamp()) } resource "random_id" "default" { @@ -212,7 +215,7 @@ resource "google_project_iam_member" "sa_image_builder_permissions" { resource "google_service_account_iam_member" "sa_ai_notebook_user_iam" { for_each = var.trusted_users - member = each.value + member = "user:${each.value}" role = "roles/iam.serviceAccountUser" service_account_id = google_service_account.sa_p_notebook.id } @@ -220,17 +223,49 @@ resource "google_service_account_iam_member" "sa_ai_notebook_user_iam" { resource "google_project_iam_member" "ai_notebook_user_role1" { for_each = var.trusted_users project = local.project.project_id - member = each.value + member = "user:${each.value}" role = "roles/notebooks.admin" } resource "google_project_iam_member" "ai_notebook_user_role2" { for_each = var.trusted_users project = local.project.project_id - member = each.value + member = "user:${each.value}" role = "roles/viewer" } +resource "google_notebooks_runtime" "ai_notebook_managed" { + count = var.notebook_count + project = local.project.project_id + name = local.notebook_names[count.index] + location = local.region + + access_config { + access_type = "SINGLE_USER" + runtime_owner = "proppy@google.com" + } + virtual_machine { + virtual_machine_config { + machine_type = var.machine_type + data_disk { + initialize_params { + disk_size_gb = "100" + disk_type = "PD_STANDARD" + } + } + container_images { + repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}" + tag = local.image_tag + } + } + } + + depends_on = [ + time_sleep.wait_120_seconds, + null_resource.build_and_push_image, + ] +} + resource "google_notebooks_instance" "ai_notebook" { count = var.notebook_count project = local.project.project_id @@ -240,9 +275,9 @@ resource "google_notebooks_instance" "ai_notebook" { container_image { repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}" - tag = "latest" + tag = local.image_tag } - + service_account = google_service_account.sa_p_notebook.email install_gpu_driver = false @@ -304,11 +339,12 @@ resource "null_resource" "build_and_push_image" { env_sha = filesha1("${path.module}/scripts/build/images/provision/install.tcl") profile_sha = filesha1("${path.module}/scripts/build/images/provision/profile.sh") notebook_sha = filesha1("${path.module}/scripts/build/notebooks/inverter.md") + image_tag = local.image_tag } provisioner "local-exec" { working_dir = path.module - command = "gcloud --project=${local.project.project_id} builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions \"_ZONE=${var.zone},_COMPUTE_IMAGE=${var.image_name},_CONTAINER_IMAGE=${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name},_NOTEBOOKS_BUCKET=${google_storage_bucket.notebooks_bucket.name},_COMPUTE_NETWORK=${local.network.id},_COMPUTE_SUBNET=${local.subnet.id},_CLOUD_BUILD_SA=${google_service_account.sa_image_builder_identity.email}\"" + command = "gcloud --project=${local.project.project_id} builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions \"_ZONE=${var.zone},_COMPUTE_IMAGE=${var.image_name},_CONTAINER_IMAGE=${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name},_NOTEBOOKS_BUCKET=${google_storage_bucket.notebooks_bucket.name},_COMPUTE_NETWORK=${local.network.id},_COMPUTE_SUBNET=${local.subnet.id},_IMAGE_TAG=${local.image_tag},_CLOUD_BUILD_SA=${google_service_account.sa_image_builder_identity.email}\"" } depends_on = [ diff --git a/modules/silicon_design/outputs.tf b/modules/silicon_design/outputs.tf index 0e0902b6..aa1c54e3 100644 --- a/modules/silicon_design/outputs.tf +++ b/modules/silicon_design/outputs.tf @@ -36,5 +36,10 @@ output "artifact_registry_repository_id" { output "notebooks_container_image" { description = "Container Image URI" - value = "${google_notebooks_instance.ai_notebook[0].container_image[0].repository}:${google_notebooks_instance.ai_notebook[0].container_image[0].tag}" + value = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}:${local.image_tag}" +} + +output "notebooks_vm" { + description = "GCE VM Image Name" + value = "${var.image_name}-${local.image_tag}" } diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 6e653b91..af970f57 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -18,6 +18,7 @@ substitutions: _ZONE: 'asia-northeast1-a' _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' + _IMAGE_TAG: 'default-tag' _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' _COMPUTE_NETWORK: 'global/networks/default' _COMPUTE_SUBNET: '' @@ -45,22 +46,27 @@ steps: cd scripts/build/images/ gsutil cp gs://compute-image-tools/release/linux/daisy . chmod +x daisy - ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$BUILD_ID,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET,service_account=$_CLOUD_BUILD_SA compute_image.wf.json + echo gcloud compute images describe $_COMPUTE_IMAGE-$_IMAGE_TAG || ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$_IMAGE_TAG,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET,service_account=$_CLOUD_BUILD_SA compute_image.wf.json waitFor: ['-'] +- id: 'container-image-pull' + name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: ['-c', 'docker pull $_CONTAINER_IMAGE:latest || exit 0'] + waitFor: ['-'] - id: 'container-image-build' name: 'gcr.io/cloud-builders/docker' - args: ['build', '-t', '$_CONTAINER_IMAGE:$BUILD_ID', './scripts/build/images'] - waitFor: ['-'] + args: ['build', '-t', '$_CONTAINER_IMAGE:$_IMAGE_TAG', '--cache-from', '$_CONTAINER_IMAGE:latest', './scripts/build/images'] + waitFor: ['container-image-pull'] - id: 'container-image-tag' name: 'gcr.io/cloud-builders/docker' - args: ['tag', '$_CONTAINER_IMAGE:$BUILD_ID', '$_CONTAINER_IMAGE:latest'] + args: ['tag', '$_CONTAINER_IMAGE:$_IMAGE_TAG', '$_CONTAINER_IMAGE:latest'] waitFor: ['container-image-build'] - id: 'container-image-test' name: 'gcr.io/cloud-builders/docker' args: ['run', '$_CONTAINER_IMAGE:$BUILD_ID', 'flow.tcl', '-design', 'inverter'] waitFor: ['container-image-tag'] images: -- '$_CONTAINER_IMAGE:$BUILD_ID' +- '$_CONTAINER_IMAGE:$_IMAGE_TAG' - '$_CONTAINER_IMAGE:latest' artifacts: objects: diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 683ed477..1e36fc5b 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -21,14 +21,10 @@ env OPENLANE_VERSION=master PROVISION_DIR=/tmp/provision -SYSTEM_NAME=$(dmidecode -s system-product-name || true) - -if [ -n "$(echo ${SYSTEM_NAME} | grep 'Google Compute Engine')" ]; then echo "DaisyStatus: fetching provisioning script" DAISY_SOURCES_PATH=$(curl -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/daisy-sources-path) mkdir -p ${PROVISION_DIR} gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true -fi echo "DaisyStatus: installing conda-eda environment" curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -C /usr/local -xvj bin/micromamba @@ -49,4 +45,9 @@ done echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh +echo "DaisyStatus: adding papermill launcher" +cp ${PROVISION_DIR}/papermill-launcher /usr/local/bin/ +chmod +x /usr/local/bin/papermill-launcher + echo "DaisySuccess: done" + diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index 4ea95518..a90b0204 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -22,6 +22,8 @@ dependencies: - netgen - yosys - gdstk + - cairosvg + - svgutils - tcllib - iverilog - xls @@ -38,4 +40,4 @@ dependencies: - klayout - scrapbook[gcs] - google-cloud-aiplatform - - cloudml-hypertune + - cloudml-hypertune diff --git a/modules/silicon_design/scripts/build/images/provision/install.tcl b/modules/silicon_design/scripts/build/images/provision/install.tcl index f00087ad..1efb9c49 100644 --- a/modules/silicon_design/scripts/build/images/provision/install.tcl +++ b/modules/silicon_design/scripts/build/images/provision/install.tcl @@ -22,6 +22,3 @@ set ::env(RUN_CVC) 0 set ::env(RUN_KLAYOUT_XOR) 0 set ::env(RUN_KLAYOUT_DRC) 0 set ::env(RUN_KLAYOUT) 0 -# https://github.com/The-OpenROAD-Project/OpenLane/issues/1195 -set ::env(DIODE_INSERTION_STRATEGY) 0 -set ::env(USE_ARC_ANTENNA_CHECK) 0 diff --git a/modules/silicon_design/scripts/build/images/provision/papermill-launcher b/modules/silicon_design/scripts/build/images/provision/papermill-launcher new file mode 100755 index 00000000..c77879a1 --- /dev/null +++ b/modules/silicon_design/scripts/build/images/provision/papermill-launcher @@ -0,0 +1,28 @@ +#!/usr/bin/env python +import os.path +import sys + +import papermill as pm + +def args(param_args): + while len(param_args): + arg = param_args.pop(0).replace('--', '') + if '=' in arg: + yield arg.split('=') + else: + val = params_args.pop(0) + yield arg, val + +def expand_path(p): + return os.path.normpath( + os.path.expandvars(p) + ).replace('gs:/', 'gs://') + +_, input_path, output_path, *params_args = sys.argv +input_path = expand_path(input_path) +output_path = expand_path(output_path) +parameters = dict( + (k, expand_path(v)) + for k, v in args(params_args) +) +pm.execute_notebook(input_path, output_path, parameters=parameters, progress_bar=False) diff --git a/modules/silicon_design/scripts/build/notebooks/inverter.md b/modules/silicon_design/scripts/build/notebooks/inverter.md index fe3f2aea..5689a6f7 100644 --- a/modules/silicon_design/scripts/build/notebooks/inverter.md +++ b/modules/silicon_design/scripts/build/notebooks/inverter.md @@ -5,13 +5,14 @@ jupyter: extension: .md format_name: markdown format_version: '1.3' - jupytext_version: 1.13.6 + jupytext_version: 1.13.8 kernelspec: display_name: Python 3 (ipykernel) language: python name: python3 --- + # Inverter Sample ``` @@ -20,31 +21,39 @@ SPDX-License-Identifier: Apache-2.0 ``` This notebook shows how to run a simple inverter design thru an end-to-end RTL to GDSII flow targetting the [SKY130](https://github.com/google/skywater-pdk/) process node. + + +## Define flow parameters + +```python tags=["parameters"] +die_width = 45 +target_density = 90 +run_path = 'runs' +``` ## Write verilog Invert the `in` input signal and continuously assign it to the `out` output signal. -```python -%%bash -c 'cat > inverter.v; iverilog inverter.v' +```bash magic_args="-c 'cat > inverter.v; iverilog inverter.v'" module inverter(input wire in, output wire out); assign out = !in; endmodule ``` -## Write OpenLane configuration +## Write flow configuration See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. ```python -%%bash -c 'cat > config.tcl; tclsh config.tcl' +<<<<<<< HEAD %%bash -c 'cat > config.tcl; tclsh config.tcl' set ::env(DESIGN_NAME) inverter set ::env(VERILOG_FILES) "inverter.v" -set ::env(FP_SIZING) absolute -set ::env(DIE_AREA) "0 0 50 50" -set ::env(PL_TARGET_DENSITY) 0.75 +set ::env(FP_SIZING) "absolute" +set ::env(DIE_AREA) "0 0 $::env(DIE_WIDTH) $::env(DIE_WIDTH)" +set ::env(PL_TARGET_DENSITY) [expr {$::env(TARGET_DENSITY) / 100.0}] set ::env(CLOCK_TREE_SYNTH) 0 set ::env(CLOCK_PORT) "" @@ -53,24 +62,94 @@ set ::env(DIODE_INSERTION_STRATEGY) 0 ## Run OpenLane flow -[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), [Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. +[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. ```python tags=[] -!flow.tcl -design . +#papermill_description=RunningOpenLaneFlow +%env DIE_WIDTH={die_width} +%env TARGET_DENSITY={target_density} +!flow.tcl -design . -run_path {run_path} ``` -## Display layout with GDSII Tool Kit +## Display layout -[Gdstk](https://github.com/heitzmann/gdstk) (GDSII Tool Kit) is a C++/Python library for creation and manipulation of GDSII and OASIS files. +Use [GDSII Tool Kit](https://github.com/heitzmann/gdstk) to convert the resulting GDSII file to SVG. ```python +#papermill_description=RenderingGDS import pathlib import gdstk -from IPython.display import SVG +import IPython.display +import scrapbook as sb -gds = sorted(pathlib.Path('runs').glob('*/results/final/gds/*.gds'))[0] -library = gdstk.read_gds(gds) +gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] +library = gdstk.read_gds(gds_path) top_cells = library.top_level() -top_cells[0].write_svg('inverter.svg') -SVG('inverter.svg') +svg_path = pathlib.Path(run_path) / 'inverter.svg' +top_cells[0].write_svg(svg_path) +sb.glue('layout', IPython.display.SVG(svg_path), 'display', display=True) +``` + +## Dump flow report + +See [OpenLane Datapoint Definitions](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/regression_results/datapoint_definitions.md) for the description of the report columns. + +```python tags=[] +#papermill_description=DumpingReport +import pandas as pd +import pathlib +import scrapbook as sb + +final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/final_summary_report.csv'))[-1] +df = pd.read_csv(final_summary_report) +pd.set_option('display.max_rows', None) +sb.glue('summary', df, 'pandas') +df.transpose() +``` + +## Extract power metrics + +Build a pandas dataframe with area, density and power consumption. + +```python tags=[] +#papermill_description=ExtractingMetrics +import scrapbook as sb + +def get_power(sta_power_report): + with sta_power_report.open() as f: + for l in f.readlines(): + if l.startswith('Total'): + return float(l.split(' ')[-2]) + +def area_density_ppa(): + for report in sorted(pathlib.Path(run_path).glob('*/reports')): + sta_power_report = report / 'routing/23-parasitics_sta.power.rpt' + final_summary_report = report / 'final_summary_report.csv' + if final_summary_report.exists() and sta_power_report.exists(): + df = pd.read_csv(final_summary_report) + power = get_power(sta_power_report) + yield (df['DIEAREA_mm^2'][0], df['PL_TARGET_DENSITY'][0], power) + +df = pd.DataFrame(area_density_ppa(), columns=('DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER')) +sb.glue('metrics', df, 'pandas') +(df.style.hide_index() + .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) + .bar(subset=['TOTAL_POWER'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) +``` + +Report metrics for hyper-parameters tuning. + +```python +#papermill_description=ReportingMetrics +import hypertune + +total_power = df['TOTAL_POWER'][0] * 1e6 +print('reporting metric:', 'total_power', total_power) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='total_power', + metric_value=total_power, +) ``` diff --git a/modules/silicon_design/scripts/build/notebooks/openram.md b/modules/silicon_design/scripts/build/notebooks/openram.md new file mode 100644 index 00000000..71ba843d --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/openram.md @@ -0,0 +1,113 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# OpenRAM SKY130 playground + +Generate OpenRAM macros with `open_pdks.sky130a`. + + + +## Install dependencies + +Using conda packages from https://github.com/hdl/conda-eda. + + +```python colab={"base_uri": "https://localhost:8080/"} id="8zaG4mCd4-Ti" outputId="33408422-77f4-4d7b-cc28-086f699bdb87" +!pip install -q condacolab +import condacolab +condacolab.install() +``` + +```python colab={"base_uri": "https://localhost:8080/"} id="twnZMX905E-U" outputId="cc15b371-edb5-4d09-c4cb-26c41c551031" +import condacolab +condacolab.check() +``` + +```python colab={"base_uri": "https://localhost:8080/"} id="emiyv2qr6SnS" outputId="544c3046-62e1-4e4b-f0f8-06a848a3bbcd" +!conda install -c LiteX-Hub -y open_pdks.sky130a magic +!conda install -y gdstk cairosvg +``` + +```python colab={"base_uri": "https://localhost:8080/"} id="Rh8PYcraHuXI" outputId="e2e5ea0a-2403-42e2-8c7b-84acc267b3da" +!conda install https://anaconda.org/LiteX-Hub/netgen/1.5.219_0_ge11dbac/download/linux-64/netgen-1.5.219_0_ge11dbac-20220222_104027.tar.bz2 +``` + + +## Get OpenRAM + +Get latest release and install requirements from PyPI. + + +```python colab={"base_uri": "https://localhost:8080/"} id="jesuQ3pG5NmR" outputId="d219bfb3-b588-4890-dee9-618bcd3be50c" +!git clone -b v1.1.19 https://github.com/VLSIDA/OpenRAM.git +!python -m pip install -r OpenRAM/requirements.txt +``` + +```python colab={"base_uri": "https://localhost:8080/"} id="uNqPoSUB5fOS" outputId="64302d22-4a96-47cf-bee5-763f55cc082b" +%%writefile config.py +""" +Pseudo-dual port (independent read and write ports), 8bit word, 1 kbyte SRAM. +Useful as a byte FIFO between two devices (the reader and the writer). +""" +word_size = 8 # Bits +num_words = 1024 +human_byte_size = "{:.0f}kbytes".format((word_size * num_words)/1024/8) + +# Allow byte writes +#write_size = 8 # Bits + +# Dual port +num_rw_ports = 0 +num_r_ports = 1 +num_w_ports = 1 +ports_human = '1r1w' + +tech_name = "sky130" +nominal_corner_only = True + +# Local wordlines have issues with met3 power routing for now +#local_array_size = 16 + +route_supplies = "ring" +#route_supplies = "left" +check_lvsdrc = True +uniquify = True +#perimeter_pins = False +#netlist_only = True +#analytical_delay = False + +output_name = "sky130_sram_1kbyte_1r1w_8x1024_8" +output_path = "." +``` + +```python colab={"base_uri": "https://localhost:8080/"} id="RT6Zj3BE5nGS" outputId="0626d1d5-e8e0-4786-e4c0-304882b470fa" +%env OPENRAM_HOME=/content/OpenRAM/compiler +%env OPENRAM_TECH=/content/OpenRAM/technology/sky130 +%env PDK_ROOT=/usr/local/share/pdk +%env PYTHONPATH=/env/python:/content/OpenRAM/compiler:/content/OpenRAM/technology:/content/OpenRAM/technology/sky130/modules +!make -C OpenRAM SRAM_GIT_REPO=https://github.com/google/skywater-pdk-libs-sky130_fd_bd_sram.git +!python $OPENRAM_HOME/openram.py config.py +``` + +```python colab={"base_uri": "https://localhost:8080/", "height": 1000} id="NUSqt4xDL4Iu" outputId="f5cf3b6d-3e64-423d-f83e-7a000b57ec63" +import gdstk +library = gdstk.read_gds("sky130_sram_1kbyte_1r1w_8x1024_8.gds") +top_cells = library.top_level() +top_cells[0].write_svg('sky130_sram_1kbyte_1r1w_8x1024_8.svg') +import cairosvg +cairosvg.svg2png(url='sky130_sram_1kbyte_1r1w_8x1024_8.svg', write_to='sky130_sram_1kbyte_1r1w_8x1024_8.png') +from IPython.display import Image +Image('sky130_sram_1kbyte_1r1w_8x1024_8.png') +``` diff --git a/modules/silicon_design/scripts/build/notebooks/serv.md b/modules/silicon_design/scripts/build/notebooks/serv.md new file mode 100644 index 00000000..3f79ff93 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/serv.md @@ -0,0 +1,154 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Serv Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to run the [SERV](https://github.com/olofk/serv) RISC-V core design thru an end-to-end RTL to GDSII flow targetting the [SKY130](https://github.com/google/skywater-pdk/) process node. + + +## Define flow parameters + +```python tags=["parameters"] +die_width = 300 +target_density = 80 +run_path = 'runs/serv' +``` + +## Get SERV RTL + +```python +!git clone https://github.com/olofk/serv +``` +## Write flow configuration + +See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. + +```python +%%writefile config.tcl +set ::env(DESIGN_NAME) serv_top + +set script_dir [file dirname [file normalize [info script]]] +set ::env(VERILOG_FILES) "$script_dir/serv/rtl/*.v" + +set ::env(CLOCK_PORT) "click" + +set ::env(FP_SIZING) "absolute" +set ::env(DIE_AREA) "0 0 $::env(DIE_WIDTH) $::env(DIE_WIDTH)" +set ::env(PL_TARGET_DENSITY) [expr {$::env(TARGET_DENSITY) / 100.0}] +``` + +## Run OpenLane flow + +[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. + +```python tags=[] +#papermill_description=RunningOpenLaneFlow +%env DIE_WIDTH={die_width} +%env TARGET_DENSITY={target_density} +!flow.tcl -design . -run_path {run_path} +``` + +## Display layout + +Use [GDSII Tool Kit](https://github.com/heitzmann/gdstk) and [CairoSVG](https://cairosvg.org/) to convert the resulting GDSII file to PNG. + +```python +#papermill_description=RenderingGDS +import pathlib +import gdstk +import cairosvg + +import IPython.display +import scrapbook as sb + +gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] +library = gdstk.read_gds(gds_path) +top_cells = library.top_level() +svg_path = pathlib.Path(run_path) / 'serv.svg' +top_cells[0].write_svg(svg_path) +png_path = pathlib.Path(run_path) / 'serv.png' + +cairosvg.svg2png(url=str(svg_path), write_to=str(png_path)) +sb.glue('layout', IPython.display.Image(png_path), 'display', display=True) +``` + +## Dump flow report + +See [OpenLane Datapoint Definitions](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/regression_results/datapoint_definitions.md) for the description of the report columns. + +```python tags=[] +#papermill_description=DumpingReport +import pandas as pd +import pathlib +import scrapbook as sb + +final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/final_summary_report.csv'))[-1] +df = pd.read_csv(final_summary_report) +pd.set_option('display.max_rows', None) +sb.glue('summary', df, 'pandas') +df.transpose() +``` + +## Extract power metrics + +Build a pandas dataframe with area, density and power consumption. + +```python tags=[] +#papermill_description=ExtractingMetrics +import scrapbook as sb + +def get_power(sta_power_report): + with sta_power_report.open() as f: + for l in f.readlines(): + if l.startswith('Total'): + return float(l.split(' ')[-2]) + +def area_density_ppa(): + for report in sorted(pathlib.Path(run_path).glob('*/reports')): + sta_power_report = report / 'routing/23-parasitics_sta.power.rpt' + final_summary_report = report / 'final_summary_report.csv' + if final_summary_report.exists() and sta_power_report.exists(): + df = pd.read_csv(final_summary_report) + power = get_power(sta_power_report) + yield (df['DIEAREA_mm^2'][0], df['PL_TARGET_DENSITY'][0], power) + +df = pd.DataFrame(area_density_ppa(), columns=('DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER')) +sb.glue('metrics', df, 'pandas') +(df.style.hide_index() + .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) + .bar(subset=['TOTAL_POWER'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) +``` + +Report metrics for hyper-parameters tuning. + +```python +#papermill_description=ReportingMetrics +import hypertune + +total_power = df['TOTAL_POWER'][0] * 1e6 +print('reporting metric:', 'total_power', total_power) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='total_power', + metric_value=total_power, +) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/tuning.md new file mode 100644 index 00000000..5550912c --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/tuning.md @@ -0,0 +1,96 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Parameter Tuning Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to leverage [Vertex AI hyperparameter tuning](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) in order to find the right flow parameters value to optimize a given metric. + + +## Define project parameters + +```python tags=["parameters"] +worker_image = 'us-central1-docker.pkg.dev/catx-demo-radlab/containers/silicon-design-ubuntu-2004:latest' +staging_bucket = 'gs://catx-demo-radlab-staging' +``` + +## Stage the notebook for the experiment + +```python tags=[] +!gsutil mb {staging_bucket} +!gsutil cp inverter.ipynb {staging_bucket}/inverter.ipynb +``` + +## Create Parameters and Metrics specs + +We want to find the best value for *target density* and *die area* in order optimize *total power* consumption. + +Those keys map to the [parameters](https://papermill.readthedocs.io/en/latest/usage-parameterize.html) and [metrics](https://github.com/GoogleCloudPlatform/cloudml-hypertune) advertised by the notebook. + +```python tags=[] +from google.cloud.aiplatform import hyperparameter_tuning as hpt + +parameter_spec = { + 'target_density': hpt.DoubleParameterSpec(min=10, max=100, scale='log'), + 'die_width': hpt.DoubleParameterSpec(min=10, max=300, scale='linear'), +} + +metric_spec={'total_power': 'minimize'} +``` + +## Create Custom Job spec + +```python tags=[] +from google.cloud import aiplatform + +worker_pool_specs = [{ + 'machine_spec': { + 'machine_type': 'n1-standard-4', + }, + 'replica_count': 1, + 'container_spec': { + 'image_uri': worker_image, + 'args': ['/usr/local/bin/papermill-launcher', + f'{staging_bucket}/inverter8.ipynb', + '$AIP_MODEL_DIR/inverter_out.ipynb', + '--run_dir=/tmp'] + } +}] + +custom_job = aiplatform.CustomJob(display_name='inverter-flow-job', + worker_pool_specs=worker_pool_specs, + staging_bucket=staging_bucket) +``` + +## Run Hyperparameter tuning job + +```python tags=[] +from google.cloud import aiplatform + +hpt_job = aiplatform.HyperparameterTuningJob( + display_name='inverter-tuning-job', + custom_job=custom_job, + metric_spec=metric_spec, + parameter_spec=parameter_spec, + max_trial_count=200, + parallel_trial_count=10, + max_failed_trial_count=200) + +hpt_job.run() +``` diff --git a/modules/silicon_design/scripts/build/notebooks/xls.md b/modules/silicon_design/scripts/build/notebooks/xls.md new file mode 100644 index 00000000..312bf4e4 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/xls.md @@ -0,0 +1,195 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# XLS Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to run a [XLS](https://google.github.io/xls/)-based CRC checksum calculator design thru an end-to-end RTL to GDSII flow targetting the [SKY130](https://github.com/google/skywater-pdk/) process node. + + + +## Define flow parameters + + +```python tags=["parameters"] +die_width = 100 +target_density = 80 +run_path = 'runs/crc32' +``` + + +## Write and test DSLX module + +The CRC computation is written using the [DSLX](https://google.github.io/xls/dslx_reference/) HLS, a domain specific, dataflow-oriented functional language used to build hardware w/ a Rust-like syntax. + + +```bash colab={"base_uri": "https://localhost:8080/"} id="JKGxScUtoV4E" outputId="b9359a05-fa7f-4366-ecf8-40138acb11f1" magic_args="-c 'cat > crc32.x; interpreter_main crc32.x'" +// Performs a table-less crc32 of the input data as in Hacker's Delight: +// https://github.com/hcs0/Hackers-Delight/blob/master/crc.c.txt (roughly flavor b) + +fn crc32_one_byte(byte: u8, polynomial: u32, crc: u32) -> u32 { + let crc = crc ^ (byte as u32); + // 8 rounds of updates. + for (i, crc): (u32, u32) in range(u32:0, u32:8) { + let mask = -(crc & u32:1); + (crc >> u32:1) ^ (polynomial & mask) + }(crc) +} + +fn main(message: u8) -> u32 { + crc32_one_byte(message, u32:0xEDB88320, u32:-1) ^ u32:-1 +} + +#![test] +fn crc32_one_char() { + assert_eq(u32:0x83DCEFB7, main('1')) +} +``` + + +## Generate IR and Verilog + +XLS can generate combinational or pipelined version of a given design. + + +```python colab={"base_uri": "https://localhost:8080/"} id="YMTh7WB6oxeW" outputId="a4e9d2f2-69e3-47e9-cad6-e1b89124553b" +!ir_converter_main --top main crc32.x > crc32.ir +!opt_main crc32.ir > crc32_opt.ir +!codegen_main --generator=combinational crc32_opt.ir > crc32.v +!cat crc32.v +``` + +## Write flow configuration + +See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. + +```python id="rBk7BdF0n_o5" +%%writefile config.tcl +set ::env(DESIGN_NAME) __crc32__main + +set script_dir [file dirname [file normalize [info script]]] +set ::env(VERILOG_FILES) "$script_dir/crc32.v" + +set ::env(CLOCK_TREE_SYNTH) 0 +set ::env(CLOCK_PORT) "" + +set ::env(FP_SIZING) "absolute" +set ::env(DIE_AREA) "0 0 $::env(DIE_WIDTH) $::env(DIE_WIDTH)" +set ::env(PL_TARGET_DENSITY) [expr {$::env(TARGET_DENSITY) / 100.0}] + +# TODO(proppy) find out why LVS fails +set ::env(RUN_LVS) 0 +``` + +## Run OpenLane flow + +[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. + +```python colab={"base_uri": "https://localhost:8080/"} id="8gim7pEdozHv" outputId="3d4cccd8-bda2-4380-a1c3-5c9002560b7b" tags=[] +#papermill_description=RunningOpenLaneFlow +%env DIE_WIDTH={die_width} +%env TARGET_DENSITY={target_density} +!flow.tcl -design . -run_path {run_path} +``` + +## Display layout + +Use [GDSII Tool Kit](https://github.com/heitzmann/gdstk) and [CairoSVG](https://cairosvg.org/) to convert the resulting GDSII file to PNG. + +```python colab={"base_uri": "https://localhost:8080/", "height": 1000} id="1uSEdmRhtXdl" outputId="6830cf44-e85f-48fc-aa84-84f794c25dc8" +#papermill_description=RenderingGDS +import pathlib +import gdstk +import cairosvg + +import IPython.display +import scrapbook as sb + +gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] +library = gdstk.read_gds(gds_path) +top_cells = library.top_level() +svg_path = pathlib.Path(run_path) / 'xls.svg' +top_cells[0].write_svg(svg_path) +png_path = pathlib.Path(run_path) / 'xls.png' + +cairosvg.svg2png(url=str(svg_path), write_to=str(png_path)) +sb.glue('layout', IPython.display.Image(png_path), 'display', display=True) +``` + +## Dump flow report + +See [OpenLane Datapoint Definitions](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/regression_results/datapoint_definitions.md) for the description of the report columns. + +```python tags=[] +#papermill_description=DumpingReport +import pandas as pd +import pathlib +import scrapbook as sb + +final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/final_summary_report.csv'))[-1] +df = pd.read_csv(final_summary_report) +pd.set_option('display.max_rows', None) +sb.glue('summary', df, 'pandas') +df.transpose() +``` + +## Extract power metrics + +Build a pandas dataframe with area, density and power consumption. + +```python tags=[] +#papermill_description=ExtractingMetrics +import scrapbook as sb + +def get_power(sta_power_report): + with sta_power_report.open() as f: + for l in f.readlines(): + if l.startswith('Total'): + return float(l.split(' ')[-2]) + +def area_density_ppa(): + for report in sorted(pathlib.Path(run_path).glob('*/reports')): + sta_power_report = report / 'routing/23-parasitics_sta.power.rpt' + final_summary_report = report / 'final_summary_report.csv' + if final_summary_report.exists() and sta_power_report.exists(): + df = pd.read_csv(final_summary_report) + power = get_power(sta_power_report) + yield (df['DIEAREA_mm^2'][0], df['PL_TARGET_DENSITY'][0], power) + +df = pd.DataFrame(area_density_ppa(), columns=('DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER')) +sb.glue('metrics', df, 'pandas') +(df.style.hide_index() + .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) + .bar(subset=['TOTAL_POWER'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) +``` + +```python +#papermill_description=ReportingMetrics +import hypertune + +total_power = df['TOTAL_POWER'][0] * 1e6 +print('reporting metric:', 'total_power', total_power) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='total_power', + metric_value=total_power, +) +``` diff --git a/modules/silicon_design/variables.tf b/modules/silicon_design/variables.tf index 833b948a..900f991d 100644 --- a/modules/silicon_design/variables.tf +++ b/modules/silicon_design/variables.tf @@ -145,7 +145,13 @@ variable "zone" { } variable "image_name" { - description = "Basename for for the compute and container image." + description = "Basename for the compute and container image." type = string default = "silicon-design-ubuntu-2004" } + +variable "image_tag" { + description = "Tag for the compute and container image." + type = string + default = "" +} From 280a1472138076aab6fda575632b70896cb95f39 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 13 Apr 2022 11:07:29 +0900 Subject: [PATCH 57/93] modules/silicon_design/notebooks/tuning: add plots --- .../scripts/build/notebooks/tuning.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/tuning.md index 5550912c..2ee55b67 100644 --- a/modules/silicon_design/scripts/build/notebooks/tuning.md +++ b/modules/silicon_design/scripts/build/notebooks/tuning.md @@ -28,6 +28,8 @@ This notebook shows how to leverage [Vertex AI hyperparameter tuning](https://cl ```python tags=["parameters"] worker_image = 'us-central1-docker.pkg.dev/catx-demo-radlab/containers/silicon-design-ubuntu-2004:latest' staging_bucket = 'gs://catx-demo-radlab-staging' +project = 'catx-demo-radlab' +location = 'us-central1' ``` ## Stage the notebook for the experiment @@ -94,3 +96,116 @@ hpt_job = aiplatform.HyperparameterTuningJob( hpt_job.run() ``` + +```python +import scrapbook as sb +from google.cloud import storage +import tqdm + +client = storage.Client() +staging_bucket = client.bucket('catx-demo-radlab-staging') +results_bucket = client.bucket('catx-demo-radlab-results') +for i in tqdm.tqdm(range(1, 501)): + src = staging_bucket.blob(f'aiplatform-custom-job-2022-04-07-16:02:35.153/{i}/model/serv_out.ipynb') + try: + blob = staging_bucket.copy_blob( + staging_bucket.blob(f'aiplatform-custom-job-2022-04-07-16:02:35.153/{i}/model/serv_out.ipynb'), + results_bucket, + f'aiplatform-custom-job-2022-04-07-16:02:35.153/serv_out_{i}.ipynb' + ) + except: + pass +``` + +```python tags=[] +import scrapbook as sb +books = sb.read_notebooks('gs://aiplatform-custom-job-2022-04-07-16:02:35.153/') +``` + +```python +import pandas as pd +import tqdm + +def metrics(): + for b in tqdm.tqdm(books): + if 'metrics' in books[b].scraps: + yield books[b].scraps['metrics'].data + +df = pd.concat(metrics(), ignore_index=True) +(df.sort_values(['TOTAL_POWER']).style + .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) + .bar(subset=['TOTAL_POWER'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) +``` + +```python +df.plot.scatter(x='DIEAREA_mm^2', y='PL_TARGET_DENSITY', c='TOTAL_POWER', + cmap='cool', s=200, sharex=False) +``` + +```python +from matplotlib import pyplot as plt +from matplotlib import animation +from tqdm import tqdm +from IPython.display import Image +from time import sleep +import matplotlib.colors + +min_total_power = df['TOTAL_POWER'].min() +max_total_power = df['TOTAL_POWER'].max() +fig, ax = plt.subplots() +fig.colorbar(cm.ScalarMappable(matplotlib.colors.Normalize(min_total_power, max_total_power), cmap='cool'), + label='TOTAL_POWER', + ax=ax) +ax.set_xlabel('DIEAREA_mm^2') +ax.set_xlabel('PL_TARGET_DENSITY') + +def generate_frames(): + for n in range(10, 500, 10): + batch = df[0:n] + yield [ax.scatter( + batch['DIEAREA_mm^2'], batch['PL_TARGET_DENSITY'], c=batch['TOTAL_POWER'], + s=50, vmin=min_total_power, vmax=max_total_power, cmap='cool')] +frames = list(generate_frames()) +anim = animation.ArtistAnimation(fig, frames) +anim.save('serv.gif', writer=animation.PillowWriter(fps=10)) +Image('serv.gif') +``` + +```python +from tqdm import tqdm +import io +import base64 +import PIL + +fig, axs = plt.subplots(25, 20, figsize=(100, 100)) +axs = axs.flatten() + +min_total_power = df['TOTAL_POWER'].min() +max_total_power = df['TOTAL_POWER'].max() + +def images_with_power(n): + for i, b in enumerate(books): + book = books[b] + if 'layout' in book.scraps: + metrics = book.scraps['metrics'] + layout = book.scraps['layout'] + f = io.BytesIO(base64.b64decode(layout.display.data['image/png'])) + img = PIL.Image.open(f).convert('L') + total_power = metrics.data['TOTAL_POWER'][0] + power = (total_power - min_total_power) / (max_total_power - min_total_power) + yield img, power + else: + yield PIL.Image.new('RGBA', (100, 100)).convert('L'), 0 + if i == n-1: + break + +cool = cm.get_cmap('cool') + +for i, (img, power) in tqdm(enumerate(images_with_power(500))): + color = np.array(cool(power)) * 255 + axs[i].imshow(PIL.ImageOps.colorize(img, (0, 0, 0, 255), color)) +fig.savefig('ALLTHESERVs.png') +fig +``` From ac92887410187360096350cf2571da578fbfc312 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 13 Apr 2022 11:15:46 +0900 Subject: [PATCH 58/93] modules/silicon_design/notebooks/tuning: add headings --- modules/silicon_design/scripts/build/notebooks/tuning.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/tuning.md index 2ee55b67..50272d95 100644 --- a/modules/silicon_design/scripts/build/notebooks/tuning.md +++ b/modules/silicon_design/scripts/build/notebooks/tuning.md @@ -97,6 +97,7 @@ hpt_job = aiplatform.HyperparameterTuningJob( hpt_job.run() ``` +## Extract experiment notebooks ```python import scrapbook as sb from google.cloud import storage @@ -113,8 +114,8 @@ for i in tqdm.tqdm(range(1, 501)): results_bucket, f'aiplatform-custom-job-2022-04-07-16:02:35.153/serv_out_{i}.ipynb' ) - except: - pass + except Exception as e: + print(f'error extracting experiment {i}:', e) ``` ```python tags=[] @@ -122,6 +123,7 @@ import scrapbook as sb books = sb.read_notebooks('gs://aiplatform-custom-job-2022-04-07-16:02:35.153/') ``` +## Aggregate experiments results ```python import pandas as pd import tqdm @@ -139,11 +141,13 @@ df = pd.concat(metrics(), ignore_index=True) .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) ``` +## Plot power against area/density ```python df.plot.scatter(x='DIEAREA_mm^2', y='PL_TARGET_DENSITY', c='TOTAL_POWER', cmap='cool', s=200, sharex=False) ``` +## Visualize experiments chronologically ```python from matplotlib import pyplot as plt from matplotlib import animation @@ -173,6 +177,7 @@ anim.save('serv.gif', writer=animation.PillowWriter(fps=10)) Image('serv.gif') ``` +## Map all the generate layouts ```python from tqdm import tqdm import io From a7bd05551687df8486d710d5de2dbec304414418 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Thu, 12 May 2022 19:16:32 +0900 Subject: [PATCH 59/93] modules/silicon_design/notebooks/tuning: fix experiments ordering --- .../scripts/build/notebooks/tuning.md | 149 +++++++++++++----- 1 file changed, 107 insertions(+), 42 deletions(-) diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/tuning.md index 50272d95..bd1c8c5c 100644 --- a/modules/silicon_design/scripts/build/notebooks/tuning.md +++ b/modules/silicon_design/scripts/build/notebooks/tuning.md @@ -36,7 +36,7 @@ location = 'us-central1' ```python tags=[] !gsutil mb {staging_bucket} -!gsutil cp inverter.ipynb {staging_bucket}/inverter.ipynb +!gsutil cp serv.ipynb {staging_bucket}/ ``` ## Create Parameters and Metrics specs @@ -69,17 +69,49 @@ worker_pool_specs = [{ 'container_spec': { 'image_uri': worker_image, 'args': ['/usr/local/bin/papermill-launcher', - f'{staging_bucket}/inverter8.ipynb', - '$AIP_MODEL_DIR/inverter_out.ipynb', + f'{staging_bucket}/serv.ipynb', + '$AIP_MODEL_DIR/serv_out.ipynb', '--run_dir=/tmp'] } }] -custom_job = aiplatform.CustomJob(display_name='inverter-flow-job', +custom_job = aiplatform.CustomJob(display_name='serv-flow-job', worker_pool_specs=worker_pool_specs, staging_bucket=staging_bucket) ``` +```python +# %load /usr/local/bin/papermill-launcher +#!/usr/bin/env python +import os.path +import sys + +import papermill as pm + +def args(param_args): + while len(param_args): + arg = param_args.pop(0).replace('--', '') + if '=' in arg: + yield arg.split('=') + else: + val = params_args.pop(0) + yield arg, val + +def expand_path(p): + return os.path.normpath( + os.path.expandvars(p) + ).replace('gs:/', 'gs://') + +_, input_path, output_path, *params_args = sys.argv +input_path = expand_path(input_path) +output_path = expand_path(output_path) +parameters = dict( + (k, expand_path(v)) + for k, v in args(params_args) +) +pm.execute_notebook(input_path, output_path, parameters=parameters, progress_bar=False) +``` + ## Run Hyperparameter tuning job ```python tags=[] @@ -97,120 +129,153 @@ hpt_job = aiplatform.HyperparameterTuningJob( hpt_job.run() ``` -## Extract experiment notebooks +## Fetch notebooks for all study trials + ```python -import scrapbook as sb +import pathlib + from google.cloud import storage import tqdm +dst_dir = pathlib.Path('results/') +dst_dir.mkdir(exist_ok=True, parents=True) + client = storage.Client() staging_bucket = client.bucket('catx-demo-radlab-staging') results_bucket = client.bucket('catx-demo-radlab-results') for i in tqdm.tqdm(range(1, 501)): src = staging_bucket.blob(f'aiplatform-custom-job-2022-04-07-16:02:35.153/{i}/model/serv_out.ipynb') - try: - blob = staging_bucket.copy_blob( - staging_bucket.blob(f'aiplatform-custom-job-2022-04-07-16:02:35.153/{i}/model/serv_out.ipynb'), - results_bucket, - f'aiplatform-custom-job-2022-04-07-16:02:35.153/serv_out_{i}.ipynb' - ) - except Exception as e: - print(f'error extracting experiment {i}:', e) + dst = dst_dir / f'serv_out_{i}.ipynb' + with dst.open('wb') as f: + src.download_to_file(f) ``` +## Extract metrics from notebooks + ```python tags=[] import scrapbook as sb -books = sb.read_notebooks('gs://aiplatform-custom-job-2022-04-07-16:02:35.153/') +books = sb.read_notebooks('results/') ``` -## Aggregate experiments results ```python +import pathlib +import math + import pandas as pd import tqdm - def metrics(): for b in tqdm.tqdm(books): + trial_id = int(pathlib.Path(books[b].filename).stem.split('_')[-1]) if 'metrics' in books[b].scraps: - yield books[b].scraps['metrics'].data + metrics = books[b].scraps['metrics'].data + yield trial_id, metrics['DIEAREA_mm^2'][0], metrics['PL_TARGET_DENSITY'][0], metrics['TOTAL_POWER'][0] + else: + params = books[b].parameters + die_width_mm = float(params.die_width) / 1000.0 + target_density = float(params.target_density) / 100.0 + yield trial_id, die_width_mm * die_width_mm, target_density, math.nan -df = pd.concat(metrics(), ignore_index=True) -(df.sort_values(['TOTAL_POWER']).style - .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) +df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER'], index='TRIAL_ID').sort_index() +(df.sort_values(['TOTAL_POWER', 'DIEAREA_mm^2'], ascending=False).style + .format({'DIEAREA_mm^2': '{:.8f}', 'PL_TARGET_DENSITY': '{:.2%}', 'TOTAL_POWER': '{:.6f}'}) .bar(subset=['TOTAL_POWER'], color='pink') .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) ``` -## Plot power against area/density +## Plot experiments + ```python df.plot.scatter(x='DIEAREA_mm^2', y='PL_TARGET_DENSITY', c='TOTAL_POWER', cmap='cool', s=200, sharex=False) ``` -## Visualize experiments chronologically ```python from matplotlib import pyplot as plt from matplotlib import animation +from matplotlib import cm + from tqdm import tqdm from IPython.display import Image -from time import sleep import matplotlib.colors +cool = matplotlib.colormaps['cool'] +cool.set_bad(color='none') min_total_power = df['TOTAL_POWER'].min() max_total_power = df['TOTAL_POWER'].max() fig, ax = plt.subplots() -fig.colorbar(cm.ScalarMappable(matplotlib.colors.Normalize(min_total_power, max_total_power), cmap='cool'), +fig.colorbar(cm.ScalarMappable(matplotlib.colors.Normalize(min_total_power, max_total_power), cmap=cool), label='TOTAL_POWER', ax=ax) ax.set_xlabel('DIEAREA_mm^2') -ax.set_xlabel('PL_TARGET_DENSITY') +ax.set_ylabel('PL_TARGET_DENSITY') +plt.close(fig) # hide current figure def generate_frames(): for n in range(10, 500, 10): batch = df[0:n] yield [ax.scatter( batch['DIEAREA_mm^2'], batch['PL_TARGET_DENSITY'], c=batch['TOTAL_POWER'], - s=50, vmin=min_total_power, vmax=max_total_power, cmap='cool')] + s=50, vmin=min_total_power, vmax=max_total_power, cmap=cool, plotnonfinite=True, edgecolor='black')] + frames = list(generate_frames()) anim = animation.ArtistAnimation(fig, frames) anim.save('serv.gif', writer=animation.PillowWriter(fps=10)) Image('serv.gif') ``` -## Map all the generate layouts +## Render chip layouts + ```python +from matplotlib import pyplot as plt +from matplotlib import animation +from matplotlib import cm from tqdm import tqdm +from IPython.display import Image +from time import sleep +import matplotlib.colors import io import base64 import PIL +import PIL.ImageOps +import PIL.ImageDraw +import numpy as np -fig, axs = plt.subplots(25, 20, figsize=(100, 100)) -axs = axs.flatten() min_total_power = df['TOTAL_POWER'].min() max_total_power = df['TOTAL_POWER'].max() def images_with_power(n): - for i, b in enumerate(books): - book = books[b] + for trial_id, trial in df.iterrows(): + book = books[f'serv_out_{trial_id}'] if 'layout' in book.scraps: - metrics = book.scraps['metrics'] layout = book.scraps['layout'] f = io.BytesIO(base64.b64decode(layout.display.data['image/png'])) - img = PIL.Image.open(f).convert('L') - total_power = metrics.data['TOTAL_POWER'][0] + img = PIL.Image.open(f)#.convert('L') + total_power = trial['TOTAL_POWER'] power = (total_power - min_total_power) / (max_total_power - min_total_power) - yield img, power + yield trial_id, img, power else: - yield PIL.Image.new('RGBA', (100, 100)).convert('L'), 0 + yield trial_id, PIL.Image.new('RGBA', (100, 100)).convert('L'), 0 if i == n-1: break +size = (500, 500) +fig, ax = plt.subplots(figsize=size) cool = cm.get_cmap('cool') -for i, (img, power) in tqdm(enumerate(images_with_power(500))): - color = np.array(cool(power)) * 255 - axs[i].imshow(PIL.ImageOps.colorize(img, (0, 0, 0, 255), color)) -fig.savefig('ALLTHESERVs.png') -fig +def generate_frames(): + for trial_id, img, power in tqdm(images_with_power(500)): + extrema = img.getextrema() + if extrema != (0,0): + #color = np.array(cool(power)) * 255 + #img = PIL.ImageOps.colorize(img, (0, 0, 0, 255), color) + img = img.resize(size) + d = PIL.ImageDraw.Draw(img) + d.text((10, 10), f'SERV_{trial_id}', fill=(255, 255, 255, 255)) + yield img + +frames = list(generate_frames()) +frames[0].save('ALLTHESERVS_RAW.gif', save_all=True, loop=0, append_images=frames[1:]) +Image('ALLTHESERVS_RAW.gif') ``` From 4614fe827d3a3fbecbedb76d85782d5551ff9a1f Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 13 May 2022 20:52:35 +0900 Subject: [PATCH 60/93] modules/silicon_design/notebooks: add subservient experiment --- .../scripts/build/notebooks/subservient.md | 160 ++++++++++++++++++ .../scripts/build/notebooks/tuning.md | 124 +++++--------- 2 files changed, 204 insertions(+), 80 deletions(-) create mode 100644 modules/silicon_design/scripts/build/notebooks/subservient.md diff --git a/modules/silicon_design/scripts/build/notebooks/subservient.md b/modules/silicon_design/scripts/build/notebooks/subservient.md new file mode 100644 index 00000000..ce9bef33 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/subservient.md @@ -0,0 +1,160 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Serv Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to run the [SERV](https://github.com/olofk/serv) RISC-V core design thru an end-to-end RTL to GDSII flow targetting the [SKY130](https://github.com/google/skywater-pdk/) process node. + + +## Define flow parameters + +```python tags=["parameters"] +die_width = 200 +target_density = 80 +run_path = 'runs/serv' +``` + +## Get SERV RTL + +```python +!git clone -b serial_dbg_if https://github.com/olofk/subservient +!git clone https://github.com/olofk/serv +``` +## Write flow configuration + +See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. + +```python +%%writefile config.tcl +set ::env(DESIGN_NAME) subservient + +set script_dir [file dirname [file normalize [info script]]] +set ::env(VERILOG_FILES) " + [glob "$script_dir/serv/rtl/*.v"] + [glob "$script_dir/serv/serving/*.v"] + [glob "$script_dir/subservient/rtl/*.v"] +" +set ::env(CLOCK_PERIOD) "10" +set ::env(CLOCK_PORT) "i_clk" +set ::env(DESIGN_IS_CORE) 0 + +set ::env(FP_SIZING) "absolute" +set ::env(DIE_AREA) "0 0 $::env(DIE_WIDTH) $::env(DIE_WIDTH)" +set ::env(PL_TARGET_DENSITY) [expr {$::env(TARGET_DENSITY) / 100.0}] +``` + +## Run OpenLane flow + +[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. + +```python tags=[] +#papermill_description=RunningOpenLaneFlow +%env DIE_WIDTH={die_width} +%env TARGET_DENSITY={target_density} +!flow.tcl -design . -run_path {run_path} -verbose 2 +``` + +## Display layout + +Use [GDSII Tool Kit](https://github.com/heitzmann/gdstk) and [CairoSVG](https://cairosvg.org/) to convert the resulting GDSII file to PNG. + +```python +#papermill_description=RenderingGDS +import pathlib +import gdstk +import cairosvg + +import IPython.display +import scrapbook as sb + +gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] +library = gdstk.read_gds(gds_path) +top_cells = library.top_level() +svg_path = gds_path.parent / 'subservient.svg' +top_cells[0].write_svg(svg_path) +png_path = gds_path.parent / 'subservient.png' + +cairosvg.svg2png(url=str(svg_path), write_to=str(png_path)) +sb.glue('layout', IPython.display.Image(png_path), 'display', display=True) +``` + +## Dump flow report + +See [OpenLane Datapoint Definitions](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/regression_results/datapoint_definitions.md) for the description of the report columns. + +```python tags=[] +#papermill_description=DumpingReport +import pandas as pd +import pathlib +import scrapbook as sb + +final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/final_summary_report.csv'))[-1] +df = pd.read_csv(final_summary_report) +pd.set_option('display.max_rows', None) +sb.glue('summary', df, 'pandas') +df.transpose() +``` + +## Extract power metrics + +Build a pandas dataframe with area, density and power consumption. + +```python tags=[] +#papermill_description=ExtractingMetrics +import scrapbook as sb + +def get_power(sta_power_report): + with sta_power_report.open() as f: + for l in f.readlines(): + if l.startswith('Total'): + return float(l.split(' ')[-2]) + +def area_density_ppa(): + for report in sorted(pathlib.Path(run_path).glob('*/reports')): + sta_power_report = report / 'routing/24-parasitics_sta.power.rpt' + final_summary_report = report / 'final_summary_report.csv' + if final_summary_report.exists() and sta_power_report.exists(): + df = pd.read_csv(final_summary_report) + power = get_power(sta_power_report) + yield (df['DIEAREA_mm^2'][0], df['PL_TARGET_DENSITY'][0], power) + +df = pd.DataFrame(area_density_ppa(), columns=('DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER')) +sb.glue('metrics', df, 'pandas') +(df.style.hide_index() + .format({'DIEAREA_mm^2': '{:.8f}', 'PL_TARGET_DENSITY': '{:.2%}', 'TOTAL_POWER': '{:.6f}'}) + .bar(subset=['TOTAL_POWER'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) +``` + +Report metrics for hyper-parameters tuning. + +```python +#papermill_description=ReportingMetrics +import hypertune + +total_power = df['TOTAL_POWER'][0] * 1e6 +print('reporting metric:', 'total_power', total_power) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='total_power', + metric_value=total_power, +) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/tuning.md index bd1c8c5c..23a616e2 100644 --- a/modules/silicon_design/scripts/build/notebooks/tuning.md +++ b/modules/silicon_design/scripts/build/notebooks/tuning.md @@ -36,7 +36,7 @@ location = 'us-central1' ```python tags=[] !gsutil mb {staging_bucket} -!gsutil cp serv.ipynb {staging_bucket}/ +!gsutil cp subservient.ipynb {staging_bucket}/ ``` ## Create Parameters and Metrics specs @@ -69,62 +69,30 @@ worker_pool_specs = [{ 'container_spec': { 'image_uri': worker_image, 'args': ['/usr/local/bin/papermill-launcher', - f'{staging_bucket}/serv.ipynb', - '$AIP_MODEL_DIR/serv_out.ipynb', + f'{staging_bucket}/subservient.ipynb', + '$AIP_MODEL_DIR/subservient_out.ipynb', '--run_dir=/tmp'] } }] -custom_job = aiplatform.CustomJob(display_name='serv-flow-job', +custom_job = aiplatform.CustomJob(display_name='subservient-flow-job', worker_pool_specs=worker_pool_specs, staging_bucket=staging_bucket) ``` -```python -# %load /usr/local/bin/papermill-launcher -#!/usr/bin/env python -import os.path -import sys - -import papermill as pm - -def args(param_args): - while len(param_args): - arg = param_args.pop(0).replace('--', '') - if '=' in arg: - yield arg.split('=') - else: - val = params_args.pop(0) - yield arg, val - -def expand_path(p): - return os.path.normpath( - os.path.expandvars(p) - ).replace('gs:/', 'gs://') - -_, input_path, output_path, *params_args = sys.argv -input_path = expand_path(input_path) -output_path = expand_path(output_path) -parameters = dict( - (k, expand_path(v)) - for k, v in args(params_args) -) -pm.execute_notebook(input_path, output_path, parameters=parameters, progress_bar=False) -``` - ## Run Hyperparameter tuning job ```python tags=[] from google.cloud import aiplatform hpt_job = aiplatform.HyperparameterTuningJob( - display_name='inverter-tuning-job', + display_name='subservient-tuning-job', custom_job=custom_job, metric_spec=metric_spec, parameter_spec=parameter_spec, - max_trial_count=200, - parallel_trial_count=10, - max_failed_trial_count=200) + max_trial_count=10000, + parallel_trial_count=50, + max_failed_trial_count=10000) hpt_job.run() ``` @@ -134,18 +102,18 @@ hpt_job.run() ```python import pathlib +last_trial_id = 3300 from google.cloud import storage import tqdm -dst_dir = pathlib.Path('results/') +dst_dir = pathlib.Path('subservient-tuning-job-results/') dst_dir.mkdir(exist_ok=True, parents=True) client = storage.Client() staging_bucket = client.bucket('catx-demo-radlab-staging') -results_bucket = client.bucket('catx-demo-radlab-results') -for i in tqdm.tqdm(range(1, 501)): - src = staging_bucket.blob(f'aiplatform-custom-job-2022-04-07-16:02:35.153/{i}/model/serv_out.ipynb') - dst = dst_dir / f'serv_out_{i}.ipynb' +for i in tqdm.tqdm(range(1, last_trial_id+1)): + src = staging_bucket.blob(f'aiplatform-custom-job-2022-05-12-12:42:55.725/{i}/model/subservient_out.ipynb') + dst = dst_dir / f'subservient_out_{i}.ipynb' with dst.open('wb') as f: src.download_to_file(f) ``` @@ -154,7 +122,7 @@ for i in tqdm.tqdm(range(1, 501)): ```python tags=[] import scrapbook as sb -books = sb.read_notebooks('results/') +books = sb.read_notebooks(str(dst_dir)) ``` ```python @@ -176,7 +144,9 @@ def metrics(): yield trial_id, die_width_mm * die_width_mm, target_density, math.nan df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER'], index='TRIAL_ID').sort_index() -(df.sort_values(['TOTAL_POWER', 'DIEAREA_mm^2'], ascending=False).style +(df.dropna() + .sort_values(['DIEAREA_mm^2', 'TOTAL_POWER'], ascending=[True, True]) + .style .format({'DIEAREA_mm^2': '{:.8f}', 'PL_TARGET_DENSITY': '{:.2%}', 'TOTAL_POWER': '{:.6f}'}) .bar(subset=['TOTAL_POWER'], color='pink') .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') @@ -186,8 +156,14 @@ df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'DIEAREA_mm^2', ' ## Plot experiments ```python -df.plot.scatter(x='DIEAREA_mm^2', y='PL_TARGET_DENSITY', c='TOTAL_POWER', - cmap='cool', s=200, sharex=False) +import matplotlib.colors + +cool = matplotlib.colormaps['cool'] +cool.set_bad(color='none') +ax = df.plot.scatter(x='DIEAREA_mm^2', y='PL_TARGET_DENSITY', c='TOTAL_POWER', + cmap=cool, s=50, sharex=False, plotnonfinite=False, edgecolor='black') +plt.savefig('subservient.png') +ax ``` ```python @@ -197,10 +173,7 @@ from matplotlib import cm from tqdm import tqdm from IPython.display import Image -import matplotlib.colors -cool = matplotlib.colormaps['cool'] -cool.set_bad(color='none') min_total_power = df['TOTAL_POWER'].min() max_total_power = df['TOTAL_POWER'].max() fig, ax = plt.subplots() @@ -212,7 +185,7 @@ ax.set_ylabel('PL_TARGET_DENSITY') plt.close(fig) # hide current figure def generate_frames(): - for n in range(10, 500, 10): + for n in range(50, last_trial_id, 50): batch = df[0:n] yield [ax.scatter( batch['DIEAREA_mm^2'], batch['PL_TARGET_DENSITY'], c=batch['TOTAL_POWER'], @@ -220,8 +193,8 @@ def generate_frames(): frames = list(generate_frames()) anim = animation.ArtistAnimation(fig, frames) -anim.save('serv.gif', writer=animation.PillowWriter(fps=10)) -Image('serv.gif') +anim.save('subservient.gif', writer=animation.PillowWriter(fps=10)) +Image('subservient.gif') ``` ## Render chip layouts @@ -245,37 +218,28 @@ import numpy as np min_total_power = df['TOTAL_POWER'].min() max_total_power = df['TOTAL_POWER'].max() -def images_with_power(n): - for trial_id, trial in df.iterrows(): - book = books[f'serv_out_{trial_id}'] - if 'layout' in book.scraps: - layout = book.scraps['layout'] - f = io.BytesIO(base64.b64decode(layout.display.data['image/png'])) - img = PIL.Image.open(f)#.convert('L') - total_power = trial['TOTAL_POWER'] - power = (total_power - min_total_power) / (max_total_power - min_total_power) - yield trial_id, img, power - else: - yield trial_id, PIL.Image.new('RGBA', (100, 100)).convert('L'), 0 - if i == n-1: - break +def images_with_power(): + for trial_id, trial in df.dropna().iterrows(): + book = books[f'subservient_out_{trial_id}'] + layout = book.scraps['layout'] + f = io.BytesIO(base64.b64decode(layout.display.data['image/png'])) + img = PIL.Image.open(f)#.convert('L') + total_power = trial['TOTAL_POWER'] + power = (total_power - min_total_power) / (max_total_power - min_total_power) + yield trial_id, img, power size = (500, 500) fig, ax = plt.subplots(figsize=size) cool = cm.get_cmap('cool') def generate_frames(): - for trial_id, img, power in tqdm(images_with_power(500)): - extrema = img.getextrema() - if extrema != (0,0): - #color = np.array(cool(power)) * 255 - #img = PIL.ImageOps.colorize(img, (0, 0, 0, 255), color) - img = img.resize(size) - d = PIL.ImageDraw.Draw(img) - d.text((10, 10), f'SERV_{trial_id}', fill=(255, 255, 255, 255)) - yield img + for trial_id, img, power in tqdm(images_with_power()): + img = img.resize(size) + d = PIL.ImageDraw.Draw(img) + d.text((10, 10), f'SUBSERVIENT_{trial_id}', fill=(255, 255, 255, 255)) + yield img frames = list(generate_frames()) -frames[0].save('ALLTHESERVS_RAW.gif', save_all=True, loop=0, append_images=frames[1:]) -Image('ALLTHESERVS_RAW.gif') +frames[0].save('allthesubservients.gif', save_all=True, loop=0, append_images=frames[1:]) +Image('allthesubservients.gif') ``` From 5fe8e24f0b66d3adc5f0d1d635948d3947321668 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 17:13:23 +0900 Subject: [PATCH 61/93] conda update all --- modules/silicon_design/scripts/build/images/provision.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 1e36fc5b..97febf70 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -50,4 +50,3 @@ cp ${PROVISION_DIR}/papermill-launcher /usr/local/bin/ chmod +x /usr/local/bin/papermill-launcher echo "DaisySuccess: done" - From c299708428f8818b8b2eb50824d6ea12af8cfa76 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 17:26:40 +0900 Subject: [PATCH 62/93] add lut notebook --- .../scripts/build/notebooks/lut.md | 297 ++++++++++++++++++ .../scripts/build/notebooks/tuning-lut.md | 207 ++++++++++++ .../scripts/build/notebooks/tuning.md | 70 +++-- 3 files changed, 544 insertions(+), 30 deletions(-) create mode 100644 modules/silicon_design/scripts/build/notebooks/lut.md create mode 100644 modules/silicon_design/scripts/build/notebooks/tuning-lut.md diff --git a/modules/silicon_design/scripts/build/notebooks/lut.md b/modules/silicon_design/scripts/build/notebooks/lut.md new file mode 100644 index 00000000..66a684b2 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/lut.md @@ -0,0 +1,297 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# LUTs exploration + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + + +## Define flow parameters + +```python tags=["parameters"] +fp_core_util = 45 +pl_target_density = 90 +synth_defines='FRACTURABLE' +synth_param_inputs=5 +run_path = 'runs/lut' +``` + +## Get LUT test designs + +```python +!git clone https://github.com/growly/lut-tests.git +``` + +## Write flow configuration + +See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. + +```python +%%writefile config.tcl +# Design +# This is the config used to openlane for synthesis only +set ::env(DESIGN_NAME) "LUT" + +#set ::env(SYNTH_DEFINES) "FRACTURABLE PREDECODE_2" +#set ::env(SYNTH_DEFINES) "" +#set ::env(SYNTH_DEFINES) "FRACTURABLE" +#set ::env(SYNTH_PARAMETERS) "INPUTS=6" + +set script_dir [file dirname [file normalize [info script]]] +set ::env(VERILOG_FILES) [glob $script_dir/lut-tests/src/*.v] +set ::env(CLOCK_TREE_SYNTH) 0 +#set ::env(CLOCK_PORT) "config_clk" +set ::env(CLOCK_PORT) "" +# Design config +set ::env(CLOCK_PERIOD) 30 +#set ::env(CLOCK_PERIOD) "5.21" +set ::env(SYNTH_STRATEGY) "DELAY 1" + +#set ::env(FP_CORE_UTIL) 50 +#set ::env(PL_TARGET_DENSITY) 0.99 +#set ::env(FP_CORE_UTIL) 40 +#set ::env(PL_TARGET_DENSITY) 0.49 + +# "Enable logic verification using yosys, for comparing each netlist at each +# stage of the flow with the previous netlist and verifying that they are +# logically equivalent." Logical equivalence checking? +#set ::env(LEC_ENABLE) "1" +#set ::env(FP_WELLTAP_CELL) "sky130_fd_sc_hd__tap*" + +set ::env(CELL_PAD) "0" +set ::env(TOP_MARGIN_MULT) 1 +set ::env(BOTTOM_MARGIN_MULT) 1 +set ::env(LEFT_MARGIN_MULT) 2 +set ::env(RIGHT_MARGIN_MULT) 2 +#set ::env(FILL_INSERTION) "0" +#set ::env(PL_RESIZER_DESIGN_OPTIMIZATIONS) "0" +#set ::env(PL_RESIZER_TIMING_OPTIMIZATIONS) "0" +#set ::env(GLB_RESIZER_DESIGN_OPTIMIZATIONS) "0" +#set ::env(GLB_RESIZER_TIMING_OPTIMIZATIONS) "0" + +set ::env(RT_MAX_LAYER) "met4" +set ::env(GLB_RT_ALLOW_CONGESTION) "1" + +#set ::env(CELLS_LEF) "$::env(DESIGN_DIR)/cells.lef" +# +#set ::env(DIE_AREA) "0 0 393.76 27.200000000000003" +# +#set ::env(DIODE_INSERTION_STRATEGY) "0" + +set ::env(ROUTING_CORES) 28 + +set ::env(DESIGN_IS_CORE) "0" +set ::env(SYNTH_PARAMETERS) "INPUTS=$::env(SYNTH_PARAM_INPUTS)" + +#set ::env(FP_PDN_CORE_RING) "0" +## +#set ::env(PRODUCTS_PATH) "./build/8x32_DEFAULT/products" +# +#set ::env(INITIAL_NETLIST) "$::env(DESIGN_DIR)/RAM8.nl.v" +#set ::env(INITIAL_DEF) "$::env(DESIGN_DIR)/RAM8.placed.def" +#set ::env(INITIAL_SDC) "$::env(BASE_SDC_FILE)" +# +#set ::env(LVS_CONNECT_BY_LABEL) "1" +# +#set ::env(QUIT_ON_TIMING_VIOLATIONS) "0" +set ::env(TEST_MISMATCHES) none +set ::env(PDN_CFG) "$script_dir/pdn_cfg.tcl" +``` + +```python +%%writefile pdn_cfg.tcl + +set ::env(VDD_NET) $::env(VDD_PIN) +set ::env(GND_NET) $::env(GND_PIN) + + foreach power_pin $::env(STD_CELL_POWER_PINS) { + add_global_connection \ + -net $::env(VDD_NET) \ + -inst_pattern .* \ + -pin_pattern $power_pin \ + -power + } + foreach ground_pin $::env(STD_CELL_GROUND_PINS) { + add_global_connection \ + -net $::env(GND_NET) \ + -inst_pattern .* \ + -pin_pattern $ground_pin \ + -ground + } + +set secondary [] + +foreach net $::env(VDD_NETS) { + if { $net != $::env(VDD_NET)} { + lappend secondary $net + + set db_net [[ord::get_db_block] findNet $net] + if {$db_net == "NULL"} { + set net [odb::dbNet_create [ord::get_db_block] $net] + $net setSpecial + $net setSigType "POWER" + } + } +} + +foreach net $::env(GND_NETS) { + if { $net != $::env(GND_NET)} { + lappend secondary $net + + set db_net [[ord::get_db_block] findNet $net] + if {$db_net == "NULL"} { + set net [odb::dbNet_create [ord::get_db_block] $net] + $net setSpecial + $net setSigType "GROUND" + } + } +} + +set_voltage_domain -name CORE -power $::env(VDD_NET) -ground $::env(GND_NET) \ + -secondary_power $secondary + +define_pdn_grid \ + -name stdcell_grid \ + -starts_with POWER \ + -voltage_domain CORE \ + -pins $::env(FP_PDN_LOWER_LAYER) + +add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(FP_PDN_LOWER_LAYER) \ + -width $::env(FP_PDN_VWIDTH) \ + -pitch $::env(FP_PDN_VPITCH) \ + -offset $::env(FP_PDN_VOFFSET) \ + -starts_with POWER + +add_pdn_stripe \ + -grid stdcell_grid \ + -layer $::env(FP_PDN_RAILS_LAYER) \ + -width $::env(FP_PDN_RAIL_WIDTH) \ + -followpins \ + -starts_with POWER + +add_pdn_connect \ + -grid stdcell_grid \ + -layers "$::env(FP_PDN_RAILS_LAYER) $::env(FP_PDN_LOWER_LAYER)" + +define_pdn_grid \ + -macro \ + -name macro \ + -starts_with POWER \ + -halo "$::env(FP_PDN_HORIZONTAL_HALO) $::env(FP_PDN_VERTICAL_HALO)" + +add_pdn_connect \ + -grid macro \ + -layers "$::env(FP_PDN_LOWER_LAYER) $::env(FP_PDN_UPPER_LAYER)" + +``` + +## Run OpenLane flow + +[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. + +```python +#papermill_description=RunningOpenLaneFlow +%env FP_CORE_UTIL={fp_core_util} +%env PL_TARGET_DENSITY={pl_target_density} +%env SYNTH_DEFINES={synth_defines} +%env SYNTH_PARAM_INPUTS={synth_param_inputs} + +!flow.tcl -design . -run_path {run_path} +``` + +## Display layout + +Use [GDSII Tool Kit](https://github.com/heitzmann/gdstk) to convert the resulting GDSII file to SVG. + +```python +!python -m pip install scrapbook +``` + +```python +#papermill_description=RenderingGDS +import pathlib +import gdstk +import IPython.display +import scrapbook as sb + +gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] +library = gdstk.read_gds(gds_path) +top_cells = library.top_level() +svg_path = pathlib.Path(run_path) / 'adders.svg' +top_cells[0].write_svg(svg_path) +sb.glue('layout', IPython.display.SVG(svg_path), 'display', display=True) +``` + +## Dump flow report + +See [OpenLane Datapoint Definitions](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/regression_results/datapoint_definitions.md) for the description of the report columns. + +```python +#papermill_description=DumpingReport +import pandas as pd +import pathlib +import scrapbook as sb + +final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/metrics.csv'))[-1] +df = pd.read_csv(final_summary_report) +pd.set_option('display.max_rows', None) +sb.glue('summary', df, 'pandas') +df.transpose() +``` + +## Extract power metrics + +Build a pandas dataframe with area, density and power consumption. + +```python +#papermill_description=ExtractingMetrics +import scrapbook as sb + +def area_density_ppa(): + for report in sorted(pathlib.Path(run_path).glob('*/reports/metrics.csv')): + yield (df['FP_CORE_UTIL'][0], df['PL_TARGET_DENSITY'][0], df['power_typical_switching_uW'][0]) + +df = pd.DataFrame(area_density_ppa(), columns=('DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'power_typical_switching_uW')) +sb.glue('metrics', df, 'pandas') +(df.style.hide_index() + .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) + .bar(subset=['power_typical_switching_uW'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) +``` + +Report metrics for hyper-parameters tuning. + +```python +!python -m pip install cloudml-hypertune +``` + +```python +#papermill_description=ReportingMetrics +import hypertune + +total_power = df['power_typical_switching_uW'][0] * 1e6 +print('reporting metric:', 'power_typical_switching_uW', total_power) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='power_typical_switching_uW', + metric_value=total_power, +) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/tuning-lut.md b/modules/silicon_design/scripts/build/notebooks/tuning-lut.md new file mode 100644 index 00000000..8e7522ed --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/tuning-lut.md @@ -0,0 +1,207 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Parameter Tuning Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to leverage [Vertex AI hyperparameter tuning](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) in order to find the right flow parameters value to optimize a given metric. + + +## Define project parameters + +```python tags=["parameters"] +import pathlib + +worker_image = 'us-east4-docker.pkg.dev/catx-demo-radlab/containers/silicon-design-ubuntu-2004:latest' +staging_bucket = 'catx-demo-radlab-staging' +project = 'catx-demo-radlab' +location = 'us-central1' +machine_type = 'n1-standard-8' +notebook = 'lut.ipynb' +prefix = pathlib.Path(notebook).stem +staging_dir = f'{prefix}-tuning' +last_trial_id = 20 +``` + +## Stage the notebook for the experiment + +```python tags=[] +!gsutil cp {notebook} gs://{staging_bucket}/{staging_dir}/{notebook} +``` + +## Create Parameters and Metrics specs + +We want to find the best value for *target density* and *die area* in order optimize *total power* consumption. + +Those keys map to the [parameters](https://papermill.readthedocs.io/en/latest/usage-parameterize.html) and [metrics](https://github.com/GoogleCloudPlatform/cloudml-hypertune) advertised by the notebook. + +```python tags=[] +from google.cloud.aiplatform import hyperparameter_tuning as hpt + +parameter_spec = { + 'pl_target_density': hpt.DoubleParameterSpec(min=0.4, max=0.99, scale='log'), + 'fp_core_util': hpt.DoubleParameterSpec(min=5, max=90, scale='linear'), +} + +metric_spec={'power_typical_switching_uW': 'minimize'} +``` + +## Create Custom Job spec + +```python tags=[] +from google.cloud import aiplatform +import pathlib + +worker_pool_specs = [{ + 'machine_spec': { + 'machine_type': machine_type, + }, + 'replica_count': 1, + 'container_spec': { + 'image_uri': worker_image, + 'args': ['/usr/local/bin/papermill-launcher', + f'gs://{staging_bucket}/{staging_dir}/{notebook}', + f'$AIP_MODEL_DIR/{prefix}_out.ipynb', + '--run_dir=/tmp'] + } +}] +custom_job = aiplatform.CustomJob(display_name=f'{prefix}-custom-job', + worker_pool_specs=worker_pool_specs, + staging_bucket=staging_bucket, + base_output_dir=f'gs://{staging_bucket}/{staging_dir}') +``` + +## Run Hyperparameter tuning job + +```python tags=[] +from google.cloud import aiplatform +parameters_count = len(parameter_spec.keys()) +metrics_count = len(metric_spec.keys()) +max_trial_count = 100 * parameters_count * metrics_count +parallel_trial_count = 20 + +hpt_job = aiplatform.HyperparameterTuningJob( + display_name=f'{prefix}-tuning-job', + custom_job=custom_job, + metric_spec=metric_spec, + parameter_spec=parameter_spec, + max_trial_count=max_trial_count, + parallel_trial_count=parallel_trial_count, + max_failed_trial_count=max_trial_count) +hpt_job.run(sync=False) +``` + +## Fetch notebooks for all study trials + +```python +import pathlib +from google.cloud import storage +import tqdm + +local_dir = pathlib.Path(staging_dir) +local_dir.mkdir(exist_ok=True, parents=True) + +client = storage.Client() +bucket = client.bucket(staging_bucket) +for i in tqdm.tqdm(range(1, last_trial_id+1)): + src = bucket.blob(f'{staging_dir}/{i}/model/{prefix}_out.ipynb') + dst = local_dir / f'{prefix}_out_{i}.ipynb' + with dst.open('wb') as f: + src.download_to_file(f) +``` + +## Extract metrics from notebooks + +```python tags=[] +import scrapbook as sb +books = sb.read_notebooks(str(local_dir)) +``` + +```python tags=[] +import pathlib +import math + +import pandas as pd +import tqdm +def metrics(): + for b in tqdm.tqdm(books): + trial_id = int(pathlib.Path(books[b].filename).stem.split('_')[-1]) + if 'metrics' in books[b].scraps: + metrics = books[b].scraps['metrics'].data + yield trial_id, metrics['FP_CORE_UTIL'][0], metrics['PL_TARGET_DENSITY'][0], metrics['power_typical_switching_uW'][0] + else: + params = books[b].parameters + fp_core_util = float(params.fp_core_util) + pl_target_density = float(params.fp_target_density) + yield trial_id, fp_core_util, pl_target_density, math.nan + +df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'FP_CORE_UTIL', 'PL_TARGET_DENSITY', 'power_typical_switching_uW'], index='TRIAL_ID').sort_index() +(df.dropna() + .sort_values(['power_typical_switching_uW'], ascending=[False]).drop_duplicates(['power_typical_switching_uW']) + .style + .format({'FP_CORE_UTIL': '{:.2f}', 'PL_TARGET_DENSITY': '{:.2%}', 'TOTAL_POWER': '{:.6f}'}) + .bar(subset=['power_typical_switching_uW'], color='pink') + .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['FP_CORE_UTIL'])) +``` + +## Plot experiments + +```python +import matplotlib.colors +from matplotlib import pyplot as plt + +cool = matplotlib.colormaps['cool'] +cool.set_bad(color='none') +ax = df.plot.scatter(x='FP_CORE_UTIL', y='PL_TARGET_DENSITY', c='power_typical_switching_uW', + cmap=cool, s=50, sharex=False, plotnonfinite=True, alpha=1.0, edgecolor='black') +plt.savefig('{prefix}.png') +ax +``` + +```python +from matplotlib import pyplot as plt +from matplotlib import animation +from matplotlib import cm + +from tqdm import tqdm +from IPython.display import Image + +min_total_power = df['power_typical_switching_uW'].min() +max_total_power = df['power_typical_switching_uW'].max() +fig, ax = plt.subplots() +fig.colorbar(cm.ScalarMappable(matplotlib.colors.Normalize(min_total_power, max_total_power), cmap=cool), + label='power_typical_switching_uW', + ax=ax) +ax.set_xlabel('FP_CORE_UTIL') +ax.set_ylabel('PL_TARGET_DENSITY') +plt.close(fig) # hide current figure + +def generate_frames(): + for n in range(50, last_trial_id, 50): + batch = df[0:n] + yield [ax.scatter( + batch['FP_CORE_UTIL'], batch['PL_TARGET_DENSITY'], c=batch['power_typical_switching_uW'], + s=50, vmin=min_total_power, vmax=max_total_power, cmap=cool, plotnonfinite=True, edgecolor='black')] + +frames = list(generate_frames()) +anim = animation.ArtistAnimation(fig, frames) +anim.save('{prefix}.gif', writer=animation.PillowWriter(fps=10)) +Image('{prefix}.gif') +``` diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/tuning.md index 23a616e2..25e95aa9 100644 --- a/modules/silicon_design/scripts/build/notebooks/tuning.md +++ b/modules/silicon_design/scripts/build/notebooks/tuning.md @@ -26,17 +26,23 @@ This notebook shows how to leverage [Vertex AI hyperparameter tuning](https://cl ## Define project parameters ```python tags=["parameters"] -worker_image = 'us-central1-docker.pkg.dev/catx-demo-radlab/containers/silicon-design-ubuntu-2004:latest' -staging_bucket = 'gs://catx-demo-radlab-staging' +import pathlib + +worker_image = 'us-east4-docker.pkg.dev/catx-demo-radlab/containers/silicon-design-ubuntu-2004:latest' +staging_bucket = 'catx-demo-radlab-staging' project = 'catx-demo-radlab' location = 'us-central1' +machine_type = 'n1-standard-8' +notebook = 'subservient.ipynb' +prefix = pathlib.Path(notebook).stem +staging_dir = f'{prefix}-tuning' +last_trial_id = 20 ``` ## Stage the notebook for the experiment ```python tags=[] -!gsutil mb {staging_bucket} -!gsutil cp subservient.ipynb {staging_bucket}/ +!gsutil cp {notebook} gs://{staging_bucket}/{staging_dir}/{notebook} ``` ## Create Parameters and Metrics specs @@ -60,60 +66,62 @@ metric_spec={'total_power': 'minimize'} ```python tags=[] from google.cloud import aiplatform +import pathlib worker_pool_specs = [{ 'machine_spec': { - 'machine_type': 'n1-standard-4', + 'machine_type': machine_type, }, 'replica_count': 1, 'container_spec': { 'image_uri': worker_image, 'args': ['/usr/local/bin/papermill-launcher', - f'{staging_bucket}/subservient.ipynb', - '$AIP_MODEL_DIR/subservient_out.ipynb', + f'gs://{staging_bucket}/{staging_dir}/{notebook}', + f'$AIP_MODEL_DIR/{prefix}_out.ipynb', '--run_dir=/tmp'] } }] - -custom_job = aiplatform.CustomJob(display_name='subservient-flow-job', - worker_pool_specs=worker_pool_specs, - staging_bucket=staging_bucket) +custom_job = aiplatform.CustomJob(display_name=f'{prefix}-custom-job', + worker_pool_specs=worker_pool_specs, + staging_bucket=staging_bucket, + base_output_dir=f'gs://{staging_bucket}/{staging_dir}') ``` ## Run Hyperparameter tuning job ```python tags=[] from google.cloud import aiplatform +parameters_count = len(parameter_spec.keys()) +metrics_count = len(metric_spec.keys()) +max_trial_count = 100 * parameters_count * metrics_count +parallel_trial_count = 20 hpt_job = aiplatform.HyperparameterTuningJob( - display_name='subservient-tuning-job', + display_name=f'{prefix}-tuning-job', custom_job=custom_job, metric_spec=metric_spec, parameter_spec=parameter_spec, - max_trial_count=10000, - parallel_trial_count=50, - max_failed_trial_count=10000) - -hpt_job.run() + max_trial_count=max_trial_count, + parallel_trial_count=parallel_trial_count, + max_failed_trial_count=max_trial_count) +hpt_job.run(sync=False) ``` ## Fetch notebooks for all study trials ```python import pathlib - -last_trial_id = 3300 from google.cloud import storage import tqdm -dst_dir = pathlib.Path('subservient-tuning-job-results/') -dst_dir.mkdir(exist_ok=True, parents=True) +local_dir = pathlib.Path(staging_dir) +local_dir.mkdir(exist_ok=True, parents=True) client = storage.Client() -staging_bucket = client.bucket('catx-demo-radlab-staging') +bucket = client.bucket(staging_bucket) for i in tqdm.tqdm(range(1, last_trial_id+1)): - src = staging_bucket.blob(f'aiplatform-custom-job-2022-05-12-12:42:55.725/{i}/model/subservient_out.ipynb') - dst = dst_dir / f'subservient_out_{i}.ipynb' + src = bucket.blob(f'{staging_dir}/{i}/model/{prefix}_out.ipynb') + dst = local_dir / f'{prefix}_out_{i}.ipynb' with dst.open('wb') as f: src.download_to_file(f) ``` @@ -146,6 +154,7 @@ def metrics(): df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER'], index='TRIAL_ID').sort_index() (df.dropna() .sort_values(['DIEAREA_mm^2', 'TOTAL_POWER'], ascending=[True, True]) + .drop_duplicates(['TOTAL_POWER']) .style .format({'DIEAREA_mm^2': '{:.8f}', 'PL_TARGET_DENSITY': '{:.2%}', 'TOTAL_POWER': '{:.6f}'}) .bar(subset=['TOTAL_POWER'], color='pink') @@ -157,12 +166,13 @@ df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'DIEAREA_mm^2', ' ```python import matplotlib.colors +from matplotlib import pyplot as plt cool = matplotlib.colormaps['cool'] cool.set_bad(color='none') ax = df.plot.scatter(x='DIEAREA_mm^2', y='PL_TARGET_DENSITY', c='TOTAL_POWER', - cmap=cool, s=50, sharex=False, plotnonfinite=False, edgecolor='black') -plt.savefig('subservient.png') + cmap=cool, s=50, sharex=False, plotnonfinite=True, alpha=1.0, edgecolor='black') +plt.savefig('{prefix}.png') ax ``` @@ -193,8 +203,8 @@ def generate_frames(): frames = list(generate_frames()) anim = animation.ArtistAnimation(fig, frames) -anim.save('subservient.gif', writer=animation.PillowWriter(fps=10)) -Image('subservient.gif') +anim.save('{prefix}.gif', writer=animation.PillowWriter(fps=10)) +Image('{prefix}.gif') ``` ## Render chip layouts @@ -240,6 +250,6 @@ def generate_frames(): yield img frames = list(generate_frames()) -frames[0].save('allthesubservients.gif', save_all=True, loop=0, append_images=frames[1:]) -Image('allthesubservients.gif') +frames[0].save('allthe{prefix}.gif', save_all=True, loop=0, append_images=frames[1:]) +Image('allthe{prefix}.gif') ``` From 381fb56b12b16485aeb0d48f0a0956f3be6e8085 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 27 May 2022 17:37:19 +0900 Subject: [PATCH 63/93] enable compute build and fix storage service account --- modules/silicon_design/main.tf | 2 +- modules/silicon_design/scripts/build/cloudbuild.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index a9a9c68c..ed193460 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -46,7 +46,7 @@ locals { "roles/notebooks.admin", "roles/compute.instanceAdmin", "roles/iam.serviceAccountUser", - "roles/storage.objectViewer", + "roles/storage.admin", "roles/aiplatform.admin", ] diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index af970f57..3eef6c16 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -46,7 +46,7 @@ steps: cd scripts/build/images/ gsutil cp gs://compute-image-tools/release/linux/daisy . chmod +x daisy - echo gcloud compute images describe $_COMPUTE_IMAGE-$_IMAGE_TAG || ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$_IMAGE_TAG,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET,service_account=$_CLOUD_BUILD_SA compute_image.wf.json + gcloud compute images describe $_COMPUTE_IMAGE-$_IMAGE_TAG || ./daisy -project $PROJECT_ID -zone $_ZONE -variables image_name=$_COMPUTE_IMAGE,image_tag=$_IMAGE_TAG,network=$_COMPUTE_NETWORK,subnet=$_COMPUTE_SUBNET,service_account=$_CLOUD_BUILD_SA compute_image.wf.json waitFor: ['-'] - id: 'container-image-pull' name: 'gcr.io/cloud-builders/docker' From 76c0b7fe6d4e731daff96f351bbe6928c25489cb Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Sat, 28 May 2022 01:43:06 +0900 Subject: [PATCH 64/93] remove managed notebook --- modules/silicon_design/main.tf | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index ed193460..91a812f7 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -234,38 +234,6 @@ resource "google_project_iam_member" "ai_notebook_user_role2" { role = "roles/viewer" } -resource "google_notebooks_runtime" "ai_notebook_managed" { - count = var.notebook_count - project = local.project.project_id - name = local.notebook_names[count.index] - location = local.region - - access_config { - access_type = "SINGLE_USER" - runtime_owner = "proppy@google.com" - } - virtual_machine { - virtual_machine_config { - machine_type = var.machine_type - data_disk { - initialize_params { - disk_size_gb = "100" - disk_type = "PD_STANDARD" - } - } - container_images { - repository = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}" - tag = local.image_tag - } - } - } - - depends_on = [ - time_sleep.wait_120_seconds, - null_resource.build_and_push_image, - ] -} - resource "google_notebooks_instance" "ai_notebook" { count = var.notebook_count project = local.project.project_id From 535d7ce637812f9e6fc5aa9df0a45fb55b978da1 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Sat, 28 May 2022 01:44:34 +0900 Subject: [PATCH 65/93] switch to micromamba --- .../silicon_design/scripts/build/cloudbuild.yaml | 4 ++-- .../scripts/build/images/provision.sh | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 3eef6c16..5f288fbf 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -56,14 +56,14 @@ steps: - id: 'container-image-build' name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', '$_CONTAINER_IMAGE:$_IMAGE_TAG', '--cache-from', '$_CONTAINER_IMAGE:latest', './scripts/build/images'] - waitFor: ['container-image-pull'] + waitFor: ['container-image-pull'] - id: 'container-image-tag' name: 'gcr.io/cloud-builders/docker' args: ['tag', '$_CONTAINER_IMAGE:$_IMAGE_TAG', '$_CONTAINER_IMAGE:latest'] waitFor: ['container-image-build'] - id: 'container-image-test' name: 'gcr.io/cloud-builders/docker' - args: ['run', '$_CONTAINER_IMAGE:$BUILD_ID', 'flow.tcl', '-design', 'inverter'] + args: ['run', '$_CONTAINER_IMAGE:$_IMAGE_TAG', 'flow.tcl', '-design', 'inverter'] waitFor: ['container-image-tag'] images: - '$_CONTAINER_IMAGE:$_IMAGE_TAG' diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 97febf70..1e0143e9 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -27,8 +27,15 @@ mkdir -p ${PROVISION_DIR} gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true echo "DaisyStatus: installing conda-eda environment" +<<<<<<< HEAD curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -C /usr/local -xvj bin/micromamba micromamba create --yes -r /opt/conda -n silicon --file ${PROVISION_DIR}/environment.yml +======= +cd /opt/conda && curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba +/opt/conda/bin/micromamba install -vvv --yes -p /opt/conda --strict-channel-priority -c litex-hub -c main openroad=2.0_3818_g2ae9162aa open_pdks.sky130a magic netgen yosys=0.17 iverilog xls +/opt/conda/bin/micromamba install -vvv --yes -p /opt/conda --strict-channel-priority -c conda-forge gdstk ngspice-lib tcllib +/opt/conda/bin/python -m pip install pyyaml click pandas pyspice gdsfactory klayout scrapbook[gcs] google-cloud-aiplatform cloudml-hypertune +>>>>>>> fe96f30 (switch to micromamba) echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane @@ -37,10 +44,16 @@ echo "DaisyStatus: patching OpenLane" cp ${PROVISION_DIR}/install.tcl /OpenLane/configuration/ echo ' install.tcl' >> /OpenLane/configuration/load_order.txt mkdir -p /OpenLane/install/build/versions +<<<<<<< HEAD for tool in yosys netgen do /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} done +======= +cp ${PROVISION_DIR}/env.tcl /OpenLane/install/ +# https://github.com/The-OpenROAD-Project/OpenLane/pull/1027 +curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project/OpenLane/pull/1027.patch | patch -d /OpenLane -p1 +>>>>>>> fe96f30 (switch to micromamba) echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh From afc1be27e7993e16f772801df05ccd99c153c1c9 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 17 Jun 2022 03:36:42 +0900 Subject: [PATCH 66/93] modules/silicon: pin some packages --- .../scripts/build/images/provision.sh | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 1e0143e9..25a2f3e6 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -27,15 +27,8 @@ mkdir -p ${PROVISION_DIR} gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true echo "DaisyStatus: installing conda-eda environment" -<<<<<<< HEAD curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -C /usr/local -xvj bin/micromamba micromamba create --yes -r /opt/conda -n silicon --file ${PROVISION_DIR}/environment.yml -======= -cd /opt/conda && curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba -/opt/conda/bin/micromamba install -vvv --yes -p /opt/conda --strict-channel-priority -c litex-hub -c main openroad=2.0_3818_g2ae9162aa open_pdks.sky130a magic netgen yosys=0.17 iverilog xls -/opt/conda/bin/micromamba install -vvv --yes -p /opt/conda --strict-channel-priority -c conda-forge gdstk ngspice-lib tcllib -/opt/conda/bin/python -m pip install pyyaml click pandas pyspice gdsfactory klayout scrapbook[gcs] google-cloud-aiplatform cloudml-hypertune ->>>>>>> fe96f30 (switch to micromamba) echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane @@ -43,17 +36,6 @@ git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Proje echo "DaisyStatus: patching OpenLane" cp ${PROVISION_DIR}/install.tcl /OpenLane/configuration/ echo ' install.tcl' >> /OpenLane/configuration/load_order.txt -mkdir -p /OpenLane/install/build/versions -<<<<<<< HEAD -for tool in yosys netgen -do - /opt/conda/bin/conda list -c ${tool} > /OpenLane/install/build/versions/${tool} -done -======= -cp ${PROVISION_DIR}/env.tcl /OpenLane/install/ -# https://github.com/The-OpenROAD-Project/OpenLane/pull/1027 -curl --silent https://patch-diff.githubusercontent.com/raw/The-OpenROAD-Project/OpenLane/pull/1027.patch | patch -d /OpenLane -p1 ->>>>>>> fe96f30 (switch to micromamba) echo "DaisyStatus: adding profile hook" cp ${PROVISION_DIR}/profile.sh /etc/profile.d/silicon-design-profile.sh From dc12343143482cab5e72e155e3b1b3d2a790133c Mon Sep 17 00:00:00 2001 From: HelgeGehring Date: Thu, 7 Jul 2022 13:07:11 -0700 Subject: [PATCH 67/93] added minimal example --- .../build/notebooks/minimal/minimal_job.md | 52 ++++++ .../build/notebooks/minimal/minimal_tuning.md | 175 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 modules/silicon_design/scripts/build/notebooks/minimal/minimal_job.md create mode 100644 modules/silicon_design/scripts/build/notebooks/minimal/minimal_tuning.md diff --git a/modules/silicon_design/scripts/build/notebooks/minimal/minimal_job.md b/modules/silicon_design/scripts/build/notebooks/minimal/minimal_job.md new file mode 100644 index 00000000..94efe54b --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/minimal/minimal_job.md @@ -0,0 +1,52 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +## Parameter cell with the tag parameters + +```python tags=["parameters"] +param_1 = 5 +param_2 = 20 +``` + +```python +# Parse string inputs +param_1 = float(param_1) +param_2 = float(param_2) +``` + +```python +# +product = ((param_1-25)**2+10) * ((param_2-25)**2+10) +``` + +```python +import hypertune + +print('reporting metric:', 'product', product) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag = 'product', + metric_value = product, +) +``` + +```python +import pandas as pd +import scrapbook as sb + +df = pd.DataFrame([[param_1, param_2, product]], columns=('param_1', 'param_2', 'product')) +sb.glue('metrics', df, 'pandas') +df +``` diff --git a/modules/silicon_design/scripts/build/notebooks/minimal/minimal_tuning.md b/modules/silicon_design/scripts/build/notebooks/minimal/minimal_tuning.md new file mode 100644 index 00000000..80e48bac --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/minimal/minimal_tuning.md @@ -0,0 +1,175 @@ +--- +jupyter: + jupytext: + formats: ipynb,md + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.13.8 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +## Define project parameters + +```python tags=["parameters"] +import pathlib + +worker_image = 'us-east4-docker.pkg.dev/catx-demo-radlab/containers/silicon-design-ubuntu-2004:latest' +staging_bucket = 'catx-demo-radlab-staging' +project = 'catx-demo-radlab' +location = 'us-central1' +machine_type = 'n1-standard-8' +notebook = 'minimal_job.ipynb' +prefix = pathlib.Path(notebook).stem +staging_dir = f'{prefix}-tuning' +``` + +## Delete data from previous runs and stage the notebook for the experiment + +```python tags=[] +!rm -r {staging_dir} +!gsutil rm -r gs://{staging_bucket}/{staging_dir}/ +!gsutil cp {notebook} gs://{staging_bucket}/{staging_dir}/{notebook} +``` + +## Create Parameters and Metrics specs + +We want to find the best value for *target density* and *die area* in order optimize *total power* consumption. + +Those keys map to the [parameters](https://papermill.readthedocs.io/en/latest/usage-parameterize.html) and [metrics](https://github.com/GoogleCloudPlatform/cloudml-hypertune) advertised by the notebook. + +```python tags=[] +from google.cloud.aiplatform import hyperparameter_tuning as hpt + +parameter_spec = { + 'param_1': hpt.DoubleParameterSpec(min=10, max=100, scale='linear'), + 'param_2': hpt.DoubleParameterSpec(min=10, max=300, scale='linear'), +} + +metric_spec={'product': 'minimize'} +``` + +## Create Custom Job spec + +```python tags=[] +from google.cloud import aiplatform +import pathlib + +worker_pool_specs = [{ + 'machine_spec': { + 'machine_type': machine_type, + }, + 'replica_count': 1, + 'container_spec': { + 'image_uri': worker_image, + 'args': ['/usr/local/bin/papermill-launcher', + f'gs://{staging_bucket}/{staging_dir}/{notebook}', + f'$AIP_MODEL_DIR/{prefix}_out.ipynb', + '--run_dir=/tmp'] + } +}] +custom_job = aiplatform.CustomJob(display_name=f'{prefix}-custom-job', + worker_pool_specs=worker_pool_specs, + staging_bucket=staging_bucket, + base_output_dir=f'gs://{staging_bucket}/{staging_dir}') +``` + +## Run Hyperparameter tuning job + +```python tags=[] +from google.cloud import aiplatform + +parameters_count = len(parameter_spec.keys()) +metrics_count = len(metric_spec.keys()) +max_trial_count = 100 * parameters_count * metrics_count +parallel_trial_count = 20 + +hpt_job = aiplatform.HyperparameterTuningJob( + display_name=f'{prefix}-tuning-job', + custom_job=custom_job, + metric_spec=metric_spec, + parameter_spec=parameter_spec, + max_trial_count=max_trial_count, + parallel_trial_count=parallel_trial_count, + max_failed_trial_count=max_trial_count) +hpt_job.run(sync=False) +``` + +## Check the current state + +```python +[(job.display_name, job.done(), job.state) for job in aiplatform.HyperparameterTuningJob.list(filter=f'display_name={prefix}-tuning-job') if not job.done()] +``` + +## Fetch notebooks for all study trials + +```python +import pathlib +from google.cloud import storage +import tqdm + +local_dir = pathlib.Path(staging_dir) +local_dir.mkdir(exist_ok=True, parents=True) + +client = storage.Client() +bucket = client.bucket(staging_bucket) +for i in tqdm.tqdm(range(1, max_trial_count+1)): + src = bucket.blob(f'{staging_dir}/{i}/model/{prefix}_out.ipynb') + if not src.exists(): + break + dst = local_dir / f'{prefix}_out_{i}.ipynb' + with dst.open('wb') as f: + src.download_to_file(f) +``` + +## Extract metrics from notebooks + +```python tags=[] +import scrapbook as sb +books = sb.read_notebooks(str(local_dir)) +``` + +```python +import pathlib +import math +import pandas as pd +import tqdm + +def metrics(): + for b in tqdm.tqdm(books): + trial_id = int(pathlib.Path(books[b].filename).stem.split('_')[-1]) + param_1 = float(books[b].parameters.param_1) + param_2 = float(books[b].parameters.param_2) + + if 'metrics' in books[b].scraps: + metrics = books[b].scraps['metrics'].data + yield trial_id, param_1, param_2, metrics['product'][0] + else: + yield trial_id, param_1, param_2, math.nan + +df = pd.DataFrame.from_records(metrics(), columns=['TRIAL_ID', 'param_1', 'param_2', 'product'], index='TRIAL_ID').sort_index() + +df.sort_values(['product'], ascending=[True]).style.bar(color='lightblue', vmin=0.001, subset=['product']).background_gradient(subset=['param_1'], cmap='Greens').background_gradient(subset=['param_2'], cmap='Blues') +``` + +## Plot experiments + +```python +import matplotlib.colors +from matplotlib import pyplot as plt + +cool = matplotlib.colormaps['cool'] +cool.set_bad(color='none') +ax = df.plot.scatter(x='param_1', y='param_2', c='product', + cmap=cool, s=50, sharex=False, plotnonfinite=True, alpha=1.0, edgecolor='black') +plt.savefig('{prefix}.png') +ax +``` + +```python + +``` From e674c541c2c2fecf5d08be3ef1c152953de75477 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 26 Jul 2022 16:50:51 +0900 Subject: [PATCH 68/93] modules/silicon/provision: guard gce only command --- modules/silicon_design/scripts/build/images/provision.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 25a2f3e6..9fac767c 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -21,10 +21,14 @@ env OPENLANE_VERSION=master PROVISION_DIR=/tmp/provision +SYSTEM_NAME=$(dmidecode -s system-product-name || true) + +if [ -n "$(echo ${SYSTEM_NAME} | grep 'Google Compute Engine')" ]; then echo "DaisyStatus: fetching provisioning script" DAISY_SOURCES_PATH=$(curl -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/daisy-sources-path) mkdir -p ${PROVISION_DIR} gsutil -m rsync ${DAISY_SOURCES_PATH}/provision/ ${PROVISION_DIR}/ || true +fi echo "DaisyStatus: installing conda-eda environment" curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -C /usr/local -xvj bin/micromamba From 2aa4c2939dd1a8a526a590453538c68417970828 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 26 Jul 2022 16:54:28 +0900 Subject: [PATCH 69/93] modules/silicon/notebooks/inverter: fix config path --- modules/silicon_design/scripts/build/notebooks/inverter.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/silicon_design/scripts/build/notebooks/inverter.md b/modules/silicon_design/scripts/build/notebooks/inverter.md index 5689a6f7..473edbc1 100644 --- a/modules/silicon_design/scripts/build/notebooks/inverter.md +++ b/modules/silicon_design/scripts/build/notebooks/inverter.md @@ -100,7 +100,7 @@ import pandas as pd import pathlib import scrapbook as sb -final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/final_summary_report.csv'))[-1] +final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/metrics.csv'))[-1] df = pd.read_csv(final_summary_report) pd.set_option('display.max_rows', None) sb.glue('summary', df, 'pandas') @@ -123,8 +123,8 @@ def get_power(sta_power_report): def area_density_ppa(): for report in sorted(pathlib.Path(run_path).glob('*/reports')): - sta_power_report = report / 'routing/23-parasitics_sta.power.rpt' - final_summary_report = report / 'final_summary_report.csv' + sta_power_report = report / 'signoff/27-rcx_mca_sta.power.rpt' + final_summary_report = report / 'metrics.csv' if final_summary_report.exists() and sta_power_report.exists(): df = pd.read_csv(final_summary_report) power = get_power(sta_power_report) From aa9a0c331c8085fd4a1d42bac06ecdec93eaaf99 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 26 Jul 2022 22:51:32 +0900 Subject: [PATCH 70/93] modules/silicon: install orfs --- .../silicon_design/scripts/build/images/Dockerfile | 1 - .../silicon_design/scripts/build/images/provision.sh | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/Dockerfile b/modules/silicon_design/scripts/build/images/Dockerfile index 5802f93e..616c48bc 100644 --- a/modules/silicon_design/scripts/build/images/Dockerfile +++ b/modules/silicon_design/scripts/build/images/Dockerfile @@ -14,7 +14,6 @@ # limitations under the License. FROM gcr.io/deeplearning-platform-release/base-cpu -RUN apt-get update && apt-get -yq install locales locales-all COPY provision.sh /tmp/provision.sh COPY provision/ /tmp/provision/ RUN bash -x /tmp/provision.sh diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 9fac767c..af175cdf 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -19,10 +19,14 @@ trap "echo DaisyFailure: trapped error" ERR env OPENLANE_VERSION=master +OPENROAD_FLOW_VERSION=master PROVISION_DIR=/tmp/provision SYSTEM_NAME=$(dmidecode -s system-product-name || true) +echo "DaisyStatus: install system dependencies" +apt-get update && apt-get -yq install locales locales-all time + if [ -n "$(echo ${SYSTEM_NAME} | grep 'Google Compute Engine')" ]; then echo "DaisyStatus: fetching provisioning script" DAISY_SOURCES_PATH=$(curl -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/daisy-sources-path) @@ -37,6 +41,13 @@ micromamba create --yes -r /opt/conda -n silicon --file ${PROVISION_DIR}/environ echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane +echo "DaisyStatus: installing OpenROAD Flow" +!git clone --depth 1 -b ${OPENROAD_FLOW_VERSION} https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts /OpenROAD-flow-script + +echo "DaisyStatus: installing KLayout Flow" +!curl -O https://www.klayout.org/downloads/Ubuntu-20/klayout_0.27.10-1_amd64.deb +!dpkg -i klayout_0.27.10-1_amd64.deb || apt-get -f -yq install + echo "DaisyStatus: patching OpenLane" cp ${PROVISION_DIR}/install.tcl /OpenLane/configuration/ echo ' install.tcl' >> /OpenLane/configuration/load_order.txt From f6d231f3a2b4965545734185d103cdc44eb339da Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 26 Jul 2022 23:10:35 +0900 Subject: [PATCH 71/93] modules/silicon: add lock --- modules/silicon_design/scripts/build/images/provision.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index af175cdf..8f123a04 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -25,7 +25,7 @@ PROVISION_DIR=/tmp/provision SYSTEM_NAME=$(dmidecode -s system-product-name || true) echo "DaisyStatus: install system dependencies" -apt-get update && apt-get -yq install locales locales-all time +apt-get update && apt-get -o DPkg::Lock::Timeout=-1 -yq install locales locales-all time if [ -n "$(echo ${SYSTEM_NAME} | grep 'Google Compute Engine')" ]; then echo "DaisyStatus: fetching provisioning script" From 8c9206cc7c3b7f4943a4d5d032f2b8729d43c2dc Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 26 Jul 2022 23:55:29 +0900 Subject: [PATCH 72/93] modules/silicon/build/images: fix command prefix --- modules/silicon_design/scripts/build/images/provision.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 8f123a04..bc2b17ee 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -42,11 +42,11 @@ echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane echo "DaisyStatus: installing OpenROAD Flow" -!git clone --depth 1 -b ${OPENROAD_FLOW_VERSION} https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts /OpenROAD-flow-script +git clone --depth 1 -b ${OPENROAD_FLOW_VERSION} https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts /OpenROAD-flow-script echo "DaisyStatus: installing KLayout Flow" -!curl -O https://www.klayout.org/downloads/Ubuntu-20/klayout_0.27.10-1_amd64.deb -!dpkg -i klayout_0.27.10-1_amd64.deb || apt-get -f -yq install +curl -O https://www.klayout.org/downloads/Ubuntu-20/klayout_0.27.10-1_amd64.deb +dpkg -i klayout_0.27.10-1_amd64.deb || apt-get -f -yq install echo "DaisyStatus: patching OpenLane" cp ${PROVISION_DIR}/install.tcl /OpenLane/configuration/ From 20d70b398a10f67008fc8271b1be49a71f250b57 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 27 Jul 2022 02:00:36 +0900 Subject: [PATCH 73/93] modules/silicon/build/images: fix orfs name --- modules/silicon_design/scripts/build/images/provision.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index bc2b17ee..1e2a7bbe 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -42,7 +42,7 @@ echo "DaisyStatus: installing OpenLane" git clone --depth 1 -b ${OPENLANE_VERSION} https://github.com/The-OpenROAD-Project/OpenLane /OpenLane echo "DaisyStatus: installing OpenROAD Flow" -git clone --depth 1 -b ${OPENROAD_FLOW_VERSION} https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts /OpenROAD-flow-script +git clone --depth 1 -b ${OPENROAD_FLOW_VERSION} https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts /OpenROAD-flow-scripts echo "DaisyStatus: installing KLayout Flow" curl -O https://www.klayout.org/downloads/Ubuntu-20/klayout_0.27.10-1_amd64.deb From 8c9d60065e7c547c722dad304a0958f871e459f2 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 27 Jul 2022 09:12:20 +0900 Subject: [PATCH 74/93] modules/silicon: add asap7 notebook --- .../scripts/build/notebooks/asap7.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 modules/silicon_design/scripts/build/notebooks/asap7.md diff --git a/modules/silicon_design/scripts/build/notebooks/asap7.md b/modules/silicon_design/scripts/build/notebooks/asap7.md new file mode 100644 index 00000000..de82f7d9 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/asap7.md @@ -0,0 +1,65 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.14.0 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# OpenROAD Flow ASAP7 Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to run a test design thru OpenROAD flow targetting the ASAP7 process node + + +## Run OpenROAD flow + +[OpenROAD Flow](https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts) is a full RTL-to-GDS flow built entirely on open-source tools. The project aims for automated, no-human-in-the-loop digital circuit design with 24-hour turnaround time. + +```python jupyter={"outputs_hidden": true} tags=[] +!cd /OpenROAD-flow-scripts/flow && make SHELL=/bin/bash DESIGN_CONFIG=./designs/asap7/gcd/config.mk +``` + + +## Dump flow metrics + + +```python tags=[] +import pathlib +import json +import pandas as pd +from IPython.display import display + +pd.set_option('display.max_rows', None) +metrics = sorted(pathlib.Path('/OpenROAD-flow-scripts/flow').glob('reports/asap7/*/base/metrics.json')) +with metrics[-1].open() as f: + data = json.load(f) + df = pd.DataFrame.from_records([data]).transpose() +df +``` + +## Display layout with GDSII Tool Kit + +[Gdstk](https://github.com/heitzmann/gdstk) (GDSII Tool Kit) is a C++/Python library for creation and manipulation of GDSII and OASIS files. + +```python tags=[] +import pathlib +import gdstk +from IPython.display import SVG + +gds = sorted(pathlib.Path('/OpenROAD-flow-scripts/flow').glob('results/asap7/*/base/6_final.gds')) +library = gdstk.read_gds(gds[-1]) +top_cells = library.top_level() +top_cells[0].write_svg('layout.svg') +SVG('layout.svg') +``` From a1fcffe97a2528a79ac676c6192fabd70502bcbd Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 27 Jul 2022 09:12:35 +0900 Subject: [PATCH 75/93] modules/silicon/notebook: fix inverter --- modules/silicon_design/scripts/build/notebooks/inverter.md | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/notebooks/inverter.md b/modules/silicon_design/scripts/build/notebooks/inverter.md index 473edbc1..5d9585a0 100644 --- a/modules/silicon_design/scripts/build/notebooks/inverter.md +++ b/modules/silicon_design/scripts/build/notebooks/inverter.md @@ -45,7 +45,6 @@ endmodule See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. ```python -<<<<<<< HEAD %%bash -c 'cat > config.tcl; tclsh config.tcl' set ::env(DESIGN_NAME) inverter From 02f118b41fe28108f4be552a6ba20e60d7ecf6ec Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Wed, 27 Jul 2022 12:06:14 +0900 Subject: [PATCH 76/93] modules/silicon/notebooks/asap7: genmetrics --- modules/silicon_design/scripts/build/notebooks/asap7.md | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/silicon_design/scripts/build/notebooks/asap7.md b/modules/silicon_design/scripts/build/notebooks/asap7.md index de82f7d9..81d26a06 100644 --- a/modules/silicon_design/scripts/build/notebooks/asap7.md +++ b/modules/silicon_design/scripts/build/notebooks/asap7.md @@ -35,6 +35,7 @@ This notebook shows how to run a test design thru OpenROAD flow targetting the A ```python tags=[] +!python /OpenROAD-flow-scripts/flow/util/genMetrics.py import pathlib import json import pandas as pd From 2a7bc786ca991f1581fb21c81d1d9245b7182ae5 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 15:00:01 +0900 Subject: [PATCH 77/93] modules/silicon: reorganize notebooks each experiment in a separate directory with separate tuning and visualization notebooks. --- modules/silicon_design/README.md | 4 +- .../scripts/build/cloudbuild.yaml | 24 +- .../scripts/build/notebooks/asap7.md | 66 ----- .../notebooks/{ => inverter}/inverter.md | 2 +- .../{inverter.svg => inverter/layout.svg} | 0 .../scripts/build/notebooks/{ => lut}/lut.md | 0 .../{tuning-lut.md => lut/tuning.md} | 0 .../{minimal/minimal_job.md => math/math.md} | 0 .../minimal_tuning.md => math/tuning.md} | 0 .../build/notebooks/{ => openram}/openram.md | 0 .../scripts/build/notebooks/serv.md | 154 ---------- .../build/notebooks/{ => serv}/subservient.md | 0 .../build/notebooks/{ => serv}/tuning.md | 2 +- .../scripts/build/notebooks/xls/grid.md | 113 ++++++++ .../scripts/build/notebooks/xls/tuning.md | 116 ++++++++ .../build/notebooks/xls/visualization.md | 200 +++++++++++++ .../build/notebooks/xls/xls-crc32-asap7.md | 262 ++++++++++++++++++ .../{xls.md => xls/xls-crc32-sky130.md} | 0 18 files changed, 708 insertions(+), 235 deletions(-) delete mode 100644 modules/silicon_design/scripts/build/notebooks/asap7.md rename modules/silicon_design/scripts/build/notebooks/{ => inverter}/inverter.md (98%) rename modules/silicon_design/scripts/build/notebooks/{inverter.svg => inverter/layout.svg} (100%) rename modules/silicon_design/scripts/build/notebooks/{ => lut}/lut.md (100%) rename modules/silicon_design/scripts/build/notebooks/{tuning-lut.md => lut/tuning.md} (100%) rename modules/silicon_design/scripts/build/notebooks/{minimal/minimal_job.md => math/math.md} (100%) rename modules/silicon_design/scripts/build/notebooks/{minimal/minimal_tuning.md => math/tuning.md} (100%) rename modules/silicon_design/scripts/build/notebooks/{ => openram}/openram.md (100%) delete mode 100644 modules/silicon_design/scripts/build/notebooks/serv.md rename modules/silicon_design/scripts/build/notebooks/{ => serv}/subservient.md (100%) rename modules/silicon_design/scripts/build/notebooks/{ => serv}/tuning.md (99%) create mode 100644 modules/silicon_design/scripts/build/notebooks/xls/grid.md create mode 100644 modules/silicon_design/scripts/build/notebooks/xls/tuning.md create mode 100644 modules/silicon_design/scripts/build/notebooks/xls/visualization.md create mode 100644 modules/silicon_design/scripts/build/notebooks/xls/xls-crc32-asap7.md rename modules/silicon_design/scripts/build/notebooks/{xls.md => xls/xls-crc32-sky130.md} (100%) diff --git a/modules/silicon_design/README.md b/modules/silicon_design/README.md index 0c1c4c62..5368c0e0 100644 --- a/modules/silicon_design/README.md +++ b/modules/silicon_design/README.md @@ -10,9 +10,9 @@ This RAD Lab module provides a managed environment for custom silicon design usi ## Samples notebooks -- [Inverter](scripts/build/notebooks/inverter.md) +- [Inverter](scripts/build/notebooks/inverter/experiment.md) -![gds render](scripts/build/notebooks/inverter.svg) +![gds render](scripts/build/notebooks/inverter/layout.svg) ## GCP Products/Services diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 5f288fbf..54a7ed4e 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -27,15 +27,23 @@ options: logging: CLOUD_LOGGING_ONLY steps: - id: 'notebooks-build' - name: 'python' + name: 'gcr.io/cloud-builders/gcloud' entrypoint: '/bin/bash' args: - '-c' - |- - python3 -m venv env-jupytext/ - env-jupytext/bin/python -m pip install jupytext - env-jupytext/bin/jupytext --to notebook scripts/build/notebooks/*.md - echo 'gsutil cp gs://$_NOTEBOOKS_BUCKET/*.ipynb /home/jupyter/' > scripts/build/notebooks/copy-notebooks.sh + python3 -m venv env/ + env/bin/python -m pip install jupytext + env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md + echo "gsutil rsync -r -x '.*\.sh' -x '.*\.json' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh + gsutil rsync -r -x '.*\.md' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ +artifacts: + objects: + location: gs://$_NOTEBOOKS_BUCKET/ + paths: + - 'scripts/build/notebooks/copy-notebooks.sh' + - 'scripts/build/notebooks/*.ipynb' + waitFor: ['-'] - id: 'compute-image-build' name: 'gcr.io/cloud-builders/gcloud' @@ -68,9 +76,3 @@ steps: images: - '$_CONTAINER_IMAGE:$_IMAGE_TAG' - '$_CONTAINER_IMAGE:latest' -artifacts: - objects: - location: gs://$_NOTEBOOKS_BUCKET/ - paths: - - 'scripts/build/notebooks/copy-notebooks.sh' - - 'scripts/build/notebooks/*.ipynb' diff --git a/modules/silicon_design/scripts/build/notebooks/asap7.md b/modules/silicon_design/scripts/build/notebooks/asap7.md deleted file mode 100644 index 81d26a06..00000000 --- a/modules/silicon_design/scripts/build/notebooks/asap7.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -jupyter: - jupytext: - text_representation: - extension: .md - format_name: markdown - format_version: '1.3' - jupytext_version: 1.14.0 - kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# OpenROAD Flow ASAP7 Sample - -``` -Copyright 2022 Google LLC. -SPDX-License-Identifier: Apache-2.0 -``` - -This notebook shows how to run a test design thru OpenROAD flow targetting the ASAP7 process node - - -## Run OpenROAD flow - -[OpenROAD Flow](https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts) is a full RTL-to-GDS flow built entirely on open-source tools. The project aims for automated, no-human-in-the-loop digital circuit design with 24-hour turnaround time. - -```python jupyter={"outputs_hidden": true} tags=[] -!cd /OpenROAD-flow-scripts/flow && make SHELL=/bin/bash DESIGN_CONFIG=./designs/asap7/gcd/config.mk -``` - - -## Dump flow metrics - - -```python tags=[] -!python /OpenROAD-flow-scripts/flow/util/genMetrics.py -import pathlib -import json -import pandas as pd -from IPython.display import display - -pd.set_option('display.max_rows', None) -metrics = sorted(pathlib.Path('/OpenROAD-flow-scripts/flow').glob('reports/asap7/*/base/metrics.json')) -with metrics[-1].open() as f: - data = json.load(f) - df = pd.DataFrame.from_records([data]).transpose() -df -``` - -## Display layout with GDSII Tool Kit - -[Gdstk](https://github.com/heitzmann/gdstk) (GDSII Tool Kit) is a C++/Python library for creation and manipulation of GDSII and OASIS files. - -```python tags=[] -import pathlib -import gdstk -from IPython.display import SVG - -gds = sorted(pathlib.Path('/OpenROAD-flow-scripts/flow').glob('results/asap7/*/base/6_final.gds')) -library = gdstk.read_gds(gds[-1]) -top_cells = library.top_level() -top_cells[0].write_svg('layout.svg') -SVG('layout.svg') -``` diff --git a/modules/silicon_design/scripts/build/notebooks/inverter.md b/modules/silicon_design/scripts/build/notebooks/inverter/inverter.md similarity index 98% rename from modules/silicon_design/scripts/build/notebooks/inverter.md rename to modules/silicon_design/scripts/build/notebooks/inverter/inverter.md index 5d9585a0..abe63d2d 100644 --- a/modules/silicon_design/scripts/build/notebooks/inverter.md +++ b/modules/silicon_design/scripts/build/notebooks/inverter/inverter.md @@ -84,7 +84,7 @@ import scrapbook as sb gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] library = gdstk.read_gds(gds_path) top_cells = library.top_level() -svg_path = pathlib.Path(run_path) / 'inverter.svg' +svg_path = pathlib.Path(run_path) / 'layout.svg' top_cells[0].write_svg(svg_path) sb.glue('layout', IPython.display.SVG(svg_path), 'display', display=True) ``` diff --git a/modules/silicon_design/scripts/build/notebooks/inverter.svg b/modules/silicon_design/scripts/build/notebooks/inverter/layout.svg similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/inverter.svg rename to modules/silicon_design/scripts/build/notebooks/inverter/layout.svg diff --git a/modules/silicon_design/scripts/build/notebooks/lut.md b/modules/silicon_design/scripts/build/notebooks/lut/lut.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/lut.md rename to modules/silicon_design/scripts/build/notebooks/lut/lut.md diff --git a/modules/silicon_design/scripts/build/notebooks/tuning-lut.md b/modules/silicon_design/scripts/build/notebooks/lut/tuning.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/tuning-lut.md rename to modules/silicon_design/scripts/build/notebooks/lut/tuning.md diff --git a/modules/silicon_design/scripts/build/notebooks/minimal/minimal_job.md b/modules/silicon_design/scripts/build/notebooks/math/math.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/minimal/minimal_job.md rename to modules/silicon_design/scripts/build/notebooks/math/math.md diff --git a/modules/silicon_design/scripts/build/notebooks/minimal/minimal_tuning.md b/modules/silicon_design/scripts/build/notebooks/math/tuning.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/minimal/minimal_tuning.md rename to modules/silicon_design/scripts/build/notebooks/math/tuning.md diff --git a/modules/silicon_design/scripts/build/notebooks/openram.md b/modules/silicon_design/scripts/build/notebooks/openram/openram.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/openram.md rename to modules/silicon_design/scripts/build/notebooks/openram/openram.md diff --git a/modules/silicon_design/scripts/build/notebooks/serv.md b/modules/silicon_design/scripts/build/notebooks/serv.md deleted file mode 100644 index 3f79ff93..00000000 --- a/modules/silicon_design/scripts/build/notebooks/serv.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -jupyter: - jupytext: - text_representation: - extension: .md - format_name: markdown - format_version: '1.3' - jupytext_version: 1.13.8 - kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - - -# Serv Sample - -``` -Copyright 2022 Google LLC. -SPDX-License-Identifier: Apache-2.0 -``` - -This notebook shows how to run the [SERV](https://github.com/olofk/serv) RISC-V core design thru an end-to-end RTL to GDSII flow targetting the [SKY130](https://github.com/google/skywater-pdk/) process node. - - -## Define flow parameters - -```python tags=["parameters"] -die_width = 300 -target_density = 80 -run_path = 'runs/serv' -``` - -## Get SERV RTL - -```python -!git clone https://github.com/olofk/serv -``` -## Write flow configuration - -See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. - -```python -%%writefile config.tcl -set ::env(DESIGN_NAME) serv_top - -set script_dir [file dirname [file normalize [info script]]] -set ::env(VERILOG_FILES) "$script_dir/serv/rtl/*.v" - -set ::env(CLOCK_PORT) "click" - -set ::env(FP_SIZING) "absolute" -set ::env(DIE_AREA) "0 0 $::env(DIE_WIDTH) $::env(DIE_WIDTH)" -set ::env(PL_TARGET_DENSITY) [expr {$::env(TARGET_DENSITY) / 100.0}] -``` - -## Run OpenLane flow - -[OpenLane](https://github.com/The-OpenROAD-Project/OpenLane) is an automated RTL to GDSII flow based on several components including [OpenROAD](https://github.com/The-OpenROAD-Project/OpenROAD), [Yosys](https://github.com/YosysHQ/yosys), Magic, Netgen, Fault, CVC, SPEF-Extractor, CU-GR, Klayout and a number of custom scripts for design exploration and optimization. - -```python tags=[] -#papermill_description=RunningOpenLaneFlow -%env DIE_WIDTH={die_width} -%env TARGET_DENSITY={target_density} -!flow.tcl -design . -run_path {run_path} -``` - -## Display layout - -Use [GDSII Tool Kit](https://github.com/heitzmann/gdstk) and [CairoSVG](https://cairosvg.org/) to convert the resulting GDSII file to PNG. - -```python -#papermill_description=RenderingGDS -import pathlib -import gdstk -import cairosvg - -import IPython.display -import scrapbook as sb - -gds_path = sorted(pathlib.Path(run_path).glob('*/results/final/gds/*.gds'))[-1] -library = gdstk.read_gds(gds_path) -top_cells = library.top_level() -svg_path = pathlib.Path(run_path) / 'serv.svg' -top_cells[0].write_svg(svg_path) -png_path = pathlib.Path(run_path) / 'serv.png' - -cairosvg.svg2png(url=str(svg_path), write_to=str(png_path)) -sb.glue('layout', IPython.display.Image(png_path), 'display', display=True) -``` - -## Dump flow report - -See [OpenLane Datapoint Definitions](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/regression_results/datapoint_definitions.md) for the description of the report columns. - -```python tags=[] -#papermill_description=DumpingReport -import pandas as pd -import pathlib -import scrapbook as sb - -final_summary_report = sorted(pathlib.Path(run_path).glob('*/reports/final_summary_report.csv'))[-1] -df = pd.read_csv(final_summary_report) -pd.set_option('display.max_rows', None) -sb.glue('summary', df, 'pandas') -df.transpose() -``` - -## Extract power metrics - -Build a pandas dataframe with area, density and power consumption. - -```python tags=[] -#papermill_description=ExtractingMetrics -import scrapbook as sb - -def get_power(sta_power_report): - with sta_power_report.open() as f: - for l in f.readlines(): - if l.startswith('Total'): - return float(l.split(' ')[-2]) - -def area_density_ppa(): - for report in sorted(pathlib.Path(run_path).glob('*/reports')): - sta_power_report = report / 'routing/23-parasitics_sta.power.rpt' - final_summary_report = report / 'final_summary_report.csv' - if final_summary_report.exists() and sta_power_report.exists(): - df = pd.read_csv(final_summary_report) - power = get_power(sta_power_report) - yield (df['DIEAREA_mm^2'][0], df['PL_TARGET_DENSITY'][0], power) - -df = pd.DataFrame(area_density_ppa(), columns=('DIEAREA_mm^2', 'PL_TARGET_DENSITY', 'TOTAL_POWER')) -sb.glue('metrics', df, 'pandas') -(df.style.hide_index() - .format({'area': '{:.8f}', 'density': '{:.2%}', 'power': '{:.8f}'}) - .bar(subset=['TOTAL_POWER'], color='pink') - .background_gradient(subset=['PL_TARGET_DENSITY'], cmap='Greens') - .bar(color='lightblue', vmin=0.001, subset=['DIEAREA_mm^2'])) -``` - -Report metrics for hyper-parameters tuning. - -```python -#papermill_description=ReportingMetrics -import hypertune - -total_power = df['TOTAL_POWER'][0] * 1e6 -print('reporting metric:', 'total_power', total_power) -hpt = hypertune.HyperTune() -hpt.report_hyperparameter_tuning_metric( - hyperparameter_metric_tag='total_power', - metric_value=total_power, -) -``` diff --git a/modules/silicon_design/scripts/build/notebooks/subservient.md b/modules/silicon_design/scripts/build/notebooks/serv/subservient.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/subservient.md rename to modules/silicon_design/scripts/build/notebooks/serv/subservient.md diff --git a/modules/silicon_design/scripts/build/notebooks/tuning.md b/modules/silicon_design/scripts/build/notebooks/serv/tuning.md similarity index 99% rename from modules/silicon_design/scripts/build/notebooks/tuning.md rename to modules/silicon_design/scripts/build/notebooks/serv/tuning.md index 25e95aa9..86eb86a2 100644 --- a/modules/silicon_design/scripts/build/notebooks/tuning.md +++ b/modules/silicon_design/scripts/build/notebooks/serv/tuning.md @@ -78,7 +78,7 @@ worker_pool_specs = [{ 'args': ['/usr/local/bin/papermill-launcher', f'gs://{staging_bucket}/{staging_dir}/{notebook}', f'$AIP_MODEL_DIR/{prefix}_out.ipynb', - '--run_dir=/tmp'] + '--run_path=/tmp'] } }] custom_job = aiplatform.CustomJob(display_name=f'{prefix}-custom-job', diff --git a/modules/silicon_design/scripts/build/notebooks/xls/grid.md b/modules/silicon_design/scripts/build/notebooks/xls/grid.md new file mode 100644 index 00000000..d4787818 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/xls/grid.md @@ -0,0 +1,113 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.14.1 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Parameter Tuning Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to leverage [Vertex AI hyperparameter tuning](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) in order to find the right flow parameters value to optimize a given metric. + + + +## Define project parameters + + +```python tags=["parameters"] +import pathlib +import datetime + +worker_image = 'us-east4-docker.pkg.dev/catx-demo-radlab/radlab-silicon-tuning-containers/silicon-design-ubuntu-2004:202207270308' +project = 'catx-demo-radlab' +location = 'us-east4' +machine_type = 'n1-standard-32' +notebook = pathlib.Path('asap7.ipynb') +now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') +prefix = notebook.stem +staging_bucket = f'catx-demo-radlab-staging-{location}' +staging_dir = f'{prefix}-tuning-{now}' +staging_dir +``` + +## Stage the notebook for the experiment + +```python tags=[] +!gsutil mb -l {location} gs://{staging_bucket} +!gsutil cp {notebook} gs://{staging_bucket}/{staging_dir}/{notebook} +``` + +## Create Parameters and Metrics specs + +We want to find the best value for *target density* and *die area* in order optimize *total power* consumption. + +Those keys map to the [parameters](https://papermill.readthedocs.io/en/latest/usage-parameterize.html) and [metrics](https://github.com/GoogleCloudPlatform/cloudml-hypertune) advertised by the notebook. + +```python tags=[] +from google.cloud.aiplatform import hyperparameter_tuning as hpt + +parameter_spec = { + 'pipeline_stages': hpt.IntegerParameterSpec(min=1, max=16, scale='linear'), +} +metric_spec={'slack': 'maximize'} +``` + +## Create Custom Job spec + +```python tags=[] +from google.cloud import aiplatform +import pathlib + +worker_pool_specs = [{ + 'machine_spec': { + 'machine_type': machine_type, + }, + 'replica_count': 1, + 'container_spec': { + 'image_uri': worker_image, + 'args': ['/usr/local/bin/papermill-launcher', + f'gs://{staging_bucket}/{staging_dir}/{notebook}', + f'$AIP_MODEL_DIR/{prefix}_out.ipynb', + '--run_dirs=/tmp'] + } +}] +custom_job = aiplatform.CustomJob(display_name=f'{prefix}-custom-job', + worker_pool_specs=worker_pool_specs, + staging_bucket=staging_bucket, + base_output_dir=f'gs://{staging_bucket}/{staging_dir}') +``` + +## Run Hyperparameter tuning job + +```python tags=[] +from google.cloud import aiplatform +parameters_count = len(parameter_spec.keys()) +metrics_count = len(metric_spec.keys()) +max_trial_count = 16 +parallel_trial_count = 16 + +hpt_job = aiplatform.HyperparameterTuningJob( + display_name=f'{prefix}-tuning-job', + custom_job=custom_job, + metric_spec=metric_spec, + parameter_spec=parameter_spec, + max_trial_count=max_trial_count, + parallel_trial_count=parallel_trial_count, + max_failed_trial_count=max_trial_count, + location=location, + search_algorithm='grid') +hpt_job.run(sync=True) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/xls/tuning.md b/modules/silicon_design/scripts/build/notebooks/xls/tuning.md new file mode 100644 index 00000000..2e9eebbd --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/xls/tuning.md @@ -0,0 +1,116 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.14.1 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Parameter Tuning Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to leverage [Vertex AI hyperparameter tuning](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) in order to find the right flow parameters value to optimize a given metric. + + + +## Define project parameters + + +```python tags=["parameters"] +import pathlib +import datetime + +worker_image = 'us-east4-docker.pkg.dev/catx-demo-radlab/radlab-silicon-tuning-containers/silicon-design-ubuntu-2004:202207270308' +staging_bucket = 'catx-demo-radlab-staging' +project = 'catx-demo-radlab' +location = 'us-east4' +machine_type = 'n1-standard-32' +notebook = pathlib.Path('asap7.ipynb') +now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') +prefix = notebook.stem +staging_dir = f'{prefix}-tuning-{now}' +staging_dir +``` + +## Stage the notebook for the experiment + +```python tags=[] +#!gsutil rm -r gs://{staging_bucket}/{staging_dir} +!gsutil cp {notebook} gs://{staging_bucket}/{staging_dir}/{notebook} +``` + +## Create Parameters and Metrics specs + +We want to find the best value for *target density* and *die area* in order optimize *total power* consumption. + +Those keys map to the [parameters](https://papermill.readthedocs.io/en/latest/usage-parameterize.html) and [metrics](https://github.com/GoogleCloudPlatform/cloudml-hypertune) advertised by the notebook. + +```python tags=[] +from google.cloud.aiplatform import hyperparameter_tuning as hpt + +parameter_spec = { + 'target_density': hpt.DoubleParameterSpec(min=0.1, max=1.0, scale='linear'), + 'die_width': hpt.IntegerParameterSpec(min=1, max=100, scale='linear'), + 'crc32_rounds': hpt.IntegerParameterSpec(min=1, max=32, scale='linear'), + 'pipeline_stages': hpt.IntegerParameterSpec(min=1, max=16, scale='linear'), +} + +metric_spec={'slack': 'maximize'} +``` + +## Create Custom Job spec + +```python tags=[] +from google.cloud import aiplatform +import pathlib + +worker_pool_specs = [{ + 'machine_spec': { + 'machine_type': machine_type, + }, + 'replica_count': 1, + 'container_spec': { + 'image_uri': worker_image, + 'args': ['/usr/local/bin/papermill-launcher', + f'gs://{staging_bucket}/{staging_dir}/{notebook}', + f'$AIP_MODEL_DIR/{prefix}_out.ipynb', + '--runs_dir=/tmp'] + } +}] +custom_job = aiplatform.CustomJob(display_name=f'{prefix}-custom-job', + worker_pool_specs=worker_pool_specs, + staging_bucket=staging_bucket, + base_output_dir=f'gs://{staging_bucket}/{staging_dir}') +``` + +## Run Hyperparameter tuning job + +```python tags=[] +from google.cloud import aiplatform +parameters_count = len(parameter_spec.keys()) +metrics_count = len(metric_spec.keys()) +max_trial_count = 100 * parameters_count * metrics_count +parallel_trial_count = int(max_trial_count / 20) + +hpt_job = aiplatform.HyperparameterTuningJob( + display_name=f'{prefix}-tuning-job', + custom_job=custom_job, + metric_spec=metric_spec, + parameter_spec=parameter_spec, + max_trial_count=max_trial_count, + parallel_trial_count=parallel_trial_count, + max_failed_trial_count=max_trial_count, + location=location) +hpt_job.run(sync=True) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/xls/visualization.md b/modules/silicon_design/scripts/build/notebooks/xls/visualization.md new file mode 100644 index 00000000..08f92507 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/xls/visualization.md @@ -0,0 +1,200 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.14.1 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + + +# Parameter Tuning Analysis + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to analyse data from [Vertex AI hyperparameter tuning](https://cloud.google.com/vertex-ai/docs/training/hyperparameter-tuning-overview) jobs. + + + +## Define project parameters + + +```python tags=["parameters"] +import pathlib + +location = 'us-east4' +#staging_bucket = 'catx-demo-radlab-staging' +staging_bucket = f'catx-demo-radlab-staging-{location}' + +notebook = pathlib.Path('asap7.ipynb') +prefix = notebook.stem +staging_dir = 'asap7-tuning-20220804141156' +``` + +## Fetch notebooks for all study trials + +```python +last_trial_id = 16 + +import pathlib +from google.cloud import storage +import tqdm + +local_dir = pathlib.Path(staging_dir) +local_dir.mkdir(exist_ok=True, parents=True) + +client = storage.Client() +bucket = client.bucket(staging_bucket) +for i in tqdm.tqdm(range(1, last_trial_id+1)): + src = bucket.blob(f'{staging_dir}/{i}/model/{prefix}_out.ipynb') + dst = local_dir / f'{prefix}_out_{i}.ipynb' + with dst.open('wb') as f: + src.download_to_file(f) +``` + +## Extract metrics from notebooks + +```python tags=[] +import scrapbook as sb +books = sb.read_notebooks(staging_dir) +``` + +```python + import pathlib +import math + +import pandas as pd +import tqdm +def metrics(): + for b in tqdm.tqdm(books): + trial_id = int(pathlib.Path(books[b].filename).stem.split('_')[-1]) + params = books[b].parameters + die_width_u = 90 #float(params.die_width) + target_density = 0.6 #float(params.target_density) + crc32_rounds = 8 #int(params.crc32_rounds) + pipeline_stages = int(params.pipeline_stages) + if ('ppa' in books[b].scraps) and not books[b].scraps['ppa'].data.empty: + metrics = books[b].scraps['metrics'].data + ppa = books[b].scraps['ppa'].data + yield trial_id, crc32_rounds, pipeline_stages, die_width_u, target_density, metrics['globalroute__timing__clock__slack'][0], metrics['finish__design__instance__utilization'][0] / 100.0, ppa['slack'][0], metrics['globalroute__timing__clock__slack'][0], metrics['finish__timing__setup__ws'][0], metrics['finish__timing__cp_delay'][0], ppa['power'][0] + else: + yield trial_id, crc32_rounds, pipeline_stages, die_width_u, target_density, math.nan, math.nan, math.nan, math.nan + +df = pd.DataFrame.from_records(metrics(), columns=['trial_id', 'crc32_rounds', 'pipeline_stages', 'die_width^2', 'target_density', 'finish__design__instance__area', 'finish__design__instance__utilization', 'critical_path_slack', 'globalroute__timing__clock__slack', 'finish__timing__setup__ws', 'finish__timing__cp_delay', 'power'], index='trial_id').sort_index() +df.to_csv(f'{prefix}.csv') +(df.sort_values(['pipeline_stages', 'finish__timing__setup__ws'], ascending=[True, True]) + .style + .format({'finish__design__instance__area': '{:.8f}', 'finish__design__instance__utilization': '{:.2%}', 'finish__timing__setup__ws': '{:.6f}'}) + .background_gradient(subset=['crc32_rounds'], cmap='Blues') + .background_gradient(subset=['pipeline_stages'], cmap='Oranges') + .bar(subset=['critical_path_slack'], color='pink') + .bar(subset=['globalroute__timing__clock__slack'], color='pink') + .bar(subset=['finish__timing__setup__ws'], color='pink') + .bar(subset=['finish__timing__cp_delay'], color='pink') + .bar(subset=['power'], color='pink') + .background_gradient(subset=['finish__design__instance__utilization'], cmap='Greens') + .bar(color='lightblue', vmin=0.001, subset=['finish__design__instance__area'])) +``` + +## Plot experiments + +```python +ax = pd.plotting.scatter_matrix(df, figsize=(30, 30)) +plt.savefig(f'{prefix}_matrix.png') +ax +``` + +```python +import matplotlib.colors +from matplotlib import pyplot as plt + +cool = matplotlib.colormaps['cool'] +cool.set_bad(color='none') +ax = df.plot.scatter(x='pipeline_stages', y='finish__timing__setup__ws', c='finish__timing__cp_delay', + cmap=cool, s=50, sharex=False, alpha=1.0, edgecolor='black') +plt.savefig(f'{prefix}.png') +ax +``` + +```python tags=[] +from matplotlib import pyplot as plt +from matplotlib import animation +from matplotlib import cm + +from tqdm import tqdm +from IPython.display import Image + +min_metric = df['slack'].min() +max_metric = df['slack'].max() +fig, ax = plt.subplots() +fig.colorbar(cm.ScalarMappable(matplotlib.colors.Normalize(min_metric, max_metric), cmap=cool), + label='slack', + ax=ax) +ax.set_xlabel('area') +ax.set_ylabel('pipelines') +plt.close(fig) # hide current figure + +def generate_frames(): + for n in range(20, last_trial_id, 20): + batch = df[0:n] + yield [ax.scatter( + batch['area'], batch['pipelines'], c=batch['slack'], + s=50, vmin=min_metric, vmax=max_metric, cmap=cool, edgecolor='black')] + +frames = list(generate_frames()) +anim = animation.ArtistAnimation(fig, frames) +anim.save(f'{prefix}.gif', writer=animation.PillowWriter(fps=10)) +Image(f'{prefix}.gif') +``` + +## Render chip layouts + +```python +from matplotlib import pyplot as plt +from matplotlib import animation +from matplotlib import cm +from tqdm import tqdm +from IPython.display import Image +from time import sleep +import matplotlib.colors +import io +import base64 +import PIL +import PIL.ImageOps +import PIL.ImageDraw +import numpy as np + +def trial_images(): + for trial_id, trial in df.dropna().sort_values(['trial_id'], ascending=[True]).iterrows(): + book = books[f'asap7_out_{trial_id}'] + layout = book.scraps['layout'] + f = io.BytesIO(base64.b64decode(layout.display.data['image/png'])) + img = PIL.Image.open(f)#.convert('L') + yield trial_id, img + +size = (500, 500) +fig, ax = plt.subplots(figsize=size) + +def generate_frames(): + for trial_id, img in tqdm(trial_images()): + img = img.resize(size) + d = PIL.ImageDraw.Draw(img) + d.text((10, 10), f'CRC32_ASAP7_{trial_id}', fill=(255, 255, 255, 255)) + yield img + +frames = list(generate_frames()) +frames[0].save(f'allthe{prefix}.gif', save_all=True, loop=0, append_images=frames[1:]) +Image(f'allthe{prefix}.gif') +``` + +```python +len(df.dropna()) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/xls/xls-crc32-asap7.md b/modules/silicon_design/scripts/build/notebooks/xls/xls-crc32-asap7.md new file mode 100644 index 00000000..dadf8903 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/xls/xls-crc32-asap7.md @@ -0,0 +1,262 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.14.1 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# OpenROAD Flow ASAP7 Sample + +``` +Copyright 2022 Google LLC. +SPDX-License-Identifier: Apache-2.0 +``` + +This notebook shows how to run a test design thru OpenROAD flow targetting the ASAP7 process node + + +## Define flow parameters + +```python tags=["parameters"] +import datetime + +die_width = 30 +core_padding = 2 +target_density = 0.90 +crc32_rounds = 8 +pipeline_stages = 4 +clock_period = 200 +runs_dir = 'runs' +``` + +## Timestamp run + +```python +now = datetime.datetime.now().strftime('%Y%m%d%H%M%S') +run_path = f'{runs_dir}/{now}' +run_path +``` + + +## Write and test DSLX module + +The CRC computation is written using the [DSLX](https://google.github.io/xls/dslx_reference/) HLS, a domain specific, dataflow-oriented functional language used to build hardware w/ a Rust-like syntax. + + +```bash colab={"base_uri": "https://localhost:8080/"} id="JKGxScUtoV4E" outputId="b9359a05-fa7f-4366-ecf8-40138acb11f1" magic_args="-c 'sed s/__CRC32_ROUNDS__/{crc32_rounds}/ > crc32.x; interpreter_main crc32.x'" +// Performs a table-less crc32 of the input data as in Hacker's Delight: +// https://github.com/hcs0/Hackers-Delight/blob/master/crc.c.txt (roughly flavor b) + +fn crc32_one_byte(byte: u8, polynomial: u32, crc: u32) -> u32 { + let crc = crc ^ (byte as u32); + // __CRC32_ROUNDS__ rounds of updates. + for (i, crc): (u32, u32) in range(u32:0, u32:__CRC32_ROUNDS__) { + let mask = -(crc & u32:1); + (crc >> u32:1) ^ (polynomial & mask) + }(crc) +} + +fn main(message: u8) -> u32 { + crc32_one_byte(message, u32:0xEDB88320, u32:-1) ^ u32:-1 +} +``` + + +## Generate IR and Verilog + +XLS can generate combinational or pipelined version of a given design. + + +```python colab={"base_uri": "https://localhost:8080/"} id="YMTh7WB6oxeW" outputId="a4e9d2f2-69e3-47e9-cad6-e1b89124553b" tags=[] +!ir_converter_main --top main crc32.x > crc32.ir +!opt_main crc32.ir > crc32_opt.ir +!codegen_main --generator=pipeline --delay_model="asap7" --module_name="crc32" --pipeline_stages={pipeline_stages} crc32_opt.ir > crc32.v +!cat crc32.v +``` + +## Configure OpenROAD Flow + +```python +%%writefile config.mk + +export PLATFORM = asap7 +export DESIGN_NAME = crc32 +export VERILOG_FILES = ${PWD}/crc32.v +export SDC_FILE = ${PWD}/constraint.sdc +export DIE_AREA = 0 0 $(DIE_WIDTH) $(DIE_WIDTH) +export CORE_AREA = $(CORE_PADDING) $(CORE_PADDING) $(CORE_WIDTH) $(CORE_WIDTH) + +export PLACE_DENSITY = $(TARGET_DENSITY) +``` + +```bash magic_args="-c \"sed s/__CLOCK_PERIOD__/{clock_period}/ | tee constraint.sdc\"" + +set clk_name clk +set clk_port_name clk +set clk_period __CLOCK_PERIOD__ +set clk_io_pct 0.1 + +set clk_port [get_ports $clk_port_name] + +create_clock -name $clk_name -period $clk_period $clk_port + +set non_clock_inputs [lsearch -inline -all -not -exact [all_inputs] $clk_port] + +set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs +set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs] +``` + +## Run OpenROAD Flow + +[OpenROAD Flow](https://github.com/The-OpenROAD-Project/OpenROAD-flow-scripts) is a full RTL-to-GDS flow built entirely on open-source tools. The project aims for automated, no-human-in-the-loop digital circuit design with 24-hour turnaround time. + +```python jupyter={"outputs_hidden": true} tags=[] +import pathlib +import os + +work_dir = pathlib.Path(run_path) +work_dir.mkdir(parents=True, exist_ok=True) +work_dir = str(work_dir.resolve()) +pwd = os.getcwd() + +!make -C /OpenROAD-flow-scripts/flow \ + SHELL=/bin/bash \ + FLOW_HOME=/OpenROAD-flow-scripts/flow \ + PLATFORM_DIR=/OpenROAD-flow-scripts/flow/platforms/asap7 \ + DESIGN_CONFIG={pwd}/config.mk \ + DIE_WIDTH={die_width} \ + CORE_PADDING={core_padding} \ + CORE_WIDTH={float(die_width) - float(core_padding)} \ + TARGET_DENSITY={target_density} \ + WORK_HOME={work_dir} +``` + + +## Dump flow metrics + + +```python tags=[] +import pathlib + +flow_path = pathlib.Path(run_path).resolve() +!PLATFORM_DIR=/OpenROAD-flow-scripts/flow/platforms python /OpenROAD-flow-scripts/flow/util/genMetrics.py --flowPath {flow_path} --design crc32 --platform asap7 --output {flow_path}/metrics.json + +import json +import pandas as pd +from IPython.display import display +import scrapbook as sb + +pd.set_option('display.max_rows', None) +metrics = pathlib.Path(run_path) / 'metrics.json' +with metrics.open() as f: + data = json.load(f) + df = pd.DataFrame.from_records([data]) +sb.glue('metrics', df, 'pandas') +df.transpose().rename(columns={0: 'metrics'}) +``` + +## Display layout with KLayout + +```python +%%writefile /OpenROAD-flow-scripts/flow/util/gallery.json +[ + { + "name" : "final", + "layout_file": "6_final.def", + "min_hierarchy": 0, + "max_hierarchy": 1, + "x_resolution": 500, + "y_resolution": 500, + "hide_layers": false + }, + { + "name" : "final_no_power", + "layout_file": "6_final_no_power.def", + "min_hierarchy": 0, + "max_hierarchy": 1, + "x_resolution": 500, + "y_resolution": 500, + "hide_layers": false + } +] +``` + +```python +!make -C /OpenROAD-flow-scripts/flow/ \ + SHELL=/bin/bash \ + FLOW_HOME=/OpenROAD-flow-scripts/flow \ + PLATFORM_DIR=/OpenROAD-flow-scripts/flow/platforms/asap7 \ + DESIGN_CONFIG={pwd}/config.mk \ + WORK_HOME={work_dir} \ + gallery + +from IPython.display import Image + +gallery = pathlib.Path(run_path) / 'results/asap7/crc32/base/gallery_final_no_power.png' +sb.glue('layout', Image(gallery), 'display', display=True) +``` + +## Extract metrics + +Build a pandas dataframe with ppa. + +```python tags=[] +#papermill_description=ExtractingMetrics +import re +import scrapbook as sb +re_critical_path_delay = r'''critical path delay +-------------------------------------------------------------------------- +(\S+) +''' +re_critical_path_slack = r'''finish critical path slack +-------------------------------------------------------------------------- +(\S+) +''' +re_total_power = r'''^Total.*%$''' +re_design_area = r'''finish report_design_area +-------------------------------------------------------------------------- +Design area (\S+) u\^2 (\S+)% utilization.''' +def runs_ppa(): + for r in pathlib.Path(runs_dir).glob('*/logs/asap7/crc32/base/6_report.log'): + with r.open() as f: + report = f.read() + critical_path_delay = float(re.search(re_critical_path_delay, report).group(1)) + critical_path_slack = float(re.search(re_critical_path_slack, report).group(1)) + m = re.search(re_total_power, report, re.MULTILINE) + total_power = float(re.split('\s+', m.group())[-2]) + m = re.search(re_design_area, report, re.MULTILINE) + area = int(m.group(1)) + utilization = float(m.group(2)) / 100.0 + yield r.parts[1], total_power, critical_path_delay, critical_path_slack, area, utilization + +df = pd.DataFrame.from_records(runs_ppa(), columns=['run', 'power', 'delay', 'slack', 'area', 'utilization'], index='run').sort_index() +sb.glue('ppa', df, 'pandas') +(df.style + .format({'area': '{:.8f}', 'utilization': '{:.2%}', 'power': '{:.6f}', 'slack': '{:.6f}', 'delay': '{:.6f}'}) + .bar(subset=['power'], color='pink') + .bar(subset=['slack'], color='lime') + .background_gradient(subset=['utilization'], cmap='Greens') + .bar(subset=['area'], color='lightblue')) +``` + +Report metrics for hyper-parameters tuning. + +```python +#papermill_description=ReportingMetrics +import hypertune + +slack = df['slack'][0] +print('reporting metric:', 'slack', slack) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='slack', + metric_value=slack, +) +``` diff --git a/modules/silicon_design/scripts/build/notebooks/xls.md b/modules/silicon_design/scripts/build/notebooks/xls/xls-crc32-sky130.md similarity index 100% rename from modules/silicon_design/scripts/build/notebooks/xls.md rename to modules/silicon_design/scripts/build/notebooks/xls/xls-crc32-sky130.md From 632a1e835e6e748c8c58a46b531c8ce48b3af765 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 15:36:14 +0900 Subject: [PATCH 78/93] modules/silicon_design: fix inverter filepath --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 91a812f7..cbf0df06 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -306,7 +306,7 @@ resource "null_resource" "build_and_push_image" { environment_sha = filesha1("${path.module}/scripts/build/images/provision/environment.yml") env_sha = filesha1("${path.module}/scripts/build/images/provision/install.tcl") profile_sha = filesha1("${path.module}/scripts/build/images/provision/profile.sh") - notebook_sha = filesha1("${path.module}/scripts/build/notebooks/inverter.md") + notebook_sha = filesha1("${path.module}/scripts/build/notebooks/inverter/inverter.md") image_tag = local.image_tag } From d907cac6d30ded6ef19f1f5c27e1d76982929e2a Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 16:04:28 +0900 Subject: [PATCH 79/93] modules/silicon: remove obsolete artifact section --- modules/silicon_design/scripts/build/cloudbuild.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 54a7ed4e..5335b8d6 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -37,13 +37,6 @@ steps: env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md echo "gsutil rsync -r -x '.*\.sh' -x '.*\.json' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh gsutil rsync -r -x '.*\.md' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ -artifacts: - objects: - location: gs://$_NOTEBOOKS_BUCKET/ - paths: - - 'scripts/build/notebooks/copy-notebooks.sh' - - 'scripts/build/notebooks/*.ipynb' - waitFor: ['-'] - id: 'compute-image-build' name: 'gcr.io/cloud-builders/gcloud' From 6985600109681ffa61c8fbc379084b9e78b2d978 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 16:38:15 +0900 Subject: [PATCH 80/93] modules/silicon: fix notebook build --- modules/silicon_design/scripts/build/cloudbuild.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 5335b8d6..61984381 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -32,11 +32,12 @@ steps: args: - '-c' - |- + apt-get -yq install python3.8-venv python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md echo "gsutil rsync -r -x '.*\.sh' -x '.*\.json' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh - gsutil rsync -r -x '.*\.md' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ + gsutil rsync -r -x '.*\.md' -x '*\.svg' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ waitFor: ['-'] - id: 'compute-image-build' name: 'gcr.io/cloud-builders/gcloud' From 92e4cac830528d5a091b07c2c7d3ff7650c18aff Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 16:44:09 +0900 Subject: [PATCH 81/93] modules/silicon: add missing sudo --- modules/silicon_design/scripts/build/cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 61984381..9fb1c33e 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -32,7 +32,7 @@ steps: args: - '-c' - |- - apt-get -yq install python3.8-venv + sudo apt-get -yq install python3-venv python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md From 09a6da5cf6eb452077ae9f3e9221205a92963a45 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 16:47:05 +0900 Subject: [PATCH 82/93] modules/silicon: fix filter --- modules/silicon_design/scripts/build/cloudbuild.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 9fb1c33e..ac6e30de 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -32,12 +32,12 @@ steps: args: - '-c' - |- - sudo apt-get -yq install python3-venv + apt-get -yq install python3-venv python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md echo "gsutil rsync -r -x '.*\.sh' -x '.*\.json' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh - gsutil rsync -r -x '.*\.md' -x '*\.svg' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ + gsutil rsync -r -x '.*\.md' -x '.*\.svg' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ waitFor: ['-'] - id: 'compute-image-build' name: 'gcr.io/cloud-builders/gcloud' From 8205835b3e3414ce41764f54dcd3943033e803f2 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 16:54:45 +0900 Subject: [PATCH 83/93] modules/silicon: fix venv package name --- modules/silicon_design/scripts/build/cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index ac6e30de..cbb16728 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -32,7 +32,7 @@ steps: args: - '-c' - |- - apt-get -yq install python3-venv + apt-get -yq install python3.8-venv python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md From 3ab0ded611d905c560d2e782f77bb031953e0c8b Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 18:27:20 +0900 Subject: [PATCH 84/93] modules/silicon/cloudbuild: apt-get update --- modules/silicon_design/scripts/build/cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index cbb16728..7898232f 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -32,7 +32,7 @@ steps: args: - '-c' - |- - apt-get -yq install python3.8-venv + apt-get update && apt-get install -yq python3.8-venv python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md From e572a10a51f50355ced349e8ed167c589d4a180c Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 19:17:55 +0900 Subject: [PATCH 85/93] modules/silicon/build/notebook: fix rsync regexp --- modules/silicon_design/scripts/build/cloudbuild.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 7898232f..734eec0c 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -36,8 +36,8 @@ steps: python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md - echo "gsutil rsync -r -x '.*\.sh' -x '.*\.json' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh - gsutil rsync -r -x '.*\.md' -x '.*\.svg' scripts/build/notebooks/ gs://$_NOTEBOOKS_BUCKET/ + echo "gsutil rsync -r -x '.*\.(sh|json)' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh + gsutil rsync -r -x '.*\.(md|svg)' scripts/build/notebooks/ gs://radlab-silicon-tuning-notebooks/ waitFor: ['-'] - id: 'compute-image-build' name: 'gcr.io/cloud-builders/gcloud' From 5624e4bea702cbaf2e20a0d3a4b8afb578c7d0cd Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 20:38:42 +0900 Subject: [PATCH 86/93] modules/silicon/notebooks/inverter: fix config and report path --- .../scripts/build/notebooks/inverter/inverter.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/silicon_design/scripts/build/notebooks/inverter/inverter.md b/modules/silicon_design/scripts/build/notebooks/inverter/inverter.md index abe63d2d..bc4bddc3 100644 --- a/modules/silicon_design/scripts/build/notebooks/inverter/inverter.md +++ b/modules/silicon_design/scripts/build/notebooks/inverter/inverter.md @@ -5,7 +5,7 @@ jupyter: extension: .md format_name: markdown format_version: '1.3' - jupytext_version: 1.13.8 + jupytext_version: 1.14.1 kernelspec: display_name: Python 3 (ipykernel) language: python @@ -26,8 +26,8 @@ This notebook shows how to run a simple inverter design thru an end-to-end RTL t ## Define flow parameters ```python tags=["parameters"] -die_width = 45 -target_density = 90 +die_width = 50 +target_density = 70 run_path = 'runs' ``` @@ -45,7 +45,7 @@ endmodule See [OpenLane Variables information](https://github.com/The-OpenROAD-Project/OpenLane/blob/master/configuration/README.md) for the list of available variables. ```python -%%bash -c 'cat > config.tcl; tclsh config.tcl' +%%writefile config.tcl set ::env(DESIGN_NAME) inverter set ::env(VERILOG_FILES) "inverter.v" @@ -122,7 +122,7 @@ def get_power(sta_power_report): def area_density_ppa(): for report in sorted(pathlib.Path(run_path).glob('*/reports')): - sta_power_report = report / 'signoff/27-rcx_mca_sta.power.rpt' + sta_power_report = report / 'signoff/28-rcx_mca_sta.power.rpt' final_summary_report = report / 'metrics.csv' if final_summary_report.exists() and sta_power_report.exists(): df = pd.read_csv(final_summary_report) From cbbd58ed37b63caf485027fa196b187b6e403898 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 22:18:21 +0900 Subject: [PATCH 87/93] modules/silicon: unique bucket name --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index cbf0df06..22d258ff 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -290,7 +290,7 @@ resource "google_artifact_registry_repository" "containers_repo" { resource "google_storage_bucket" "notebooks_bucket" { project = local.project.project_id - name = "${var.name}-notebooks" + name = "${local.project.project_id}-${var.name}-notebooks" location = local.region force_destroy = true uniform_bucket_level_access = true From 5e44478779536e71b58108b5c7566f5679a99c3f Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 22:24:26 +0900 Subject: [PATCH 88/93] modules/silicon: fix output variables --- modules/silicon_design/main.tf | 8 ++++---- modules/silicon_design/outputs.tf | 9 +++++++-- modules/silicon_design/scripts/build/cloudbuild.yaml | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 22d258ff..97bfde9f 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -258,7 +258,7 @@ resource "google_notebooks_instance" "ai_notebook" { network = local.network.self_link subnet = local.subnet.self_link - post_startup_script = "gs://${google_storage_bucket.notebooks_bucket.name}/copy-notebooks.sh" + post_startup_script = "gs://${google_storage_bucket.staging_bucket.name}/copy-notebooks.sh" labels = { module = "silicon-design" @@ -288,7 +288,7 @@ resource "google_artifact_registry_repository" "containers_repo" { ] } -resource "google_storage_bucket" "notebooks_bucket" { +resource "google_storage_bucket" "staging_bucket" { project = local.project.project_id name = "${local.project.project_id}-${var.name}-notebooks" location = local.region @@ -312,12 +312,12 @@ resource "null_resource" "build_and_push_image" { provisioner "local-exec" { working_dir = path.module - command = "gcloud --project=${local.project.project_id} builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions \"_ZONE=${var.zone},_COMPUTE_IMAGE=${var.image_name},_CONTAINER_IMAGE=${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name},_NOTEBOOKS_BUCKET=${google_storage_bucket.notebooks_bucket.name},_COMPUTE_NETWORK=${local.network.id},_COMPUTE_SUBNET=${local.subnet.id},_IMAGE_TAG=${local.image_tag},_CLOUD_BUILD_SA=${google_service_account.sa_image_builder_identity.email}\"" + command = "gcloud --project=${local.project.project_id} builds submit . --config ./scripts/build/cloudbuild.yaml --substitutions \"_ZONE=${var.zone},_COMPUTE_IMAGE=${var.image_name},_CONTAINER_IMAGE=${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name},_STAGING_BUCKET=${google_storage_bucket.staging_bucket.name},_COMPUTE_NETWORK=${local.network.id},_COMPUTE_SUBNET=${local.subnet.id},_IMAGE_TAG=${local.image_tag},_CLOUD_BUILD_SA=${google_service_account.sa_image_builder_identity.email}\"" } depends_on = [ google_artifact_registry_repository.containers_repo, - google_storage_bucket.notebooks_bucket, + google_storage_bucket.staging_bucket, google_project_iam_member.sa_image_builder_permissions, google_project_iam_member.sa_cloudbuild_permissions, google_service_account_iam_member.sa_cloudbuild_image_builder_access, diff --git a/modules/silicon_design/outputs.tf b/modules/silicon_design/outputs.tf index aa1c54e3..403b228e 100644 --- a/modules/silicon_design/outputs.tf +++ b/modules/silicon_design/outputs.tf @@ -19,7 +19,7 @@ output "deployment_id" { value = local.random_id } -output "project_radlab_silicon_design_id" { +output "project_id" { description = "Silicon Design RAD Lab Project ID" value = local.project.project_id } @@ -39,7 +39,12 @@ output "notebooks_container_image" { value = "${google_artifact_registry_repository.containers_repo.location}-docker.pkg.dev/${local.project.project_id}/${google_artifact_registry_repository.containers_repo.repository_id}/${var.image_name}:${local.image_tag}" } -output "notebooks_vm" { +output "notebooks_vm_image" { description = "GCE VM Image Name" value = "${var.image_name}-${local.image_tag}" } + +output "notebooks_staging_bucket" { + description = "Noteooks Staging bucket" + value = "${google_storage_bucket.staging_bucket.name}" +} diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 734eec0c..4f924895 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -19,7 +19,7 @@ substitutions: _COMPUTE_IMAGE: 'silicon-design-ubuntu-2004' _CONTAINER_IMAGE: 'silicon-design-ubuntu-2004' _IMAGE_TAG: 'default-tag' - _NOTEBOOKS_BUCKET: 'silicon-design-notebooks' + _STAGING_BUCKET: 'silicon-design-notebooks' _COMPUTE_NETWORK: 'global/networks/default' _COMPUTE_SUBNET: '' _CLOUD_BUILD_SA: '' @@ -36,7 +36,7 @@ steps: python3 -m venv env/ env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md - echo "gsutil rsync -r -x '.*\.(sh|json)' gs://$_NOTEBOOKS_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh + echo "gsutil rsync -r -x '.*\.(sh|json)' gs://$_STAGING_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh gsutil rsync -r -x '.*\.(md|svg)' scripts/build/notebooks/ gs://radlab-silicon-tuning-notebooks/ waitFor: ['-'] - id: 'compute-image-build' From 6229c6c253a11c05e79e7a13f13ec827b725f77e Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 22:30:10 +0900 Subject: [PATCH 89/93] modules/silicon: fix notebook staging bucket name --- modules/silicon_design/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/main.tf b/modules/silicon_design/main.tf index 97bfde9f..977e39e4 100644 --- a/modules/silicon_design/main.tf +++ b/modules/silicon_design/main.tf @@ -290,7 +290,7 @@ resource "google_artifact_registry_repository" "containers_repo" { resource "google_storage_bucket" "staging_bucket" { project = local.project.project_id - name = "${local.project.project_id}-${var.name}-notebooks" + name = "${local.project.project_id}-${var.name}-staging" location = local.region force_destroy = true uniform_bucket_level_access = true From f2094fa2215832c1db24aaecc33f7c2ba5390326 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Mon, 8 Aug 2022 22:44:57 +0900 Subject: [PATCH 90/93] modules/silicon: fix hardcoded bucket name --- modules/silicon_design/scripts/build/cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/scripts/build/cloudbuild.yaml b/modules/silicon_design/scripts/build/cloudbuild.yaml index 4f924895..da5b9e26 100644 --- a/modules/silicon_design/scripts/build/cloudbuild.yaml +++ b/modules/silicon_design/scripts/build/cloudbuild.yaml @@ -37,7 +37,7 @@ steps: env/bin/python -m pip install jupytext env/bin/jupytext --to notebook scripts/build/notebooks/**/*.md echo "gsutil rsync -r -x '.*\.(sh|json)' gs://$_STAGING_BUCKET/ /home/jupyter/" > scripts/build/notebooks/copy-notebooks.sh - gsutil rsync -r -x '.*\.(md|svg)' scripts/build/notebooks/ gs://radlab-silicon-tuning-notebooks/ + gsutil rsync -r -x '.*\.(md|svg)' scripts/build/notebooks/ gs://$_STAGING_BUCKET/ waitFor: ['-'] - id: 'compute-image-build' name: 'gcr.io/cloud-builders/gcloud' From 053adcf2277b721b3749460ee91c19f41ff76143 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 9 Aug 2022 19:00:44 +0900 Subject: [PATCH 91/93] modules/silicon: fix permissions --- modules/silicon_design/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/silicon_design/README.md b/modules/silicon_design/README.md index 5368c0e0..9e4fab94 100644 --- a/modules/silicon_design/README.md +++ b/modules/silicon_design/README.md @@ -44,7 +44,7 @@ When deploying in an existing project, ensure the identity has the following per - `roles/resourcemanager.projectIamAdmin` - `roles/iam.serviceAccountAdmin` - `roles/iam.serviceAccountUser` -- `serviceusage.serviceUsageConsumer` +- `roles/serviceusage.serviceUsageAdmin` NOTE: Additional [permissions](./radlab-launcher/README.md#iam-permissions-prerequisites) are required when deploying the RAD Lab modules via [RAD Lab Launcher](./radlab-launcher) From 97d7609c57730927b749076d6d7176f36954c6ec Mon Sep 17 00:00:00 2001 From: Shvetank Prakash Date: Tue, 9 Aug 2022 19:19:15 -0400 Subject: [PATCH 92/93] Add CFU Playground env and setup needed to rad-lab --- .../scripts/build/images/provision.sh | 10 ++++ .../build/images/provision/environment.yml | 49 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/modules/silicon_design/scripts/build/images/provision.sh b/modules/silicon_design/scripts/build/images/provision.sh index 1e2a7bbe..284b5d36 100644 --- a/modules/silicon_design/scripts/build/images/provision.sh +++ b/modules/silicon_design/scripts/build/images/provision.sh @@ -20,6 +20,7 @@ trap "echo DaisyFailure: trapped error" ERR env OPENLANE_VERSION=master OPENROAD_FLOW_VERSION=master +CFU_PLAYGROUND_VERSION=dse_framework PROVISION_DIR=/tmp/provision SYSTEM_NAME=$(dmidecode -s system-product-name || true) @@ -60,3 +61,12 @@ cp ${PROVISION_DIR}/papermill-launcher /usr/local/bin/ chmod +x /usr/local/bin/papermill-launcher echo "DaisySuccess: done" + +echo "Cloning CFU-Playground repository" +git clone -b ${CFU_PLAYGROUND_VERSION} https://github.com/google/CFU-Playground.git /CFU-Playground + +echo "Running CFU-Playground setup" +cd /CFU-Playground +./scripts/setup +./scripts/setup_vexriscv_build.sh + diff --git a/modules/silicon_design/scripts/build/images/provision/environment.yml b/modules/silicon_design/scripts/build/images/provision/environment.yml index a90b0204..e4429507 100644 --- a/modules/silicon_design/scripts/build/images/provision/environment.yml +++ b/modules/silicon_design/scripts/build/images/provision/environment.yml @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. channels: + - defaults - litex-hub - conda-forge dependencies: @@ -20,7 +21,6 @@ dependencies: - magic - openroad - netgen - - yosys - gdstk - cairosvg - svgutils @@ -34,10 +34,55 @@ dependencies: - pandas - pymeep=*=mpi_mpich_* - jupyterlab - - python + - litex-hub::gcc-riscv32-elf-newlib + - litex-hub::openfpgaloader + - litex-hub::dfu-util + - litex-hub::flterm + - litex-hub::openocd + - litex-hub::verilator + - litex-hub::nextpnr-nexus + - litex-hub::nextpnr-ecp5 + - litex-hub::nextpnr-ice40 + - litex-hub::yosys + - litex-hub::iceprog + - litex-hub::prjxray-tools + - litex-hub::prjxray-db + - litex-hub::vtr-optimized + - litex-hub::symbiflow-yosys-plugins + - lxml + - simplejson + - intervaltree + - json-c + - libevent + - python=3.7 - pip - pip: - klayout - scrapbook[gcs] - google-cloud-aiplatform - cloudml-hypertune + - simplejson + - termcolor + - tqdm + - yapf==0.24.0 + - requests + - meson + - Pillow + - intervaltree + - junit-xml + - numpy + - openpyxl + - ordered-set + - parse + - progressbar2 + - pyjson5 + - pytest + - pyyaml + - scipy>=1.2.1 + - sympy + - textx + - python-constraint + - fasm + - https://github.com/chipsalliance/f4pga/archive/8c411eb74e4bb23d1ec243a1515b9bfb48e2cd83.zip#subdirectory=f4pga + - git+https://github.com/f4pga/prjxray.git@ae546d6b7648bf4df9cf63f0b25b2028b5623c43#egg=prjxray + - git+https://github.com/chipsalliance/f4pga-xc-fasm.git@25dc605c9c0896204f0c3425b52a332034cf5e5c#egg=xc-fasm From d069d2941ecb05cd6f2050bcb1e54cffd76227eb Mon Sep 17 00:00:00 2001 From: Shvetank Prakash Date: Tue, 9 Aug 2022 19:58:49 -0400 Subject: [PATCH 93/93] Added CFU-Playground dse notebook in md format --- .../notebooks/cfuplayground/cfuplayground.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 modules/silicon_design/scripts/build/notebooks/cfuplayground/cfuplayground.md diff --git a/modules/silicon_design/scripts/build/notebooks/cfuplayground/cfuplayground.md b/modules/silicon_design/scripts/build/notebooks/cfuplayground/cfuplayground.md new file mode 100644 index 00000000..58f5cb88 --- /dev/null +++ b/modules/silicon_design/scripts/build/notebooks/cfuplayground/cfuplayground.md @@ -0,0 +1,74 @@ +--- +jupyter: + jupytext: + text_representation: + extension: .md + format_name: markdown + format_version: '1.3' + jupytext_version: 1.14.1 + kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# CFU Playground + + +### Design Space Exploration + +```python tags=["parameters"] +# Vexriscv soft core parameters available for tuning +bypass = True +cfu = True +dCacheSize = 2048 +hardwareDiv = True +iCacheSize = 1024 +mulDiv = True +prediction = "none" +safe = True +singleCycleShift = True +singleCycleMulDiv = True +``` + +```python +# Constants +CSR_PLUGIN_CONFIG = "mcycle" +TARGET = "digilent_arty" +``` + +```python +# Change directory to design space exploration project in CFU-Playground +%cd /CFU-Playground/proj/dse_template +``` + +```python tags=[] +import dse_framework + +dse_framework.dse(CSR_PLUGIN_CONFIG, bypass, cfu, dCacheSize, hardwareDiv, iCacheSize, mulDiv, prediction, safe, singleCycleShift, singleCycleMulDiv) +``` + +```python tags=[] +# Obtain metrics and glue to notebook for later use +import scrapbook as sb + +cycles = dse_framework.get_cycle_count() +cells = dse_framework.get_resource_util(TARGET) + +sb.glue('cells', cells) +sb.glue('cycles', cycles) + +print("Cycle Count: " + str(cycles)) +print("Cells Used: " + str (cells)) +``` + +```python +import hypertune + +print('Reporting Metric:', 'Cells^2 + Cycles', (cells*cells) + cycles) +hpt = hypertune.HyperTune() +hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag='cells+cycles', + metric_value=((cells*cells) + cycles), +) +```