diff --git a/.github/workflows/docker-builds.yaml b/.github/workflows/docker-builds.yaml index c920dbc..814d109 100644 --- a/.github/workflows/docker-builds.yaml +++ b/.github/workflows/docker-builds.yaml @@ -10,9 +10,13 @@ jobs: strategy: fail-fast: false matrix: - test: [["2023-RADIUSS-AWS/JupyterNotebook", "docker/Dockerfile.hub", "ghcr.io/flux-framework/flux-jupyter-hub:2023"], - ["2023-RADIUSS-AWS/JupyterNotebook", "docker/Dockerfile.init", "ghcr.io/flux-framework/flux-jupyter-init:2023"], - ["2023-RADIUSS-AWS/JupyterNotebook", "docker/Dockerfile.spawn", "ghcr.io/flux-framework/flux-jupyter-spawn:2023"]] +# Tutorial is over - these builds are disabled +# test: [["2023-RADIUSS-AWS/JupyterNotebook", "docker/Dockerfile.hub", "ghcr.io/flux-framework/flux-jupyter-hub:2023"], +# ["2023-RADIUSS-AWS/JupyterNotebook", "docker/Dockerfile.init", "ghcr.io/flux-framework/flux-jupyter-init:2023"], +# ["2023-RADIUSS-AWS/JupyterNotebook", "docker/Dockerfile.spawn", "ghcr.io/flux-framework/flux-jupyter-spawn:2023"]] + test: [["2024-RIKEN-AWS/JupyterNotebook", "docker/Dockerfile.hub", "ghcr.io/flux-framework/flux-jupyter-hub:riken-2024"], + ["2024-RIKEN-AWS/JupyterNotebook", "docker/Dockerfile.init", "ghcr.io/flux-framework/flux-jupyter-init:riken-2024"], + ["2024-RIKEN-AWS/JupyterNotebook", "docker/Dockerfile.spawn", "ghcr.io/flux-framework/flux-jupyter-spawn:riken-2024"]] steps: - name: Clone the code diff --git a/2024-RIKEN-AWS/JupyterNotebook/README.md b/2024-RIKEN-AWS/JupyterNotebook/README.md new file mode 100644 index 0000000..f52a570 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/README.md @@ -0,0 +1,1020 @@ +# Flux + Jupyter via KubeSpawner + +This set of tutorials provides: + + - [Building Base Images](#build-images) + - [Deploy A Cluster to AWS or Google Cloud Using](#deploy-to-kubernetes) using Google Cloud or AWS + - [Local Development or Usage](#local-usage) + +Pre-requisites: + + - kubectl, (eksctl|gcloud), and (optionally) docker installed locally + - A cloud with a Kubernetes cluster deployed (AWS and Google) + - Excitement to learn about Flux! + +For AWS Tutorial Day users: + +> To run the AWS tutorial, visit https://tutorial.flux-framework.org. You can use any login you want, but choose something relatvely uncommon (like your email address) or you may end up sharing a JupyterLab instance with another user. The tutorial password will be provided to you. + +## Build Images + +Let's build a set of images - one spawner and one hub. + +```bash +docker build -t ghcr.io/flux-framework/flux-jupyter-hub:2023 -f docker/Dockerfile.hub . +docker build -t ghcr.io/flux-framework/flux-jupyter-spawn:2023 -f docker/Dockerfile.spawn . +docker build -t ghcr.io/flux-framework/flux-jupyter-init:2023 -f docker/Dockerfile.init . +``` + +Note that these are available under the flux-framework organization GitHub packages, so you shouldn't need +to build them unless you are developing or changing them. + +If you do build (and use a different name) be sure to push your images to a public registry (or load them locally to your development cluster). +Remember that if you just want to test locally, you can jump to the [local usage](#local-usage) section. + +## Local Deploy + +While the tutorial here is intended for deployment on AWS or Google Cloud, you can also give it a try on your local machine with a single container! You will need to [install Docker](https://docs.docker.com/engine/install/). +When you have Docker available, you can build and run the tutorial with: + +```bash +docker build -t flux-tutorial -f docker/Dockerfile.spawn . +docker network create jupyterhub + +# Here is how to run an entirely contained tutorial (the notebook in the container) +docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock --net jupyterhub --name jupyterhub -p 8888:8888 flux-tutorial + +# Here is how to bind the tutorial files locally to make enduring changes! +docker run --rm -it --entrypoint /start.sh -v /var/run/docker.sock:/var/run/docker.sock -v ./tutorial:/home/jovyan/flux-radiuss-tutorial-2023 --net jupyterhub --name jupyterhub -p 8888:8888 flux-tutorial +``` + +If you want to develop the ipynb files, you can bind the tutorials directory: + +```bash +docker run --rm -it --entrypoint /start.sh -v $PWD/tutorial:/home/jovyan/flux-tutorial-2024 -v /var/run/docker.sock:/var/run/docker.sock --net jupyterhub --name jupyterhub -p 8888:8888 flux-tutorial +``` + +And then editing and saving will save to your host. You can also File -> Download if you forget to do +this bind. Either way, when the container is running you can open the localhost or 127.0.0.1 (home sweet home!) link in your browser on port 8888. You'll want to go to flux-tutorial-2024 -> notebook to see the notebook. +You'll need to select http only (and bypass the no certificate warning). + +## Deploy to Kubernetes + +### 1. Create Cluster + +#### Google Cloud + +Here is how to create the cluster on Google Cloud using [gcloud](https://cloud.google.com/sdk/docs/install) (and assuming you have logged in +with [gcloud auth login](https://cloud.google.com/sdk/gcloud/reference/auth/login): + +```bash +export GOOGLE_PROJECT=myproject +gcloud container clusters create flux-jupyter --project $GOOGLE_PROJECT \ + --zone us-central1-a --machine-type n1-standard-2 \ + --num-nodes=4 --enable-network-policy --enable-intra-node-visibility +``` + +#### AWS + +Here is how to create an equivalent cluster on AWS (EKS). We will be using [eksctl](https://eksctl.io/introduction/), which +you should install. + +```bash +# Create an EKS cluster with autoscaling with default storage +eksctl create cluster --config-file aws/eksctl-config.yaml + +# Create an EKS cluster with io1 node storage but no autoscaling, used for the RADIUSS 2023 tutorial +eksctl create cluster --config-file aws/eksctl-radiuss-tutorial-2023.yaml +``` + +You can find vanilla (manual) instructions [here](https://z2jh.jupyter.org/en/stable/kubernetes/amazon/step-zero-aws-eks.html) if you +are interested in how it works. We emulate the logic there using eksctl. Then generate a secret token - we will add this to [config-aws.yaml](aws/config-aws.yaml) (without SSL) or [config-aws-ssl.yaml](aws/config-aws-ssl.yaml) (with SSL). When your cluster is ready, this will deploy an EBS CSI driver: + +```bash +kubectl apply -k "github.com/kubernetes-sigs/aws-ebs-csi-driver/deploy/kubernetes/overlays/stable/?ref=master" +``` + +And install the cluster-autoscaler: + +```bash +kubectl apply -f aws/cluster-autoscaler-autodiscover.yaml +``` + +If you want to use a different storage class than the default (`gp2`), you also need to create the new storage class (`gp3` here) and set it as the default storage class: + +```bash +kubectl apply -f aws/storageclass.yaml +kubectl patch storageclass gp3 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' +kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' +``` + +Most of the information I needed to read about this was [here](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md) - the Jupyter documentation wasn't super helpful beyond saying to install it. Also note that I got this (seemingly working) without the `propagateASGTags` set to true, but that is something that I've seen have issue. +You can look at the autoscaler pod logs for information. + +While the spawned containers (e.g., where you run your notebook) don't use these volumes, the hub will. +You can read about [gp2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html) class. +Note that we will be using [config-aws.yaml](aws/config-aws.yaml) if you don't need SSL, and [config-aws-ssl.yaml](aws/config-aws-ssl.yaml) if you do. For the latter, the jupyter spawner will generate let's encrypt certificates for us, given that we have correctly configured DNS. + +### 2. Deploy JupyterHub + +We will use [helm](https://helm.sh/docs/helm/helm_install/) to install charts and deploy. + +```bash +helm repo add jupyterhub https://hub.jupyter.org/helm-chart/ +helm repo update +``` + +You can see the versions available: + +```bash +helm search repo jupyterhub +``` +```console +NAME CHART VERSION APP VERSION DESCRIPTION +bitnami/jupyterhub 4.2.0 4.0.2 JupyterHub brings the power of notebooks to gro... +jupyterhub/jupyterhub 3.0.2 4.0.2 Multi-user Jupyter installation +jupyterhub/pebble 1.0.1 v2.3.1 This Helm chart bootstraps Pebble: an ACME serv... +``` + +Note that chart versions don't always coincide with software (or "app") versions. At the time of writing this, +we are using the jupyterhub/jupyterhub 3.0.2/4.0.2 versions, and our container bases point to 3.0.2 tags for the +corresponding images. Next, see the values we can set, which likely will come from a config*.yaml that we will choose. + +```bash +helm show values jupyterhub/jupyterhub +``` + +
+ +Example values for the jupyterhub helm chart + +```console +# fullnameOverride and nameOverride distinguishes blank strings, null values, +# and non-blank strings. For more details, see the configuration reference. +fullnameOverride: "" +nameOverride: + +# enabled is ignored by the jupyterhub chart itself, but a chart depending on +# the jupyterhub chart conditionally can make use this config option as the +# condition. +enabled: + +# custom can contain anything you want to pass to the hub pod, as all passed +# Helm template values will be made available there. +custom: {} + +# imagePullSecret is configuration to create a k8s Secret that Helm chart's pods +# can get credentials from to pull their images. +imagePullSecret: + create: false + automaticReferenceInjection: true + registry: + username: + password: + email: +# imagePullSecrets is configuration to reference the k8s Secret resources the +# Helm chart's pods can get credentials from to pull their images. +imagePullSecrets: [] + +# hub relates to the hub pod, responsible for running JupyterHub, its configured +# Authenticator class KubeSpawner, and its configured Proxy class +# ConfigurableHTTPProxy. KubeSpawner creates the user pods, and +# ConfigurableHTTPProxy speaks with the actual ConfigurableHTTPProxy server in +# the proxy pod. +hub: + revisionHistoryLimit: + config: + JupyterHub: + admin_access: true + authenticator_class: dummy + service: + type: ClusterIP + annotations: {} + ports: + nodePort: + extraPorts: [] + loadBalancerIP: + baseUrl: / + cookieSecret: + initContainers: [] + nodeSelector: {} + tolerations: [] + concurrentSpawnLimit: 64 + consecutiveFailureLimit: 5 + activeServerLimit: + deploymentStrategy: + ## type: Recreate + ## - sqlite-pvc backed hubs require the Recreate deployment strategy as a + ## typical PVC storage can only be bound to one pod at the time. + ## - JupyterHub isn't designed to support being run in parallel. More work + ## needs to be done in JupyterHub itself for a fully highly available (HA) + ## deployment of JupyterHub on k8s is to be possible. + type: Recreate + db: + type: sqlite-pvc + upgrade: + pvc: + annotations: {} + selector: {} + accessModes: + - ReadWriteOnce + storage: 1Gi + subPath: + storageClassName: + url: + password: + labels: {} + annotations: {} + command: [] + args: [] + extraConfig: {} + extraFiles: {} + extraEnv: {} + extraContainers: [] + extraVolumes: [] + extraVolumeMounts: [] + image: + name: jupyterhub/k8s-hub + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + resources: {} + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + lifecycle: {} + loadRoles: {} + services: {} + pdb: + enabled: false + maxUnavailable: + minAvailable: 1 + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: true + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true + interNamespaceAccessLabels: ignore + allowedIngressPorts: [] + allowNamedServers: false + namedServerLimitPerUser: + authenticatePrometheus: + redirectToServer: + shutdownOnLogout: + templatePaths: [] + templateVars: {} + livenessProbe: + # The livenessProbe's aim to give JupyterHub sufficient time to startup but + # be able to restart if it becomes unresponsive for ~5 min. + enabled: true + initialDelaySeconds: 300 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 3 + readinessProbe: + # The readinessProbe's aim is to provide a successful startup indication, + # but following that never become unready before its livenessProbe fail and + # restarts it if needed. To become unready following startup serves no + # purpose as there are no other pod to fallback to in our non-HA deployment. + enabled: true + initialDelaySeconds: 0 + periodSeconds: 2 + failureThreshold: 1000 + timeoutSeconds: 1 + existingSecret: + serviceAccount: + create: true + name: + annotations: {} + extraPodSpec: {} + +rbac: + create: true + +# proxy relates to the proxy pod, the proxy-public service, and the autohttps +# pod and proxy-http service. +proxy: + secretToken: + annotations: {} + deploymentStrategy: + ## type: Recreate + ## - JupyterHub's interaction with the CHP proxy becomes a lot more robust + ## with this configuration. To understand this, consider that JupyterHub + ## during startup will interact a lot with the k8s service to reach a + ## ready proxy pod. If the hub pod during a helm upgrade is restarting + ## directly while the proxy pod is making a rolling upgrade, the hub pod + ## could end up running a sequence of interactions with the old proxy pod + ## and finishing up the sequence of interactions with the new proxy pod. + ## As CHP proxy pods carry individual state this is very error prone. One + ## outcome when not using Recreate as a strategy has been that user pods + ## have been deleted by the hub pod because it considered them unreachable + ## as it only configured the old proxy pod but not the new before trying + ## to reach them. + type: Recreate + ## rollingUpdate: + ## - WARNING: + ## This is required to be set explicitly blank! Without it being + ## explicitly blank, k8s will let eventual old values under rollingUpdate + ## remain and then the Deployment becomes invalid and a helm upgrade would + ## fail with an error like this: + ## + ## UPGRADE FAILED + ## Error: Deployment.apps "proxy" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate' + ## Error: UPGRADE FAILED: Deployment.apps "proxy" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate' + rollingUpdate: + # service relates to the proxy-public service + service: + type: LoadBalancer + labels: {} + annotations: {} + nodePorts: + http: + https: + disableHttpPort: false + extraPorts: [] + loadBalancerIP: + loadBalancerSourceRanges: [] + # chp relates to the proxy pod, which is responsible for routing traffic based + # on dynamic configuration sent from JupyterHub to CHP's REST API. + chp: + revisionHistoryLimit: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: jupyterhub/configurable-http-proxy + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + tag: "4.5.6" # https://github.com/jupyterhub/configurable-http-proxy/tags + pullPolicy: + pullSecrets: [] + extraCommandLineFlags: [] + livenessProbe: + enabled: true + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 3 + readinessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 2 + failureThreshold: 1000 + timeoutSeconds: 1 + resources: {} + defaultTarget: + errorTarget: + extraEnv: {} + nodeSelector: {} + tolerations: [] + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: true + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true + interNamespaceAccessLabels: ignore + allowedIngressPorts: [http, https] + pdb: + enabled: false + maxUnavailable: + minAvailable: 1 + extraPodSpec: {} + # traefik relates to the autohttps pod, which is responsible for TLS + # termination when proxy.https.type=letsencrypt. + traefik: + revisionHistoryLimit: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: traefik + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + tag: "v2.10.4" # ref: https://hub.docker.com/_/traefik?tab=tags + pullPolicy: + pullSecrets: [] + hsts: + includeSubdomains: false + preload: false + maxAge: 15724800 # About 6 months + resources: {} + labels: {} + extraInitContainers: [] + extraEnv: {} + extraVolumes: [] + extraVolumeMounts: [] + extraStaticConfig: {} + extraDynamicConfig: {} + nodeSelector: {} + tolerations: [] + extraPorts: [] + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: true + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: true + interNamespaceAccessLabels: ignore + allowedIngressPorts: [http, https] + pdb: + enabled: false + maxUnavailable: + minAvailable: 1 + serviceAccount: + create: true + name: + annotations: {} + extraPodSpec: {} + secretSync: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: jupyterhub/k8s-secret-sync + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + resources: {} + labels: {} + https: + enabled: false + type: letsencrypt + #type: letsencrypt, manual, offload, secret + letsencrypt: + contactEmail: + # Specify custom server here (https://acme-staging-v02.api.letsencrypt.org/directory) to hit staging LE + acmeServer: https://acme-v02.api.letsencrypt.org/directory + manual: + key: + cert: + secret: + name: + key: tls.key + crt: tls.crt + hosts: [] + +# singleuser relates to the configuration of KubeSpawner which runs in the hub +# pod, and its spawning of user pods such as jupyter-myusername. +singleuser: + podNameTemplate: + extraTolerations: [] + nodeSelector: {} + extraNodeAffinity: + required: [] + preferred: [] + extraPodAffinity: + required: [] + preferred: [] + extraPodAntiAffinity: + required: [] + preferred: [] + networkTools: + image: + name: jupyterhub/k8s-network-tools + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + resources: {} + cloudMetadata: + # block set to true will append a privileged initContainer using the + # iptables to block the sensitive metadata server at the provided ip. + blockWithIptables: true + ip: 169.254.169.254 + networkPolicy: + enabled: true + ingress: [] + egress: [] + egressAllowRules: + cloudMetadataServer: false + dnsPortsCloudMetadataServer: true + dnsPortsKubeSystemNamespace: true + dnsPortsPrivateIPs: true + nonPrivateIPs: true + privateIPs: false + interNamespaceAccessLabels: ignore + allowedIngressPorts: [] + events: true + extraAnnotations: {} + extraLabels: + hub.jupyter.org/network-access-hub: "true" + extraFiles: {} + extraEnv: {} + lifecycleHooks: {} + initContainers: [] + extraContainers: [] + allowPrivilegeEscalation: false + uid: 1000 + fsGid: 100 + serviceAccountName: + storage: + type: dynamic + extraLabels: {} + extraVolumes: [] + extraVolumeMounts: [] + static: + pvcName: + subPath: "{username}" + capacity: 10Gi + homeMountPath: /home/jovyan + dynamic: + storageClass: + pvcNameTemplate: claim-{username}{servername} + volumeNameTemplate: volume-{username}{servername} + storageAccessModes: [ReadWriteOnce] + image: + name: jupyterhub/k8s-singleuser-sample + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + startTimeout: 300 + cpu: + limit: + guarantee: + memory: + limit: + guarantee: 1G + extraResource: + limits: {} + guarantees: {} + cmd: jupyterhub-singleuser + defaultUrl: + extraPodConfig: {} + profileList: [] + +# scheduling relates to the user-scheduler pods and user-placeholder pods. +scheduling: + userScheduler: + enabled: true + revisionHistoryLimit: + replicas: 2 + logLevel: 4 + # plugins are configured on the user-scheduler to make us score how we + # schedule user pods in a way to help us schedule on the most busy node. By + # doing this, we help scale down more effectively. It isn't obvious how to + # enable/disable scoring plugins, and configure them, to accomplish this. + # + # plugins ref: https://kubernetes.io/docs/reference/scheduling/config/#scheduling-plugins-1 + # migration ref: https://kubernetes.io/docs/reference/scheduling/config/#scheduler-configuration-migrations + # + plugins: + score: + # These scoring plugins are enabled by default according to + # https://kubernetes.io/docs/reference/scheduling/config/#scheduling-plugins + # 2022-02-22. + # + # Enabled with high priority: + # - NodeAffinity + # - InterPodAffinity + # - NodeResourcesFit + # - ImageLocality + # Remains enabled with low default priority: + # - TaintToleration + # - PodTopologySpread + # - VolumeBinding + # Disabled for scoring: + # - NodeResourcesBalancedAllocation + # + disabled: + # We disable these plugins (with regards to scoring) to not interfere + # or complicate our use of NodeResourcesFit. + - name: NodeResourcesBalancedAllocation + # Disable plugins to be allowed to enable them again with a different + # weight and avoid an error. + - name: NodeAffinity + - name: InterPodAffinity + - name: NodeResourcesFit + - name: ImageLocality + enabled: + - name: NodeAffinity + weight: 14631 + - name: InterPodAffinity + weight: 1331 + - name: NodeResourcesFit + weight: 121 + - name: ImageLocality + weight: 11 + pluginConfig: + # Here we declare that we should optimize pods to fit based on a + # MostAllocated strategy instead of the default LeastAllocated. + - name: NodeResourcesFit + args: + scoringStrategy: + resources: + - name: cpu + weight: 1 + - name: memory + weight: 1 + type: MostAllocated + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + # IMPORTANT: Bumping the minor version of this binary should go hand in + # hand with an inspection of the user-scheduelrs RBAC resources + # that we have forked in + # templates/scheduling/user-scheduler/rbac.yaml. + # + # Debugging advice: + # + # - Is configuration of kube-scheduler broken in + # templates/scheduling/user-scheduler/configmap.yaml? + # + # - Is the kube-scheduler binary's compatibility to work + # against a k8s api-server that is too new or too old? + # + # - You can update the GitHub workflow that runs tests to + # include "deploy/user-scheduler" in the k8s namespace report + # and reduce the user-scheduler deployments replicas to 1 in + # dev-config.yaml to get relevant logs from the user-scheduler + # pods. Inspect the "Kubernetes namespace report" action! + # + # - Typical failures are that kube-scheduler fails to search for + # resources via its "informers", and won't start trying to + # schedule pods before they succeed which may require + # additional RBAC permissions or that the k8s api-server is + # aware of the resources. + # + # - If "successfully acquired lease" can be seen in the logs, it + # is a good sign kube-scheduler is ready to schedule pods. + # + name: registry.k8s.io/kube-scheduler + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. The minor version is pinned in the + # workflow, and should be updated there if a minor version bump is done + # here. We aim to stay around 1 minor version behind the latest k8s + # version. + # + tag: "v1.26.7" # ref: https://github.com/kubernetes/kubernetes/tree/master/CHANGELOG + pullPolicy: + pullSecrets: [] + nodeSelector: {} + tolerations: [] + labels: {} + annotations: {} + pdb: + enabled: true + maxUnavailable: 1 + minAvailable: + resources: {} + serviceAccount: + create: true + name: + annotations: {} + extraPodSpec: {} + podPriority: + enabled: false + globalDefault: false + defaultPriority: 0 + imagePullerPriority: -5 + userPlaceholderPriority: -10 + userPlaceholder: + enabled: true + image: + name: registry.k8s.io/pause + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + # If you update this, also update prePuller.pause.image.tag + # + tag: "3.9" + pullPolicy: + pullSecrets: [] + revisionHistoryLimit: + replicas: 0 + labels: {} + annotations: {} + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + resources: {} + corePods: + tolerations: + - key: hub.jupyter.org/dedicated + operator: Equal + value: core + effect: NoSchedule + - key: hub.jupyter.org_dedicated + operator: Equal + value: core + effect: NoSchedule + nodeAffinity: + matchNodePurpose: prefer + userPods: + tolerations: + - key: hub.jupyter.org/dedicated + operator: Equal + value: user + effect: NoSchedule + - key: hub.jupyter.org_dedicated + operator: Equal + value: user + effect: NoSchedule + nodeAffinity: + matchNodePurpose: prefer + +# prePuller relates to the hook|continuous-image-puller DaemonsSets +prePuller: + revisionHistoryLimit: + labels: {} + annotations: {} + resources: {} + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + extraTolerations: [] + # hook relates to the hook-image-awaiter Job and hook-image-puller DaemonSet + hook: + enabled: true + pullOnlyOnChanges: true + # image and the configuration below relates to the hook-image-awaiter Job + image: + name: jupyterhub/k8s-image-awaiter + tag: "3.0.2" + pullPolicy: + pullSecrets: [] + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + podSchedulingWaitDuration: 10 + nodeSelector: {} + tolerations: [] + resources: {} + serviceAccount: + create: true + name: + annotations: {} + continuous: + enabled: true + pullProfileListImages: true + extraImages: {} + pause: + containerSecurityContext: + runAsUser: 65534 # nobody user + runAsGroup: 65534 # nobody group + allowPrivilegeEscalation: false + image: + name: registry.k8s.io/pause + # tag is automatically bumped to new patch versions by the + # watch-dependencies.yaml workflow. + # + # If you update this, also update scheduling.userPlaceholder.image.tag + # + tag: "3.9" + pullPolicy: + pullSecrets: [] + +ingress: + enabled: false + annotations: {} + ingressClassName: + hosts: [] + pathSuffix: + pathType: Prefix + tls: [] + +# cull relates to the jupyterhub-idle-culler service, responsible for evicting +# inactive singleuser pods. +# +# The configuration below, except for enabled, corresponds to command-line flags +# for jupyterhub-idle-culler as documented here: +# https://github.com/jupyterhub/jupyterhub-idle-culler#as-a-standalone-script +# +cull: + enabled: true + users: false # --cull-users + adminUsers: true # --cull-admin-users + removeNamedServers: false # --remove-named-servers + timeout: 3600 # --timeout + every: 600 # --cull-every + concurrency: 10 # --concurrency + maxAge: 0 # --max-age + +debug: + enabled: false + +global: + safeToShowValues: false +``` + +
+ +#### Changes You Might Need to Make: + +- Change the config*.yaml image-> name and tag that you deploy to use your images. +- You might want to change the number of user placeholder pods +- Also change the hub->concurrentSpawnLimit +- Change the password, ssl secret, and domain name if applicable +- Change the aws/eksctl-config.yaml autoscaling ranges depending on your needs. +- Remove pullPolicy Always if you don't expect to want to update/re-pull an image every time (ideal for production) + +And here is how to deploy, assuming the default namespace. Please choose your cloud appropriately! + +```bash +# This is for Google Cloud +helm install flux-jupyter jupyterhub/jupyterhub --values gcp/config.yaml + +# This is for Amazon EKS without SSL +helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws.yaml + +# This is for Amazon EKS with SSL (assuming DNS is configured) +helm install flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml +``` + +If you mess something up, you can change the file and run `helm upgrade`: + +```bash +helm upgrade flux-jupyter jupyterhub/jupyterhub --values aws/config-aws-ssl.yaml +``` + +If you REALLY mess something up, you can tear the whole thing down and then install again: + +```bash +helm uninstall flux-jupyter +``` + +Note that in practice of bringing this up and down many times, we have seen the proxy-public +not create a handful of times. If this happens, just tear down everything, wait for all pods +to terminate, and then start freshly. When you run a command, also note that the terminal will hang! +You can see progress in another terminal: + +```bash +$ kubectl get pods +``` + +or try watching: + +```bash +$ kubectl get pods --watch +``` + +When it's done, you should see: + +```bash +$ kubectl get pods +NAME READY STATUS RESTARTS AGE +continuous-image-puller-nvr4g 1/1 Running 0 5m31s +hub-7d59dfb748-mrfdv 1/1 Running 0 5m31s +proxy-d9dfbf77b-v488t 1/1 Running 0 5m31s +user-scheduler-587fcc5479-c4mmk 1/1 Running 0 5m31s +user-scheduler-587fcc5479-x6jmk 1/1 Running 0 5m31s +``` + +(The numbers of each above might vary based on the size of your cluster). And the terminal provides a lot of useful output: + +
+ +Output of Terminal on Completed Install + +```console +NAME: flux-jupyter +LAST DEPLOYED: Sun Aug 27 15:00:15 2023 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +. __ __ __ __ __ + / / __ __ ____ __ __ / /_ ___ _____ / / / / __ __ / /_ + __ / / / / / / / __ \ / / / / / __/ / _ \ / ___/ / /_/ / / / / / / __ \ +/ /_/ / / /_/ / / /_/ / / /_/ / / /_ / __/ / / / __ / / /_/ / / /_/ / +\____/ \__,_/ / .___/ \__, / \__/ \___/ /_/ /_/ /_/ \__,_/ /_.___/ + /_/ /____/ + + You have successfully installed the official JupyterHub Helm chart! + +### Installation info + + - Kubernetes namespace: default + - Helm release name: flux-jupyter + - Helm chart version: 3.0.2 + - JupyterHub version: 4.0.2 + - Hub pod packages: See https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/3.0.2/images/hub/requirements.txt + +### Followup links + + - Documentation: https://z2jh.jupyter.org + - Help forum: https://discourse.jupyter.org + - Social chat: https://gitter.im/jupyterhub/jupyterhub + - Issue tracking: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues + +### Post-installation checklist + + - Verify that created Pods enter a Running state: + + kubectl --namespace=default get pod + + If a pod is stuck with a Pending or ContainerCreating status, diagnose with: + + kubectl --namespace=default describe pod + + If a pod keeps restarting, diagnose with: + + kubectl --namespace=default logs --previous + + - Verify an external IP is provided for the k8s Service proxy-public. + + kubectl --namespace=default get service proxy-public + + If the external ip remains , diagnose with: + + kubectl --namespace=default describe service proxy-public + + - Verify web based access: + + You have not configured a k8s Ingress resource so you need to access the k8s + Service proxy-public directly. + + If your computer is outside the k8s cluster, you can port-forward traffic to + the k8s Service proxy-public with kubectl to access it from your + computer. + + kubectl --namespace=default port-forward service/proxy-public 8080:http + + Try insecure HTTP access: http://localhost:8080 +``` + +
+ +### 3. Get Public Proxy + +Then to find the public proxy: + +```bash +kubectl get service proxy-public +``` +```console +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +proxy-public LoadBalancer 10.96.179.168 80:32530/TCP 7m22s +``` +or: + +```bash +kubectl get service proxy-public --output jsonpath='{.status.loadBalancer.ingress[].ip}' +``` + +Note that for Google, it looks like an ip address. For aws you get a string monster! + +```console +a054af2758c1549f780a433e5515a9d4-1012389935.us-east-2.elb.amazonaws.com +``` + +This might take a minute to fully be there - if it doesn't work immediately give it that. +At this point, you should be able to login as any user, open the notebook (nested two levels) +and interact with Flux! Remember that if you don't see the service, try deleting everything and +starting fresh. If that doesn't work, there might be some new error we didn't anticipate, +and you can look at logs. + +### Clean up + +For both: + +```bash +helm uninstall flux-jupyter +``` + +For Google Cloud: + +```bash +gcloud container clusters delete flux-jupyter +``` + +For AWS: + +```bash +# If you don't do this first, it will tell the pods are un-evictable and loop forever +$ kubectl delete pod --all-namespaces --all --force +# Then delete the cluster +$ eksctl delete cluster --config-file aws/eksctl-config.yaml --wait +``` + +In practice, you'll need to start deleting with `eksctl` and then you will see the pod eviction warning +(because they were re-created) and you'll need to run the command again, and then it will clean up. diff --git a/2024-RIKEN-AWS/JupyterNotebook/aws/cluster-autoscaler-autodiscover.yaml b/2024-RIKEN-AWS/JupyterNotebook/aws/cluster-autoscaler-autodiscover.yaml new file mode 100644 index 0000000..56869d0 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/aws/cluster-autoscaler-autodiscover.yaml @@ -0,0 +1,180 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler + name: cluster-autoscaler + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-autoscaler + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: [""] + resources: ["events", "endpoints"] + verbs: ["create", "patch"] + - apiGroups: [""] + resources: ["pods/eviction"] + verbs: ["create"] + - apiGroups: [""] + resources: ["pods/status"] + verbs: ["update"] + - apiGroups: [""] + resources: ["endpoints"] + resourceNames: ["cluster-autoscaler"] + verbs: ["get", "update"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["watch", "list", "get", "update"] + - apiGroups: [""] + resources: + - "namespaces" + - "pods" + - "services" + - "replicationcontrollers" + - "persistentvolumeclaims" + - "persistentvolumes" + verbs: ["watch", "list", "get"] + - apiGroups: ["extensions"] + resources: ["replicasets", "daemonsets"] + verbs: ["watch", "list", "get"] + - apiGroups: ["policy"] + resources: ["poddisruptionbudgets"] + verbs: ["watch", "list"] + - apiGroups: ["apps"] + resources: ["statefulsets", "replicasets", "daemonsets"] + verbs: ["watch", "list", "get"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"] + verbs: ["watch", "list", "get"] + - apiGroups: ["batch", "extensions"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "patch"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["create"] + - apiGroups: ["coordination.k8s.io"] + resourceNames: ["cluster-autoscaler"] + resources: ["leases"] + verbs: ["get", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "list", "watch"] + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"] + verbs: ["delete", "get", "update", "watch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cluster-autoscaler + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: cluster-autoscaler +subjects: + - kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + app: cluster-autoscaler +spec: + replicas: 1 + selector: + matchLabels: + app: cluster-autoscaler + template: + metadata: + labels: + app: cluster-autoscaler + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '8085' + spec: + priorityClassName: system-cluster-critical + securityContext: + runAsNonRoot: true + runAsUser: 65534 + fsGroup: 65534 + seccompProfile: + type: RuntimeDefault + serviceAccountName: cluster-autoscaler + containers: + - image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.26.2 + name: cluster-autoscaler + resources: + limits: + cpu: 100m + memory: 600Mi + requests: + cpu: 100m + memory: 600Mi + command: + - ./cluster-autoscaler + - --v=4 + - --stderrthreshold=info + - --cloud-provider=aws + - --skip-nodes-with-local-storage=false + - --expander=least-waste + - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/jupyterhub + volumeMounts: + - name: ssl-certs + mountPath: /etc/ssl/certs/ca-certificates.crt # /etc/ssl/certs/ca-bundle.crt for Amazon Linux Worker Nodes + readOnly: true + imagePullPolicy: "Always" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumes: + - name: ssl-certs + hostPath: + path: "/etc/ssl/certs/ca-bundle.crt" diff --git a/2024-RIKEN-AWS/JupyterNotebook/aws/config-aws-ssl.yaml b/2024-RIKEN-AWS/JupyterNotebook/aws/config-aws-ssl.yaml new file mode 100644 index 0000000..89125bc --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/aws/config-aws-ssl.yaml @@ -0,0 +1,79 @@ +# A few notes! +# The hub -> authentic class defaults to "dummy" +# We shouldn't need any image pull secrets assuming public +# There is a note about the database being a sqlite pvc +# (and a TODO for better solution for Kubernetes) + +# This is the concurrent spawn limit, likely should be increased (deafults to 64) +hub: + concurrentSpawnLimit: 128 + config: + DummyAuthenticator: + password: butter + JupyterHub: + admin_access: true + authenticator_class: dummy + db: + pvc: + # Defaults to 1Gi + storage: 32Gi + # Add the storageclass name, defaults to gp2 + storageClassName: gp3 + + # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this + image: + name: ghcr.io/flux-framework/flux-jupyter-hub + tag: "2023" + pullPolicy: Always + +# # https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders +# scheduling: +# podPriority: +# enabled: true +# userPlaceholder: +# # Specify 3 dummy user pods will be used as placeholders +# replicas: 3 + +proxy: + https: + enabled: true + hosts: + - tutorial.flux-framework.org + letsencrypt: + contactEmail: you@email.com + +# This is the "spawn" image +singleuser: + image: + name: ghcr.io/flux-framework/flux-jupyter-spawn + tag: "2023" + pullPolicy: Always + cpu: + limit: 2 + guarantee: 2 + memory: + limit: '4G' + guarantee: '4G' + cmd: /entrypoint.sh + + # This runs as the root user, who clones and changes ownership to uid 1000 + initContainers: + - name: init-myservice + image: ghcr.io/flux-framework/flux-jupyter-init:2023 + command: ["/entrypoint.sh"] + volumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan + + # This is how we get the tutorial files added + storage: + type: none + + # gitRepo volume is deprecated so we need another way + # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo + extraVolumes: + - name: flux-tutorial + emptyDir: {} + extraVolumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan diff --git a/2024-RIKEN-AWS/JupyterNotebook/aws/config-aws.yaml b/2024-RIKEN-AWS/JupyterNotebook/aws/config-aws.yaml new file mode 100644 index 0000000..c21e37f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/aws/config-aws.yaml @@ -0,0 +1,62 @@ +# A few notes! +# The hub -> authentic class defaults to "dummy" +# We shouldn't need any image pull secrets assuming public +# There is a note about the database being a sqlite pvc +# (and a TODO for better solution for Kubernetes) + +# This is the concurrent spawn limit, likely should be increased (deafults to 64) +hub: + concurrentSpawnLimit: 10 + config: + DummyAuthenticator: + password: butter + JupyterHub: + admin_access: true + authenticator_class: dummy + + # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this + image: + name: ghcr.io/flux-framework/flux-jupyter-hub + tag: "2023" + pullPolicy: Always + +# https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders +scheduling: + podPriority: + enabled: true + userPlaceholder: + # Specify 3 dummy user pods will be used as placeholders + replicas: 3 + +# This is the "spawn" image +singleuser: + image: + name: ghcr.io/flux-framework/flux-jupyter-spawn + tag: "2023" + pullPolicy: Always + cpu: + limit: 1 + memory: + limit: '4G' + cmd: /entrypoint.sh + + # This runs as the root user, who clones and changes ownership to uid 1000 + initContainers: + - name: init-myservice + image: ghcr.io/flux-framework/flux-jupyter-init:2023 + command: ["/entrypoint.sh"] + volumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan + + # This is how we get the tutorial files added + storage: + type: none + # gitRepo volume is deprecated so we need another way + # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo + extraVolumes: + - name: flux-tutorial + emptyDir: {} + extraVolumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan diff --git a/2024-RIKEN-AWS/JupyterNotebook/aws/eksctl-config.yaml b/2024-RIKEN-AWS/JupyterNotebook/aws/eksctl-config.yaml new file mode 100644 index 0000000..d88f0b1 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/aws/eksctl-config.yaml @@ -0,0 +1,110 @@ +# https://www.arhea.net/posts/2020-06-18-jupyterhub-amazon-eks +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig +metadata: + name: jupyterhub + region: us-east-2 + +iam: + withOIDC: true + serviceAccounts: + - metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + aws-usage: "cluster-ops" + app.kubernetes.io/name: cluster-autoscaler + + # https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md + attachPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "autoscaling:DescribeAutoScalingGroups" + - "autoscaling:DescribeAutoScalingInstances" + - "autoscaling:DescribeLaunchConfigurations" + - "autoscaling:DescribeTags" + - "autoscaling:SetDesiredCapacity" + - "autoscaling:TerminateInstanceInAutoScalingGroup" + - "ec2:DescribeLaunchTemplateVersions" + Resource: '*' + + - metadata: + name: ebs-csi-controller-sa + namespace: kube-system + labels: + aws-usage: "cluster-ops" + app.kubernetes.io/name: aws-ebs-csi-driver + attachPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "ec2:AttachVolume" + - "ec2:CreateSnapshot" + - "ec2:CreateTags" + - "ec2:CreateVolume" + - "ec2:DeleteSnapshot" + - "ec2:DeleteTags" + - "ec2:DeleteVolume" + - "ec2:DescribeInstances" + - "ec2:DescribeSnapshots" + - "ec2:DescribeTags" + - "ec2:DescribeVolumes" + - "ec2:DetachVolume" + Resource: '*' + +availabilityZones: ["us-east-2a", "us-east-2b", "us-east-2c"] +managedNodeGroups: + - name: ng-us-east-2a + iam: + withAddonPolicies: + autoScaler: true + instanceType: m5.large + volumeSize: 30 + desiredCapacity: 1 + minSize: 1 + maxSize: 3 + privateNetworking: true + availabilityZones: + - us-east-2a + # I didn't set this, but I know it's been an issue + # propagateASGTags: true + tags: + k8s.io/cluster-autoscaler/enabled: "true" + k8s.io/cluster-autoscaler/jupyterhub: "owned" + + - name: ng-us-east-2b + iam: + withAddonPolicies: + autoScaler: true + instanceType: m5.large + volumeSize: 30 + desiredCapacity: 1 + minSize: 1 + maxSize: 3 + privateNetworking: true + availabilityZones: + - us-east-2b + # propagateASGTags: true + tags: + k8s.io/cluster-autoscaler/enabled: "true" + k8s.io/cluster-autoscaler/jupyterhub: "owned" + + - name: ng-us-east-2c + iam: + withAddonPolicies: + autoScaler: true + instanceType: m5.large + volumeSize: 30 + desiredCapacity: 1 + minSize: 1 + maxSize: 3 + privateNetworking: true + availabilityZones: + - us-east-2c + # propagateASGTags: true + tags: + k8s.io/cluster-autoscaler/enabled: "true" + k8s.io/cluster-autoscaler/jupyterhub: "owned" diff --git a/2024-RIKEN-AWS/JupyterNotebook/aws/eksctl-radiuss-tutorial-2023.yaml b/2024-RIKEN-AWS/JupyterNotebook/aws/eksctl-radiuss-tutorial-2023.yaml new file mode 100644 index 0000000..93c04d9 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/aws/eksctl-radiuss-tutorial-2023.yaml @@ -0,0 +1,80 @@ +# https://www.arhea.net/posts/2020-06-18-jupyterhub-amazon-eks +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig +metadata: + name: jupyterhub + region: us-east-1 + +iam: + withOIDC: true + serviceAccounts: + - metadata: + name: ebs-csi-controller-sa + namespace: kube-system + labels: + aws-usage: "cluster-ops" + app.kubernetes.io/name: aws-ebs-csi-driver + attachPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "ec2:AttachVolume" + - "ec2:CreateSnapshot" + - "ec2:CreateTags" + - "ec2:CreateVolume" + - "ec2:DeleteSnapshot" + - "ec2:DeleteTags" + - "ec2:DeleteVolume" + - "ec2:DescribeInstances" + - "ec2:DescribeSnapshots" + - "ec2:DescribeTags" + - "ec2:DescribeVolumes" + - "ec2:DetachVolume" + Resource: '*' + +availabilityZones: + - us-east-1a + - us-east-1b + - us-east-1c + +managedNodeGroups: + - name: ng-us-east-1a + instanceType: m6a.8xlarge + volumeSize: 256 + volumeType: gp3 + volumeIOPS: 16000 + volumeThroughput: 512 + desiredCapacity: 1 + minSize: 1 + maxSize: 6 + privateNetworking: true + availabilityZones: + - us-east-1a + + - name: ng-us-east-1b + instanceType: m6a.8xlarge + volumeSize: 256 + volumeType: gp3 + volumeIOPS: 16000 + volumeThroughput: 512 + desiredCapacity: 1 + minSize: 1 + maxSize: 6 + privateNetworking: true + availabilityZones: + - us-east-1b + + - name: ng-us-east-1c + instanceType: m6a.8xlarge + volumeSize: 256 + volumeType: gp3 + volumeIOPS: 16000 + volumeThroughput: 512 + desiredCapacity: 1 + minSize: 1 + maxSize: 6 + privateNetworking: true + availabilityZones: + - us-east-1c + diff --git a/2024-RIKEN-AWS/JupyterNotebook/aws/storageclass.yaml b/2024-RIKEN-AWS/JupyterNotebook/aws/storageclass.yaml new file mode 100644 index 0000000..b9bef8f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/aws/storageclass.yaml @@ -0,0 +1,7 @@ +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: gp3 +provisioner: kubernetes.io/aws-ebs +volumeBindingMode: WaitForFirstConsumer +reclaimPolicy: Delete diff --git a/2024-RIKEN-AWS/JupyterNotebook/config-aws.yaml b/2024-RIKEN-AWS/JupyterNotebook/config-aws.yaml new file mode 100644 index 0000000..c1da104 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/config-aws.yaml @@ -0,0 +1,55 @@ +# A few notes! +# The hub -> authentic class defaults to "dummy" +# We shouldn't need any image pull secrets assuming public +# There is a note about the database being a sqlite pvc +# (and a TODO for better solution for Kubernetes) + +# This is the concurrent spawn limit, likely should be increased (deafults to 64) +hub: + concurrentSpawnLimit: 4 + # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this + image: + name: vanessa/flux-jupyter-hub + tag: latest + pullPolicy: Always + config: + KubeSpawner: + working_dir: /home/jovyan/flux-tutorial/flux-tutorial/2023-RADIUSS-AWS/JupyterNotebook/tutorial + DummyAuthenticator: + password: blueberrypancakes + JupyterHub: + admin_access: true + authenticator_class: dummy + +# This is the "spawn" image +singleuser: + image: + name: vanessa/flux-jupyter-spawn + tag: latest + pullPolicy: Always + cpu: + limit: 1 + memory: + limit: '4G' + cmd: /entrypoint.sh + + initContainers: + - name: init-myservice + image: alpine/git + command: ['git', 'clone', "https://github.com/rse-ops/flux-radiuss-tutorial-2023", "/home/jovyan/flux-radiuss-tutorial-2023"] + volumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan + + # This is how we get the tutorial files added + storage: + type: none + # gitRepo volume is deprecated so we need another way + # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo + extraVolumes: + - name: flux-tutorial + emptyDir: {} + extraVolumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan/flux-tutorial + # subPath: flux-radiuss-tutorial-2023 diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.hub b/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.hub new file mode 100644 index 0000000..595a53e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.hub @@ -0,0 +1,9 @@ +ARG JUPYTERHUB_VERSION=3.0.2 +FROM jupyterhub/k8s-hub:$JUPYTERHUB_VERSION + +# Add template override directory and copy our example +# Replace the default +USER root +RUN mv /usr/local/share/jupyterhub/templates/login.html /usr/local/share/jupyterhub/templates/_login.html +COPY ./docker/login.html /usr/local/share/jupyterhub/templates/login.html +USER jovyan diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.init b/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.init new file mode 100644 index 0000000..b4ba70c --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.init @@ -0,0 +1,13 @@ +FROM alpine/git + +ENV NB_USER=jovyan \ + NB_UID=1000 \ + HOME=/home/jovyan + +RUN adduser \ + -D \ + -g "Default user" \ + -u ${NB_UID} \ + -h ${HOME} \ + ${NB_USER} +COPY ./docker/init-entrypoint.sh /entrypoint.sh diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.spawn b/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.spawn new file mode 100644 index 0000000..a9c61b4 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/Dockerfile.spawn @@ -0,0 +1,106 @@ +FROM fluxrm/flux-sched:jammy + +# Based off of https://github.com/jupyterhub/zero-to-jupyterhub-k8s/tree/main/images/singleuser-sample +# Local usage +# docker run -p 8888:8888 -v $(pwd):/home/jovyan/work test + +USER root + +ENV NB_USER=jovyan \ + NB_UID=1000 \ + HOME=/home/jovyan + +RUN adduser \ + --disabled-password \ + --gecos "Default user" \ + --uid ${NB_UID} \ + --home ${HOME} \ + --force-badname \ + ${NB_USER} + +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + dnsutils \ + iputils-ping \ + python3-pip \ + tini \ + # requirement for nbgitpuller + git \ + && rm -rf /var/lib/apt/lists/* + +COPY ./requirements.txt ./requirements.txt +RUN ln -s /usr/bin/python3 /usr/bin/python && \ + python -m pip install -r requirements.txt && \ + python -m pip install ipython==7.34.0 && \ + python -m IPython kernel install + +# This is code to install DYAD +# This was added to the RADIUSS 2023 tutorials on AWS +RUN git clone https://github.com/openucx/ucx.git \ + && cd ucx \ + && git checkout v1.13.1 \ + && ./autogen.sh \ + && ./configure --disable-optimizations --enable-logging --enable-debug --disable-assertions --enable-mt --disable-params-check \ + --without-go --without-java --disable-cma --without-cuda --without-gdrcopy --without-verbs --without-knem --without-rmdacm \ + --without-rocm --without-xpmem --without-fuse3 --without-ugni --prefix=/usr CC=$(which gcc) CXX=$(which g++) \ + && make -j \ + && sudo make install \ + && cd .. \ + && rm -rf ucx + +RUN git clone https://github.com/TauferLab/dyad.git \ + && cd dyad \ + && git checkout ucx \ + && cp -r docs/demos/ecp_feb_2023 .. \ + && ./autogen.sh \ + && ./configure --prefix=/usr CC=$(which gcc) CXX=$(which g++) --enable-dyad-debug \ + && make -j \ + && sudo make install \ + && cd .. \ + && rm -rf dyad + +RUN mv ecp_feb_2023 /opt/dyad_demo \ + && cd /opt/dyad_demo \ + && CC=$(which gcc) CXX=$(which g++) make all \ + && cd .. + +# This adds the flux-tree command, which is provided in flux-sched source +# but not installed alongside production flux-core +COPY ./flux-tree/* /usr/libexec/flux/cmd/ +RUN chmod +x /usr/libexec/flux/cmd/flux-tree* + +# This customizes the launcher UI +# https://jupyter-app-launcher.readthedocs.io/en/latest/usage.html +RUN python3 -m pip install jupyter_app_launcher && \ + python3 -m pip install --upgrade jupyter-server && \ + mkdir -p /usr/local/share/jupyter/lab/jupyter_app_launcher +COPY ./docker/jupyter-launcher.yaml /usr/local/share/jupyter/lab/jupyter_app_launcher/config.yaml +ENV JUPYTER_APP_LAUNCHER_PATH /usr/local/share/jupyter/lab/jupyter_app_launcher + +# No permission errors here +USER ${NB_USER} +WORKDIR $HOME +COPY ./docker/flux-icon.png $HOME/flux-icon.png + +# note that previous examples are added via git volume in config.yaml +ENV SHELL=/usr/bin/bash +ENV FLUX_URI_RESOLVE_LOCAL=t + +EXPOSE 8888 +ENTRYPOINT ["tini", "--"] + +# This is for JupyterHub +COPY ./docker/entrypoint.sh /entrypoint.sh + +# This is for a local start +COPY ./docker/start.sh /start.sh +CMD ["flux", "start", "--test-size=4", "jupyter", "lab"] + +# This won't be available in K8s, but will be for a single container build +COPY ./tutorial /home/jovyan/flux-tutorial-2024 + +# Previous command for non-kubernetes +# CMD PATH=$HOME/.local/bin:$PATH \ +# flux start --test-size=4 /home/fluxuser/.local/bin/jupyterhub-singleuser diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/entrypoint.sh b/2024-RIKEN-AWS/JupyterNotebook/docker/entrypoint.sh new file mode 100755 index 0000000..8b11568 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +/usr/bin/flux start --test-size=4 /usr/local/bin/jupyterhub-singleuser \ No newline at end of file diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/flux-icon.png b/2024-RIKEN-AWS/JupyterNotebook/docker/flux-icon.png new file mode 100644 index 0000000..d50aa52 Binary files /dev/null and b/2024-RIKEN-AWS/JupyterNotebook/docker/flux-icon.png differ diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/init-entrypoint.sh b/2024-RIKEN-AWS/JupyterNotebook/docker/init-entrypoint.sh new file mode 100755 index 0000000..456fe9b --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/init-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Copy the notebook icon +# This would be for the customized launcher, not working yet +# wget https://flux-framework.org/assets/images/Flux-logo-mark-only-full-color.png +# mv Flux-logo-mark-only-full-color.png /home/jovyan/flux-icon.png + +# We need to clone to the user home, and then change permissions to uid 1000 +# That uid is shared by jovyan here and the spawn container +git clone https://github.com/rse-ops/flux-radiuss-tutorial-2023 /home/jovyan/flux-tutorial +chown -R 1000 /home/jovyan diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/jupyter-launcher.yaml b/2024-RIKEN-AWS/JupyterNotebook/docker/jupyter-launcher.yaml new file mode 100644 index 0000000..becbc80 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/jupyter-launcher.yaml @@ -0,0 +1,69 @@ +# These don't work, but we can try again next year. +#- title: Flux Tutorial Notebook +# description: This is the main Flux Framework Tutorial +# source: /home/jovyan/flux-tutorial/notebook/flux.ipynb +# cwd: /home/jovyan/flux-tutorial/notebook/ +# type: notebook +# catalog: Notebook +# icon: /home/jovyan/flux-icon.png + +# - title: Dyad Notebook Tutorial +# description: This is a tutorial for using Dyad +# source: /home/jovyan/flux-tutorial/notebook/dyad.ipynb +# cwd: /home/jovyan/flux-tutorial/notebook/ +# type: notebook +# catalog: Notebook +# icon: /home/jovyan/flux-icon.png + +- title: Flux Framework Portal + description: Flux Framework portal for projects, releases, and publication. + source: https://flux-framework.org/ + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Flux Documentation + source: https://flux-framework.readthedocs.io/en/latest/ + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Flux Cheat Sheet + source: https://flux-framework.org/cheat-sheet/ + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Flux Glossary of Terms + source: https://flux-framework.readthedocs.io/en/latest/glossary.html + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Flux Comics + source: https://flux-framework.readthedocs.io/en/latest/comics/fluxonomicon.html + description: come and meet FluxBird - the pink bird who knows things! + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Flux Learning Guide + source: https://flux-framework.readthedocs.io/en/latest/guides/learning_guide.html + description: learn about what Flux does, how it works, and real research applications + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Getting Started with Flux and Go + source: https://converged-computing.github.io/flux-go + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] +- title: Getting Started with Flux in C + source: https://converged-computing.github.io/flux-c-examples/ + description: ...looking for contributors! + type: url + catalog: Flux Resources + args: + sandbox: [ 'allow-same-origin', 'allow-scripts', 'allow-downloads', 'allow-modals', 'allow-popups'] diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/login.html b/2024-RIKEN-AWS/JupyterNotebook/docker/login.html new file mode 100644 index 0000000..d997a0f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/login.html @@ -0,0 +1,168 @@ +{% extends "page.html" %} +{% if announcement_login is string %} + {% set announcement = announcement_login %} +{% endif %} + +{% block login_widget %} +{% endblock %} + +{% block stylesheet %} +{{ super() }} + +{% endblock %} + +{% block main %} + +{% block login %} +
+{% block login_container %} + +
+ + + +
+

Flux Tutorial running on AWS

+ +
+
+
+

Sign in

+
+
+ + + + {% if login_error %} + + {% endif %} + + + + + + + + + {% block login_terms %} + {% if login_term_url %} + + {% endif %} + {% endblock login_terms %} + +
+
+
+{% endblock login_container %} +
+{% endblock login %} + +{% endblock %} + +{% block script %} +{{ super() }} + +{% endblock %} diff --git a/2024-RIKEN-AWS/JupyterNotebook/docker/start.sh b/2024-RIKEN-AWS/JupyterNotebook/docker/start.sh new file mode 100755 index 0000000..bad19ab --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/docker/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/bin/flux start --test-size=4 /usr/local/bin/jupyter-lab --ip=0.0.0.0 diff --git a/2024-RIKEN-AWS/JupyterNotebook/flux-tree/flux-tree b/2024-RIKEN-AWS/JupyterNotebook/flux-tree/flux-tree new file mode 100644 index 0000000..f12a9b8 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/flux-tree/flux-tree @@ -0,0 +1,847 @@ +#! /bin/bash + +############################################################## +# Copyright 2020 Lawrence Livermore National Security, LLC +# (c.f. AUTHORS, NOTICE.LLNS, LICENSE) +# +# This file is part of the Flux resource manager framework. +# For details, see https://github.com/flux-framework. +# +# SPDX-License-Identifier: LGPL-3.0 +############################################################## + +set -o errexit +set -o pipefail +#set -o xtrace + +FT_TOPO='1' # store arg to --topology (e.g., 2x2) +FT_QUEUE='default' # store arg to --queue-policy (e.g., fcfs:easy) +FT_PARAMS='default' # store arg to --queue-params +FT_MATCH='default' # store arg to --match-policy (e.g., low:high) +FT_PERF_OUT='%^+_no' # store perf-out filename given by --perf-out +FT_PERF_FORMAT='{treeid:<15s} {elapse:>20f} {begin:>20f} {end:>20f} {match:>15f} '\ +'{njobs:>15d} {my_nodes:>5d} {my_cores:>4d} {my_gpus:>4d}' + # store perf-out with format given by --perf-format +FT_FXLOGS='%^+_no' # dir in which flux logs are produced +FT_FXRDIR='%^+_no' # flux rundir attribute to pass +FT_LEAF='no' # is this a leaf instance (given by --leaf)? +FT_G_PREFIX='tree' # store hierarchical path given by --prefix +FT_DRY_RUN='no' # --dry-run for testing? +FT_INTER='no' # is an option for internal levels given? +FT_MAX_EXIT_CODE=0 # maximum exit code detected +FT_MAX_FLUX_JOBID=0 # maximum flux jobid that reports max exit code +FT_MAX_TREE_ID="" # FLUX_TREE_ID that reports max exit code +FT_MAX_JOBSCRIPT_IX="" # FLUX_TREE_JOBSCRIPT_INDEX reporting max code + # --prefix is an internal-use-only option +FT_JOB_NAME='%^+_no' # job name to use when submitting children +ORIG_FLUXION_QMANAGER_OPTIONS='' # +ORIG_FLUXION_RESOURCE_OPTIONS='' # to apply and unapply FLUXION_RESOURCE options +ORIG_FLUXION_QMANAGER_RC_NOOP='' # module load options. +ORIG_FLUXION_RESOURCE_RC_NOOP='' # + +declare -i FT_NJOBS=1 # store num of jobs to run, given by --njobs +declare -i FT_NNODES=1 # store num of nodes assigned, given by --nnodes +declare -i FT_NCORES=1 # store num of cores per node (--ncores-per-node) +declare -i FT_NGPUS=0 # store num of gpus per node (--ngpus-per-node) +declare -r top_prefix='tree' # prefix name to identify the top Flux instance +declare -r t_delim='x' # topology delimiter +declare -r p_delim=':' # match policy delimiter +declare -r perf_format='%-15s %20s %20s %20s %15s %15s %5s %4s %4s' +declare -a FT_CL=() # save the jobscript command into an array +declare -A mp_policies=( # make sure to update this when match + [low]=1 # policies are updated. + [high]=1 + [locality]=1 + [variation]=1 + [default]=1 +) +declare -A qp_policies=( # make sure to update this when + [fcfs]=1 # queuing policies are updated. + [easy]=1 + [hybrid]=1 + [conservative]=1 + [default]=1 +) +declare -A q_params=( # make sure to update this when + [queue-depth]=1 # queuing parameters are updated. + [reservation-depth]=1 + [default]=1 +) +declare -a jobids # array to store a set of submitted job IDs + +declare -r long_opts='help,leaf,flux-logs:,flux-rundir:,nnodes:'\ +',ncores-per-node:,ngpus-per-node:,topology:,queue-policy:,queue-params:'\ +',match-policy:,njobs:,perf-out:,perf-format:,prefix:,job-name:,dry-run' +declare -r short_opts='hlf:r:N:c:g:T:Q:P:M:J:o:X:d' +declare -r prog=${0##*/} +declare -r usage=" +Usage: ${prog} [OPTIONS] -- Jobscript\n\ +\n\ +Create a Flux instance hierarchy according to the specified\n\ +policies and schedule/run the specified number\n\ +of Jobscripts at the last level of this hierarchy.\n\ +\n\ +If --topology=2x4 and --njobs=32 are given, for instance,\n\ +2 Flux instances will be spawned from within the current instance,\n\ +each of which will in turn spawn 4 child Flux instances, totaling\n\ +8 instances at the last level of this hierarchy.\n\ +Once this is done, 4 jobs (of Jobscripts) will be scheduled\n\ +and executed at each of these 8 last-level Flux instances.\n\ +\n\ +The resources specified by --nnodes (total number of nodes) and\n\ +--ncores-per-node (total number of cores per node)\n\ +are recursively divided such that each sibling Flux instance\n\ +will be assigned to an equal split of the resources of their\n\ +parent instance. In addition, --ngpus-per-node can be given,\n\ +in which case the given GPU count will also be split.\n\ +If not given, it is assumed that there is no GPU on nodes.\n\ +\n\ +Jobscript is expected to submit one or more programs through\n\ +the flux-job submit command or its variants.\n\ +Jobscript is passed with five environment variables:\n\ +FLUX_TREE_ID, FLUX_TREE_JOBSCRIPT_INDEX, FLUX_TREE_NNODES,\n\ +FLUX_TREE_NCORES_PER_NODE and FLUX_TREE_NGPUS_PER_NODE.\n\ +FLUX_TREE_ID is an ID string uniquely identifying the hierarchical\n\ +path of the Flux instance on which Jobscript is being executed.\n\ +FLUX_TREE_JOBSCRIPT_INDEX is the integer ID of each jobscript\n\ +invocation local to the Flux instance. It starts from 1 and\n\ +sequentially increases.\n\ +FLUX_TREE_NNODES is the number nodes assigned to the instance.\n\ +FLUX_TREE_NCORES_PER_NODE is the number of cores per node\n\ +assigned to the instance.\n\ +FLUX_TREE_NGPUS_PER_NODE is the number of GPUs per node\n\ +assigned to the instance.\n\ +\n\ +If --queue-policy (additionally --queue-params) and/or\n\ +--match-policy are given, each level of this hierarchy will\n\ +be set to the specified queuing and matching policies and\n\ +parameters. Otherwise, all levels will be configured\n\ +to be used either the default policies or policies specified\n\ +through the FLUXION_RESOURCE_OPTIONS and/or FLUXION_QMANAGER_OPTIONS\n\ +environment variables.\n\ +\n\ +If any one of Jobscripts returns a non-zero exit code, flux-tree\n\ +detects the script invocation exited with the highest code and print\n\ +both that exit code and the outputs printed from executing the script.\n\ +In this case, FLUX_TREE_ID and FLUX_TREE_JOBSCRIPT_INDEX are also\n\ +reported in the from of \${FLUX_TREE_ID}@index[\${FLUX_TREE_JOBSCRIPT_INDEX}]\n\ +\n\ +Options:\n\ + -h, --help Display this message\n\ + -l, --leaf Leaf instance. Directly submit jobs\n\ + to enclosing Flux instance. Mutually-exclusive\n\ + with internal tree-node options like -T.\n\ + (default=${FT_LEAF})\n\ + -f, --flux-logs=DIR Dump Flux logs for all instances into DIR\n\ + -r, --flux-rundir=DIR Set the rundir attribute of each Flux tree instance\n\ + into a subdirectory within DIR. The content\n\ + stores will be redirected to them as well\n\ + -N, --nnodes=NNODES Total num of nodes to use\n\ + (default=${FT_NNODES})\n\ + -c, --ncores-per-node=NCORES Total num of cores per node to use\n\ + (default=${FT_NCORES})\n\ + -g, --ngpus-per-node=NGPUS Total num of gpus per node to use\n\ + (default=${FT_NGPUS})\n\ + -T, --topology=HPOLICY Topology of Flux instance hierarchy:\n\ + e.g., 2x2 (default=${FT_TOPO})\n\ + -Q, --queue-policy=QPOLICY Queuing policy for each level of\n\ + the hierarchy: e.g., easy:fcfs\n\ + -P, --queue-params=QPARAMS Queuing parameters for each level of\n\ + the hierarchy: e.g.,\n\ + queue-depth=5:reservation-depth=5\n\ + -M, --match-policy=MPOLICY Match policy for each level of\n\ + the hierarchy: e.g., low:high\n\ + -J, --njobs=NJOBS Total num of Jobscripts to run\n\ + (default=${FT_NJOBS})\n\ + -o, --perf-out=FILENAME Dump the performance data into\n\ + the given file (default: don't print)\n\ + --perf-format=FORMAT Dump the performance data with the given\n\ + format. Uses the python format\n\ + specification mini-language.\n\ + Example: \"{treeid:<15s},{elapse:>20f}\"\n\ + --job-name=NAME Name to use when submitting child jobs\n\ + -- Stop parsing options after this\n\ +" + +die() { echo -e "${prog}:" "$@"; exit 1; } +warn() { echo -e "${prog}: warning:" "$@"; } +dr_print() { echo -e "${prog}: dry-run:" "$@"; } + +# +# Roll up the performance records for each Flux instance to the KVS +# guest namespace of the parent Flux instance or print them out if top level. +# +rollup() { + local prefix="${1}" + local blurb="${2}" + local out="${3}" + local num_children="${4}" + local format="${5}" + + if [[ "${prefix}" == "${top_prefix}" && "${out}" != "%^+_no" ]]; then + flux tree-helper --perf-out="${out}" --perf-format="${format}" \ + ${num_children} "tree-perf" "${FT_JOB_NAME}" <<< "${blurb}" + else + flux tree-helper ${num_children} "tree-perf" "${FT_JOB_NAME}" \ + <<< "${blurb}" + fi +} + + +# +# Return a JSON string out of the performance data passed. +# +jsonify() { + local prefix="${1}" + local njobs="${2}" + local nnodes="${3}" + local ncores="${4}" + local ngpus="${5}" + local begin="${6}" + local end="${7}" + local avg=0 + local avail="no" + local el_match=0 + + # Print resource match time only for internal study + # flux-resource isn't a public command + if [[ "x${FT_DRY_RUN}" = "xno" ]] + then + flux ion-resource -h > /dev/null 2>&1 && avail="yes" + fi + + if [[ "${avail}" = "yes" ]] + then + avg=$(flux ion-resource stat | grep "Avg" | awk '{print $4}') + el_match=$(awk "BEGIN {print ${avg}*${njobs}*1000000.0}") + fi + + local elapse=0 + elapse=$(awk "BEGIN {print ${end} - ${begin}}") + echo "{\"treeid\":\"${prefix}\",\"njobs\":${njobs},\"my_nodes\":${nnodes},\ +\"my_cores\":${ncores},\"my_gpus\":${ngpus},\"perf\":{\"begin\":${begin},\ +\"end\":${end},\"elapse\":${elapse},\"match\":${el_match}}}" +} + + +# +# Fetch the next topology parameter that will be passed to +# the next-level Flux instances. E.g., If the current level topology +# is 2x3x4, the topology handled at the next level will be 3x4. +# +next_topo() { + local topo="${1}" + local nx='' + local nfields=0 + nfields=$(echo "${topo}" | awk -F"${t_delim}" '{print NF}') + # Remove the first topo parameter + [[ ${nfields} -gt 1 ]] && nx="${topo#*${t_delim}}" + echo "${nx}" +} + + +# +# Fetch the next policy parameter that will be passed to +# the next-level Flux instances. E.g., If the current policy parameter +# is high:low:locality, the policies handled at the next level +# will be low:locality. +# +next_policy_or_param() { + local policy_or_param="${1}" + local nx="" + local nfields=0 + nfields=$(echo "${policy_or_param}" | awk -F"${p_delim}" '{print NF}') + [[ ${nfields} -gt 1 ]] && nx="${policy_or_param#*${p_delim}}" + echo "${nx}" +} + + +# +# Check if the given queuing policy is valid +# +qpolicy_check() { + local policy=${1%%${p_delim}*} + [[ "x${policy}" = "x" ]] && return 1 + [[ "${qp_policies["${policy}"]:-missing}" = "missing" ]] && return 1 + return 0 +} + + +# +# Check if the given match policy is valid +# +mpolicy_check() { + local policy=${1%%${p_delim}*} + [[ "x${policy}" = "x" ]] && return 1 + [[ "${mp_policies["${policy}"]:-missing}" = "missing" ]] && return 1 + return 0 +} + + +# +# Check if the given queue param is valid +# +qparams_check() { + local param='' + param=$(echo "${1}" | awk -F"${p_delim}" '{print $1}') + param=${1%%${p_delim}*} + local final_param='' + final_param=${param##*,} + + for i in $(seq 1 10) + do + local token1=${param%%,*} + local token2=${token1%=*} + [[ "x${token2}" = "x" ]] && return 1 + [[ "${q_params["${token2}"]:-missing}" = "missing" ]] && return 1 + [[ "x${token1}" = "x${final_param}" ]] && break + param=${param#*,} + done + return 0 +} + + +# +# Calculate the number of jobs to execute based on the number of Flux instances +# being used at a level and the rank of the instance amongst its siblings. +# +get_my_njobs(){ + local njobs="${1}" + local size="${2}" # rank starts from 1 + local rank="${3}" + echo $(( njobs / size + (size + njobs % size)/(size + rank) )) +} + + +# +# Calculate the total number of cores that will be assigned to a child +# Flux instance based on the total number of nodes and cores per node +# assigned to the current Flux instance as well as the size and rank parameter. +# +get_my_cores(){ + local nnodes="${1}" + local ncores="${2}" + local size="${3}" + local rank="${4}" + local t_cores=$(( nnodes * ncores )) + echo $(( t_cores / size + (size + t_cores % size) / (size + rank) )) +} + + +# +# Calculate the total number of GPUs that will be assigned to a child +# Flux instance based on the total number of nodes and GPUs per node +# assigned to the current Flux instance as well as the size and rank parameter. +# +get_my_gpus(){ + local nnodes="${1}" + local ngpus="${2}" + local size="${3}" + local rank="${4}" + local t_gpus=$(( nnodes * ngpus )) + echo $(( t_gpus / size + (size + t_gpus % size) / (size + rank) )) +} + + +# +# Adjust the number of Flux instances to spawn at the next level +# if the amount of resources managed by the parent instance is small. +# +get_effective_size(){ + local ncores="${1}" + local ngpus="${2}" + local size="${3}" + [[ ${ngpus} -ne 0 && ${ngpus} -lt ${size} ]] && size=${ngpus} + [[ ${ncores} -lt ${size} ]] && size=${ncores} + echo "${size}" +} + + +# +# Calculate the total number of nodes that will be assigned to a child +# Flux instance based on the total number of cores per node as well as +# the total number of cores assigned to this child instance. Returns +# minimum num of nodes required. +# +get_my_nodes(){ + local ncores="${1}" + local m_cores="${2}" + echo $(( m_cores / ncores + (ncores + m_cores % ncores) / (ncores + 1 ))) +} + + +# +# Apply all of the policies for the target Flux instance +# by setting environment variables. +# +apply_policies() { + local queue_policy="${1%%${p_delim}*}" + local queue_param="${2%%${p_delim}*}" + local match_policy="${3%%${p_delim}*}" + + ORIG_FLUXION_QMANAGER_OPTIONS=${FLUXION_QMANAGER_OPTIONS:-none} + ORIG_FLUXION_RESOURCE_OPTIONS=${FLUXION_RESOURCE_OPTIONS:-none} + ORIG_FLUXION_QMANAGER_RC_NOOP=${FLUXION_QMANAGER_RC_NOOP:-none} + ORIG_FLUXION_RESOURCE_RC_NOOP=${FLUXION_RESOURCE_RC_NOOP:-none} + unset FLUXION_QMANAGER_RC_NOOP + unset FLUXION_RESOURCE_RC_NOOP + + if [[ "${queue_policy}" != "default" ]] + then + export FLUXION_QMANAGER_OPTIONS="queue-policy=${queue_policy}" + fi + if [[ "${queue_param}" != "default" ]] + then + local qo="${FLUXION_QMANAGER_OPTIONS}" + export FLUXION_QMANAGER_OPTIONS="${qo:+${qo},}queue-params=${queue_param}" + fi + if [[ "${match_policy}" != "default" ]] + then + export FLUXION_RESOURCE_OPTIONS="hwloc-allowlist=node,core,gpu \ +policy=${match_policy}" + fi + if [[ "x${FT_DRY_RUN}" = "xyes" ]] + then + dr_print "FLUXION_QMANAGER_OPTIONS:${FLUXION_QMANAGER_OPTIONS}" + dr_print "FLUXION_RESOURCE_OPTIONS:${FLUXION_RESOURCE_OPTIONS}" + fi +} + + +# +# Undo all of the policies set for the target Flux instance +# by unsetting environment variables. +# +unapply_policies() { + unset FLUXION_QMANAGER_OPTIONS + unset FLUXION_RESOURCE_OPTIONS + + if [ "${ORIG_FLUXION_QMANAGER_OPTIONS}" != "none" ] + then + export FLUXION_QMANAGER_OPTIONS="${ORIG_FLUXION_QMANAGER_OPTIONS}" + fi + if [ "${ORIG_FLUXION_RESOURCE_OPTIONS}" != "none" ] + then + export FLUXION_RESOURCE_OPTIONS="${ORIG_FLUXION_RESOURCE_OPTIONS}" + fi + if [ "${ORIG_FLUXION_QMANAGER_RC_NOOP}" != "none" ] + then + export FLUXION_QMANAGER_RC_NOOP="${ORIG_FLUXION_QMANAGER_RC_NOOP}" + fi + if [ "${ORIG_FLUXION_RESOURCE_RC_NOOP}" != "none" ] + then + export FLUXION_RESOURCE_RC_NOOP="${ORIG_FLUXION_RESOURCE_RC_NOOP}" + fi + if [[ "x${FT_DRY_RUN}" = "xyes" ]] + then + dr_print "FLUXION_QMANAGER_OPTIONS:${FLUXION_QMANAGER_OPTIONS}" + dr_print "FLUXION_RESOURCE_OPTIONS:${FLUXION_RESOURCE_OPTIONS}" + dr_print "FLUXION_QMANAGER_RC_NOOP:${FLUXION_QMANAGER_RC_NOOP}" + dr_print "FLUXION_RESOURCE_RC_NOOP:${FLUXION_RESOURCE_RC_NOOP}" + fi +} + + + +################################################################################ +# # +# Handle Leaf or Internal Flux Instances # +# # +################################################################################ + +# +# Execute the script. Export a predefined set of +# environment variables and execute the given jobscript. +# +execute() { + local prefix="${1}" + local nnodes="${2}" + local ncores="${3}" + local ngpus="${4}" + local njobs="${5}" + local rc=0 + + for job in $(seq 1 "${njobs}"); + do + export FLUX_TREE_ID="${prefix}" + export FLUX_TREE_JOBSCRIPT_INDEX="${job}" + export FLUX_TREE_NNODES="${nnodes}" + export FLUX_TREE_NCORES_PER_NODE="${ncores}" + export FLUX_TREE_NGPUS_PER_NODE="${ngpus}" + + if [[ "x${FT_DRY_RUN}" = "xyes" ]] + then + dr_print "FLUX_TREE_ID=${FLUX_TREE_ID}" + dr_print "FLUX_TREE_JOBSCRIPT_INDEX=${FLUX_TREE_JOBSCRIPT_INDEX}" + dr_print "FLUX_TREE_NCORES_PER_NODE=${FLUX_TREE_NCORES_PER_NODE}" + dr_print "FLUX_TREE_NGPUS_PER_NODE=${FLUX_TREE_NGPUS_PER_NODE}" + dr_print "FLUX_TREE_NNODES=${FLUX_TREE_NNODES}" + dr_print "eval ${FT_CL[@]}" + continue + else + rc=0 + "${FT_CL[@]}" || rc=$? + if [[ ${rc} -gt ${FT_MAX_EXIT_CODE} ]] + then + FT_MAX_EXIT_CODE=${rc} + FT_MAX_TREE_ID="${FLUX_TREE_ID}" + FT_MAX_JOBSCRIPT_IX="${FLUX_TREE_JOBSCRIPT_INDEX}" + fi + fi + done + + [[ "x${FT_DRY_RUN}" = "xno" ]] && flux queue drain + + if [[ "x${FT_MAX_TREE_ID}" != "x" ]] + then + warn "${FT_CL[@]}: exited with exit code (${FT_MAX_EXIT_CODE})" + warn "invocation id: ${FT_MAX_TREE_ID}@index[${FT_MAX_JOBSCRIPT_IX}]" + warn "output displayed above, if any" + fi + + unset FLUX_TREE_ID + unset FLUX_TREE_NNODES + unset FLUX_TREE_NCORES_PER_NODE +} + + +# +# Entry point to execute the job script. When this is invoke, +# the parent Flux instance has already been started. +# Measure the elapse time of the job script execution, and +# dump the performance data. +# +leaf() { + local prefix="${1}" + local nnodes="${2}" + local ncores="${3}" + local ngpus="${4}" + local njobs="${5}" + local perfout="${6}" + local format="${7}" + + # Begin Time Stamp + local B='' + B=$(date +%s.%N) + + execute "$@" + + # End Time Stamp + local E='' + E=$(date +%s.%N) + + local o='' + + o=$(jsonify "${prefix}" "${njobs}" "${nnodes}" "${ncores}" \ +"${ngpus}" "${B}" "${E}") + rollup "${prefix}" "${o}" "${perfout}" "0" "${format}" +} + + +# +# Roll up exit code from child instances +# +rollup_exit_code() { + local rc=0 + for job in "${jobids[@]}" + do + rc=0 + flux job status --exception-exit-code=255 ${job} || rc=$? + if [[ ${rc} -gt ${FT_MAX_EXIT_CODE} ]] + then + FT_MAX_EXIT_CODE=${rc} + FT_MAX_FLUX_JOBID=${job} + fi + done + + if [[ "${FT_MAX_FLUX_JOBID}" != "0" ]] + then + flux job attach ${FT_MAX_FLUX_JOBID} || true + fi +} + +# +# Submit the specified number of Flux instances at the next level of the calling +# instance. Use flux-tree recursively. Instances that have 0 jobs assigned are +# not launched. +# +submit() { + local prefix="${1}" + local nx_topo=$(next_topo "${2}") + local nx_queue=$(next_policy_or_param "${3}") + local nx_q_params=$(next_policy_or_param "${4}") + local nx_match=$(next_policy_or_param "${5}") + local nnodes="${6}" + local ncores="${7}" + local ngpus="${8}" + local size="${9}" + local njobs="${10}" + local log="${11}" + local rdir="${12}" + + # Flux instance rank-agnostic command-line options for the next level + local T="${nx_topo:+--topology=${nx_topo}}" + T="${T:---leaf}" + local Q="${nx_queue:+--queue-policy=${nx_queue}}" + local P="${nx_q_params:+--queue-params=${nx_q_params}}" + local M="${nx_match:+--match-policy=${nx_match}}" + local F='' + [[ "x${log}" != "x%^+_no" ]] && F="--flux-logs=${log}" + local R='' + [[ "x${rdir}" != "x%^+_no" ]] && R="--flux-rundir=${rdir}" + local rank=0 + + # Main Loop to Submit the Next-Level Flux Instances + size=$(get_effective_size "${ncores}" "${ngpus}" "${size}") + apply_policies "${3}" "${4}" "${5}" + for rank in $(seq 1 "${size}"); do + local my_cores=0 + my_cores=$(get_my_cores "${nnodes}" "${ncores}" "${size}" "${rank}") + local my_gpus=0 + my_gpus=$(get_my_gpus "${nnodes}" "${ngpus}" "${size}" "${rank}") + local my_njobs=0 + my_njobs=$(get_my_njobs "${njobs}" "${size}" "${rank}") + + [[ "${my_njobs}" -eq 0 ]] && break + + # Flux instance rank-aware command-line options + local J="--njobs=${my_njobs}" + local o='' + if [[ x"${log}" != "x%^+_no" ]] + then + if [[ "x${FT_DRY_RUN}" != "xyes" ]] + then + mkdir -p "${log}" + fi + o="-o,-Slog-filename=${log}/${prefix}.${rank}.log" + fi + if [[ x"${rdir}" != "x%^+_no" ]] + then + if [[ "x${FT_DRY_RUN}" != "xyes" ]] + then + rm -rf "${rdir}/${prefix}.${rank}.pfs" + mkdir -p "${rdir}/${prefix}.${rank}.pfs" + fi + o="${o:+${o} }-o,-Srundir=${rdir}/${prefix}.${rank}.pfs" + fi + local N=0 + N=$(get_my_nodes "${ncores}" "${my_cores}") + local c=0 + c=$((my_cores/N + (my_cores + my_cores % N)/(my_cores + 1))) + local g=0 + g=$((my_gpus/N + (my_gpus + my_gpus % N)/(my_gpus + 1))) + local G='' + [[ ${g} -gt 0 ]] && G="-g ${g}" + local X="--prefix=${prefix}.${rank}" + + if [[ "x${FT_DRY_RUN}" = "xyes" ]] + then + dr_print "Rank=${rank}: N=${N} c=${c} ${G:+g=${G}} ${o:+o=${o}}" + dr_print "Rank=${rank}: ${T:+T=${T}}" + dr_print "Rank=${rank}: ${Q:+Q=${Q}} ${P:+P=${P}} ${M:+M=${M}}" + dr_print "Rank=${rank}: ${X:+X=${X}} ${J:+J=${J}} ${FT_CL:+S=${FT_CL[@]}}" + dr_print "" + continue + fi + jobid=$(\ +flux submit --job-name=${FT_JOB_NAME} -N${N} -n${N} -c${c} ${G} \ + flux start ${o} \ + flux tree -N${N} -c${c} ${G} ${T} ${Q} ${P} ${M} ${F} ${R} ${X} ${J} \ + -- "${FT_CL[@]}") + jobids["${rank}"]="${jobid}" + done + + [[ "x${FT_DRY_RUN}" = "xno" ]] && flux queue drain && rollup_exit_code + unapply_policies +} + + +# +# Collect the performance record for sibling Flux instances at one level. +# For each child instance, get the performance record from the guest KVS +# namespace, which had all of the records gathered for the subtree rooted +# at this instance, and add that to the current record with its child key. +# +coll_perf() { + local prefix="${1}" + local nnodes="${2}" + local ncores="${3}" + local ngpus="${4}" + local njobs="${5}" + local begin="${6}" + local end="${7}" + local perfout="${8}" + local nchildren="${9}" + local format="${10}" + + # + # Make a JSON string from the performance data + # + local blurb='' + blurb=$(jsonify "${prefix}" "${njobs}" "${nnodes}" "${ncores}" "${ngpus}" "${begin}" "${end}") + rollup "${prefix}" "${blurb}" "${perfout}" "${nchildren}" "${format}" +} + + +# +# Entry point to submit child Flux instances at the next level from the +# calling Flux instance. Measure the elapse time of running all of these +# Flux instances. Collect the performance record for that level at the end. +# +internal() { + local prefix="${1}" + local nnodes="${6}" + local ncores="${7}" + local ngpus="${8}" + local njobs="${10}" + local perfout="${13}" + local format="${14}" + + # Begin Time Stamp + local B='' + B=$(date +%s.%N) + + submit "$@" + + # End Time Stamp + local E='' + E=$(date +%s.%N) + + if [[ "x${FT_DRY_RUN}" = "xyes" ]]; then + nchildren=0 + else + nchildren=${#jobids[@]} + fi + coll_perf "${prefix}" "${nnodes}" "${ncores}" "${ngpus}" \ +"${njobs}" "${B}" "${E}" "${perfout}" "${nchildren}" "${format}" +} + + +################################################################################ +# # +# Main # +# # +################################################################################ + +main() { + local leaf="${1}" # is this a leaf Flux instance? + local prefix="${2}" # id showing hierarchical path of the instance + local topo="${3}" # topology shape at the invoked level + local queue="${4}" # queuing policies at the invoked level and below + local param="${5}" # queue parameters at the invoked level and below + local match="${6}" # match policy shape at the invoked level + local nnodes="${7}" # num of nodes allocated to this instance + local ncores="${8}" # num of cores per node + local ngpus="${9}" # num of gpus per node + local njobs="${10}" # num of jobs assigned to this Flux instance + local flogs="${11}" # flux log output option + local frdir="${12}" # flux rundir attribute + local out="${13}" # perf output filename + local format="${14}" # perf output format + local size=0 + + if [[ ${leaf} = "yes" ]] + then + # + # flux-tree is invoked for a leaf: all of the internal Flux instances + # leading to this leaf have been instantiated and ${script} should + # be executed on the last-level Flux instance. + # + leaf "${prefix}" "${nnodes}" "${ncores}" "${ngpus}" "${njobs}" \ + "${out}" "${format}" + else + # + # flux-tree is invoked to instantiate ${size} internal Flux instances + # at the next level of the calling instance. + # + size=${topo%%${t_delim}*} + internal "${prefix}" "${topo}" "${queue}" "${param}" "${match}" \ + "${nnodes}" "${ncores}" "${ngpus}" "${size}" "${njobs}" \ + "${flogs}" "${frdir}" "${out}" "${format}" + fi + + exit ${FT_MAX_EXIT_CODE} +} + + +################################################################################ +# # +# Commandline Parsing and Validate Options # +# # +################################################################################ + +GETOPTS=$(/usr/bin/getopt -o ${short_opts} -l ${long_opts} -n "${prog}" -- "${@}") +eval set -- "${GETOPTS}" +rcopt=$? + +while true; do + case "${1}" in + -h|--help) echo -ne "${usage}"; exit 0 ;; + -l|--leaf) FT_LEAF="yes"; shift 1 ;; + -d|--dry-run) FT_DRY_RUN="yes"; shift 1 ;; + -f|--flux-logs) FT_FXLOGS="${2}"; shift 2 ;; + -r|--flux-rundir) FT_FXRDIR="${2}"; shift 2 ;; + -N|--nnodes) FT_NNODES=${2}; shift 2 ;; + -c|--ncores-per-node) FT_NCORES=${2}; shift 2 ;; + -g|--ngpus-per-node) FT_NGPUS=${2}; shift 2 ;; + -T|--topology) FT_TOPO="${2}"; FT_INTER="yes"; shift 2 ;; + -Q|--queue-policy) FT_QUEUE="${2}"; FT_INTER="yes"; shift 2 ;; + -P|--queue-params) FT_PARAMS="${2}"; FT_INTER="yes"; shift 2 ;; + -M|--match-policy) FT_MATCH="${2}"; FT_INTER="yes"; shift 2 ;; + -J|--njobs) FT_NJOBS=${2}; shift 2 ;; + -o|--perf-out) FT_PERF_OUT="${2}"; shift 2 ;; + --perf-format) FT_PERF_FORMAT="${2}"; shift 2 ;; + -X|--prefix) FT_G_PREFIX="${2}"; shift 2 ;; + --job-name) FT_JOB_NAME="${2}"; shift 2 ;; + --) shift; break; ;; + *) die "Invalid option '${1}'\n${usage}" ;; + esac +done + +FT_SCRIPT="${1}" +FT_CL=( "${@}" ) + +[[ "$#" -lt 1 || "${rcopt}" -ne 0 ]] && die "${usage}" + +[[ ! -x $(which ${FT_SCRIPT}) ]] && die "cannot execute ${FT_SCRIPT}!" + +[[ "${FT_NNODES}" -le 0 ]] && die "nnodes must be greater than 0!" + +[[ "${FT_NCORES}" -le 0 ]] && die "ncores must be greater than 0!" + +[[ "${FT_NGPUS}" -lt 0 ]] && die "incorrect ngpus!" + +qpolicy_check "${FT_QUEUE}" || die "invalid queue policy!" + +mpolicy_check "${FT_MATCH}" || die "invalid match policy!" + +qparams_check "${FT_PARAMS}" || die "invalid queue params!" + +if [[ "${FT_INTER}" = "yes" && "${FT_LEAF}" = "yes" ]] +then + die "--leaf must not be used together with internal tree-node options!" +fi + +# if the user did not set a name, then use a partially random string to prevent +# conflicts with other flux-tree instances during performance data collection +# via flux-tree-helper +if [[ "$FT_JOB_NAME" == '%^+_no' ]]; then + # code copied from: + # https://unix.stackexchange.com/questions/230673/how-to-generate-a-random-string + FT_JOB_NAME="flux-tree-$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)" +fi + + +################################################################################ +# # +# Invoke the Main Entry Level # +# # +################################################################################ + +main "${FT_LEAF}" "${FT_G_PREFIX}" "${FT_TOPO}" "${FT_QUEUE}" "${FT_PARAMS}" \ + "${FT_MATCH}" "${FT_NNODES}" "${FT_NCORES}" "${FT_NGPUS}" "${FT_NJOBS}" \ + "${FT_FXLOGS}" "${FT_FXRDIR}" "${FT_PERF_OUT}" "${FT_PERF_FORMAT}" + +# +# vi:tabstop=4 shiftwidth=4 expandtab +# diff --git a/2024-RIKEN-AWS/JupyterNotebook/flux-tree/flux-tree-helper.py b/2024-RIKEN-AWS/JupyterNotebook/flux-tree/flux-tree-helper.py new file mode 100644 index 0000000..eba17d5 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/flux-tree/flux-tree-helper.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +############################################################## +# Copyright 2020 Lawrence Livermore National Security, LLC +# (c.f. AUTHORS, NOTICE.LLNS, LICENSE) +# +# This file is part of the Flux resource manager framework. +# For details, see https://github.com/flux-framework. +# +# SPDX-License-Identifier: LGPL-3.0 +############################################################## + +import os +import sys +import time +import json +import argparse +import logging + +import flux +import flux.util +import flux.kvs +import flux.job + +LOGGER = logging.getLogger("flux-tree-helper") + + +def get_child_jobids(flux_handle, num_children, child_name): + """ + Get the jobids of num_children instances. Will repeatedly query the + job-info module until num_children jobids are collected, with sleeps + inbetween queries. + """ + jobids = set() + since = 0.0 + LOGGER.debug("Getting IDs of inactive children with name == %s", child_name) + while True: + for job in flux.job.job_list_inactive( + flux_handle, + max_entries=num_children, + since=since, + attrs=["t_inactive"], + name=child_name, + ).get_jobs(): + jobid = job["id"] + since = max(since, job["t_inactive"]) + jobids.add(jobid) + if len(jobids) >= num_children: + break + LOGGER.debug( + "Only %d out of %d children are inactive, sleeping before trying again", + len(jobids), + num_children, + ) + time.sleep(1) + return jobids + + +def get_this_instance_data(): + data = json.load(sys.stdin) + return data + + +def get_child_data(flux_handle, num_children, child_name, kvs_key): + child_data = [] + jobids = get_child_jobids(flux_handle, num_children, child_name) + for jobid in jobids: + kvs_dir = flux.job.job_kvs_guest(flux_handle, jobid) + child_data.append(kvs_dir[kvs_key]) + return child_data + + +def combine_data(this_instance_data, child_data): + this_instance_data["child"] = child_data + return this_instance_data + + +class PerfOutputFormat(flux.util.OutputFormat): + """ + Store a parsed version of the program's output format, + allowing the fields to iterated without modifiers, building + a new format suitable for headers display, etc... + """ + + # List of legal format fields and their header names + headings = dict( + treeid="TreeID", + elapse="Elapsed(sec)", + begin="Begin(Epoch)", + end="End(Epoch)", + match="Match(usec)", + njobs="NJobs", + my_nodes="NNodes", + my_cores="CPN", + my_gpus="GPN", + ) + + def __init__(self, fmt): + """ + Parse the input format fmt with string.Formatter. + Save off the fields and list of format tokens for later use, + (converting None to "" in the process) + + Throws an exception if any format fields do not match the allowed + list of headings above. + """ + # Support both new and old style OutputFormat constructor: + try: + super().__init__(fmt, headings=self.headings, prepend="") + except TypeError: + super().__init__(PerfOutputFormat.headings, fmt) + + +def write_data_to_file(output_filename, output_format, data): + def json_traverser(data): + fieldnames = PerfOutputFormat.headings.keys() + output = {k: v for k, v in data.items() if k in fieldnames} + output.update(data["perf"]) + yield output + for child in data["child"]: + yield from json_traverser(child) + + formatter = PerfOutputFormat(output_format) + with open(output_filename, "w") as outfile: + header = formatter.header() + "\n" + outfile.write(header) + fmt = formatter.get_format() + "\n" + for data_row in json_traverser(data): + # newline = formatter.format(data_row) + newline = fmt.format(**data_row) + outfile.write(newline) + + +def write_data_to_parent(flux_handle, kvs_key, data): + try: + parent_uri = flux_handle.flux_attr_get("parent-uri") + except FileNotFoundError: + return + parent_handle = flux.Flux(parent_uri) + + try: + parent_kvs_namespace = flux_handle.flux_attr_get("parent-kvs-namespace").decode( + "utf-8" + ) + except FileNotFoundError: + return + env_name = "FLUX_KVS_NAMESPACE" + os.environ[env_name] = parent_kvs_namespace + + flux.kvs.put(parent_handle, kvs_key, data) + flux.kvs.commit(parent_handle) + + +def parse_args(): + parser = argparse.ArgumentParser( + prog="flux-tree-helper", formatter_class=flux.util.help_formatter() + ) + parser.add_argument( + "num_children", + type=int, + help="number of children to collect data from. Should be 0 at leaves.", + ) + parser.add_argument( + "kvs_key", type=str, help="key to use when propagating data up through the tree" + ) + parser.add_argument( + "job_name", + type=str, + help="name of the child jobs to use when filtering the inactive jobs", + ) + parser.add_argument( + "--perf-out", + type=str, + help="Dump the performance data into the given file. " + "Assumed to be given at the root instance.", + ) + parser.add_argument( + "--perf-format", + type=str, + help="Dump the performance data with the given format string.", + ) + return parser.parse_args() + + +@flux.util.CLIMain(LOGGER) +def main(): + args = parse_args() + flux_handle = None + try: + flux_handle = flux.Flux() + except FileNotFoundError: + flux_handle = None + + LOGGER.debug("Getting this instance's data") + this_data = get_this_instance_data() + if flux_handle is not None and args.num_children > 0: + LOGGER.debug("Getting children's data") + child_data = get_child_data( + flux_handle, args.num_children, args.job_name, args.kvs_key + ) + else: + child_data = [] + LOGGER.debug("Combining data") + combined_data = combine_data(this_data, child_data) + if flux_handle is not None: + LOGGER.debug("Writing data to parent's KVS") + write_data_to_parent(flux_handle, args.kvs_key, combined_data) + if args.perf_out: + LOGGER.debug("Writing data to file") + write_data_to_file(args.perf_out, args.perf_format, combined_data) + + +if __name__ == "__main__": + main() diff --git a/2024-RIKEN-AWS/JupyterNotebook/gcp/config.yaml b/2024-RIKEN-AWS/JupyterNotebook/gcp/config.yaml new file mode 100644 index 0000000..b44626c --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/gcp/config.yaml @@ -0,0 +1,62 @@ +# A few notes! +# The hub -> authentic class defaults to "dummy" +# We shouldn't need any image pull secrets assuming public +# There is a note about the database being a sqlite pvc +# (and a TODO for better solution for Kubernetes) + +# This is the concurrent spawn limit, likely should be increased (deafults to 64) +hub: + concurrentSpawnLimit: 10 + config: + DummyAuthenticator: + password: butter + JupyterHub: + admin_access: true + authenticator_class: dummy + + # This is the image I built based off of jupyterhub/k8s-hub, 3.0.2 at time of writing this + image: + name: ghcr.io/flux-framework/flux-jupyter-hub + tag: "2023" + pullPolicy: Always + +# https://z2jh.jupyter.org/en/latest/administrator/optimization.html#scaling-up-in-time-user-placeholders +scheduling: + podPriority: + enabled: true + userPlaceholder: + # Specify 3 dummy user pods will be used as placeholders + replicas: 3 + +# This is the "spawn" image +singleuser: + image: + name: ghcr.io/flux-framework/flux-jupyter-spawn + tag: "2023" + pullPolicy: Always + cpu: + limit: 1 + memory: + limit: '4G' + cmd: /entrypoint.sh + + initContainers: + - name: init-myservice + image: alpine/git + command: ["git", "clone", "https://github.com/rse-ops/flux-radiuss-tutorial-2023", "/home/jovyan/flux-tutorial"] + volumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan + + # This is how we get the tutorial files added + storage: + type: none + + # gitRepo volume is deprecated so we need another way + # https://kubernetes.io/docs/concepts/storage/volumes/#gitrepo + extraVolumes: + - name: flux-tutorial + emptyDir: {} + extraVolumeMounts: + - name: flux-tutorial + mountPath: /home/jovyan/ diff --git a/2024-RIKEN-AWS/JupyterNotebook/requirements.txt b/2024-RIKEN-AWS/JupyterNotebook/requirements.txt new file mode 100644 index 0000000..0d9d99e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/requirements.txt @@ -0,0 +1,339 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# Use the "Run workflow" button at https://github.com/jupyterhub/zero-to-jupyterhub-k8s/actions/workflows/watch-dependencies.yaml +# +alembic==1.11.3 + # via jupyterhub +anyio==3.7.1 + # via jupyter-server +argon2-cffi==23.1.0 + # via + # jupyter-server + # nbclassic +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +arrow==1.2.3 + # via isoduration +asttokens==2.2.1 + # via stack-data +async-generator==1.10 + # via jupyterhub +async-lru==2.0.4 + # via jupyterlab +attrs==23.1.0 + # via + # jsonschema + # referencing +babel==2.12.1 + # via jupyterlab-server +backcall==0.2.0 + # via ipython +beautifulsoup4==4.12.2 + # via nbconvert +bleach==6.0.0 + # via nbconvert +certifi==2023.7.22 + # via requests +certipy==0.1.3 + # via jupyterhub +cffi==1.15.1 + # via + # argon2-cffi-bindings + # cryptography +charset-normalizer==3.2.0 + # via requests +comm==0.1.4 + # via ipykernel +cryptography==41.0.3 + # via pyopenssl +debugpy==1.6.7.post1 + # via ipykernel +decorator==5.1.1 + # via ipython +defusedxml==0.7.1 + # via nbconvert +executing==1.2.0 + # via stack-data +fastjsonschema==2.18.0 + # via nbformat +fqdn==1.5.1 + # via jsonschema +greenlet==2.0.2 + # via sqlalchemy +idna==3.4 + # via + # anyio + # jsonschema + # requests +ipykernel==6.25.1 + # via + # jupyterlab + # nbclassic +ipython==8.13.0 + # via ipykernel +ipython-genutils==0.2.0 + # via nbclassic +isoduration==20.11.0 + # via jsonschema +jedi==0.19.0 + # via ipython +jinja2==3.1.2 + # via + # jupyter-server + # jupyterhub + # jupyterlab + # jupyterlab-server + # nbclassic + # nbconvert +json5==0.9.14 + # via jupyterlab-server +jsonpointer==2.4 + # via jsonschema +jsonschema[format-nongpl]==4.19.0 + # via + # jupyter-events + # jupyter-telemetry + # jupyterlab-server + # nbformat +jsonschema-specifications==2023.7.1 + # via jsonschema +jupyter-client==8.3.0 + # via + # ipykernel + # jupyter-server + # nbclassic + # nbclient +jupyter-core==5.3.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterlab + # nbclassic + # nbclient + # nbconvert + # nbformat +jupyter-events==0.7.0 + # via jupyter-server +jupyter-lsp==2.2.0 + # via jupyterlab +jupyter-server==2.7.2 + # via + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # nbclassic + # nbgitpuller + # notebook-shim +jupyter-server-terminals==0.4.4 + # via jupyter-server +jupyter-telemetry==0.1.0 + # via jupyterhub +jupyterhub==4.0.2 + # via -r requirements.in +jupyterlab==4.0.5 + # via -r requirements.in +jupyterlab-pygments==0.2.2 + # via nbconvert +jupyterlab-server==2.24.0 + # via jupyterlab +mako==1.2.4 + # via alembic +markupsafe==2.1.3 + # via + # jinja2 + # mako + # nbconvert +matplotlib-inline==0.1.6 + # via + # ipykernel + # ipython +mistune==3.0.1 + # via nbconvert +nbclassic==1.0.0 + # via -r requirements.in +nbclient==0.8.0 + # via nbconvert +nbconvert==7.7.4 + # via + # jupyter-server + # nbclassic +nbformat==5.9.2 + # via + # jupyter-server + # nbclassic + # nbclient + # nbconvert +nbgitpuller==1.2.0 + # via -r requirements.in +nest-asyncio==1.5.7 + # via + # ipykernel + # nbclassic +notebook-shim==0.2.3 + # via + # jupyterlab + # nbclassic +oauthlib==3.2.2 + # via jupyterhub +overrides==7.4.0 + # via jupyter-server +packaging==23.1 + # via + # ipykernel + # jupyter-server + # jupyterhub + # jupyterlab + # jupyterlab-server + # nbconvert +pamela==1.1.0 + # via jupyterhub +pandocfilters==1.5.0 + # via nbconvert +parso==0.8.3 + # via jedi +pexpect==4.8.0 + # via ipython +pickleshare==0.7.5 + # via ipython +platformdirs==3.10.0 + # via jupyter-core +prometheus-client==0.17.1 + # via + # jupyter-server + # jupyterhub + # nbclassic +prompt-toolkit==3.0.39 + # via ipython +psutil==5.9.5 + # via ipykernel +ptyprocess==0.7.0 + # via + # pexpect + # terminado +pure-eval==0.2.2 + # via stack-data +pycparser==2.21 + # via cffi +pygments==2.16.1 + # via + # ipython + # nbconvert +pyopenssl==23.2.0 + # via certipy +python-dateutil==2.8.2 + # via + # arrow + # jupyter-client + # jupyterhub +python-json-logger==2.0.7 + # via + # jupyter-events + # jupyter-telemetry +pyyaml==6.0.1 + # via jupyter-events +pyzmq==25.1.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # nbclassic +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications + # jupyter-events +requests==2.31.0 + # via + # jupyterhub + # jupyterlab-server +rfc3339-validator==0.1.4 + # via + # jsonschema + # jupyter-events +rfc3986-validator==0.1.1 + # via + # jsonschema + # jupyter-events +rpds-py==0.9.2 + # via + # jsonschema + # referencing +ruamel-yaml==0.17.32 + # via jupyter-telemetry +ruamel-yaml-clib==0.2.7 + # via ruamel-yaml +send2trash==1.8.2 + # via + # jupyter-server + # nbclassic +six==1.16.0 + # via + # asttokens + # bleach + # python-dateutil + # rfc3339-validator +sniffio==1.3.0 + # via anyio +soupsieve==2.4.1 + # via beautifulsoup4 +sqlalchemy==2.0.20 + # via + # alembic + # jupyterhub +stack-data==0.6.2 + # via ipython +terminado==0.17.1 + # via + # jupyter-server + # jupyter-server-terminals + # nbclassic +tinycss2==1.2.1 + # via nbconvert +tornado==6.3.3 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterhub + # jupyterlab + # nbclassic + # nbgitpuller + # terminado +traitlets==5.9.0 + # via + # comm + # ipykernel + # ipython + # jupyter-client + # jupyter-core + # jupyter-events + # jupyter-server + # jupyter-telemetry + # jupyterhub + # jupyterlab + # matplotlib-inline + # nbclassic + # nbclient + # nbconvert + # nbformat +typing-extensions==4.7.1 + # via + # alembic + # sqlalchemy +uri-template==1.3.0 + # via jsonschema +urllib3==2.0.4 + # via requests +wcwidth==0.2.6 + # via prompt-toolkit +webcolors==1.13 + # via jsonschema +webencodings==0.5.1 + # via + # bleach + # tinycss2 +websocket-client==1.6.1 + # via jupyter-server diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/.gitignore b/2024-RIKEN-AWS/JupyterNotebook/tutorial/.gitignore new file mode 100644 index 0000000..3ebb2aa --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/.gitignore @@ -0,0 +1,2 @@ +flux*.out +.ipynb_checkpoints diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/.github/workflows/main.yml b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/.github/workflows/main.yml new file mode 100644 index 0000000..5d301e8 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/.github/workflows/main.yml @@ -0,0 +1,33 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +on: [pull_request] +jobs: + check-pr: + name: check formatting + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - run: git fetch origin master + - uses: flux-framework/pr-validator@master + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Lint with flake8 + run: | + pip install flake8 + pip install black + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + black . diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/.mergify.yml b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/.mergify.yml new file mode 100644 index 0000000..65c6341 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/.mergify.yml @@ -0,0 +1,18 @@ +pull_request_rules: + - name: rebase and merge when passing all checks + conditions: + - base=master + - status-success="check formatting (3.6)" + - status-success="check formatting (3.7)" + - status-success="check formatting (3.8)" + - label="merge-when-passing" + - label!="work-in-progress" + - "approved-reviews-by=@flux-framework/core" + - "#approved-reviews-by>0" + - "#changes-requested-reviews-by=0" + - -title~=^\[*[Ww][Ii][Pp] + actions: + merge: + method: merge + strict: smart + strict_method: rebase diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/Makefile b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/Makefile new file mode 100644 index 0000000..f219905 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/Makefile @@ -0,0 +1,25 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile check spelling $(SCHEMA_DIRS) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +check: spelling + +spelling: + @$(SPHINXBUILD) -W -b spelling "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/README.md new file mode 100644 index 0000000..9208b12 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/README.md @@ -0,0 +1,73 @@ +**WARNING** + +This repository has been archived. It is no longer maintained and it is +likely the examples do not work or are no longer good or suggested +examples. + +Please look elswhere for examples. + +**Flux Workflow Examples** + +The examples contained here demonstrate and explain some simple use-cases with Flux, +and make use of Flux's command-line interface (CLI), Flux's C library, +and the Python and Lua bindings to the C library. + +**Requirements** + +The examples assume that you have installed: + +1. A recent version of Flux + +2. Python 3.6+ + +3. Lua 5.1+ + +**_1. [CLI: Job Submission](https://github.com/flux-framework/flux-workflow-examples/tree/master/job-submit-cli)_** + +Launch a flux instance and schedule/launch compute and io-forwarding jobs on +separate nodes using the CLI + +**_2. [Python: Job Submission](https://github.com/flux-framework/flux-workflow-examples/tree/master/job-submit-api)_** + +Schedule/launch compute and io-forwarding jobs on separate nodes using the Python bindings + +**_3. [Python: Job Submit/Wait](https://github.com/flux-framework/flux-workflow-examples/tree/master/job-submit-wait)_** + +Submit jobs and wait for them to complete using the Flux Python bindings + +**_4. [Python: Asynchronous Bulk Job Submission](https://github.com/flux-framework/flux-workflow-examples/tree/master/async-bulk-job-submit)_** + +Asynchronously submit jobspec files from a directory and wait for them to complete in any order + +**_5. [Python: Tracking Job Status and Events](https://github.com/flux-framework/flux-workflow-examples/tree/master/job-status-control)_** + +Submit job bundles, get event updates, and wait until all jobs complete + +**_6. [Python: Job Cancellation](https://github.com/flux-framework/flux-workflow-examples/tree/master/job-cancel)_** + +Cancel a running job + +**_7. [Lua: Use Events](https://github.com/flux-framework/flux-workflow-examples/tree/master/synchronize-events)_** + +Use events to synchronize compute and io-forwarding jobs running on separate +nodes + +**_8. [Python: Simple KVS Example](https://github.com/flux-framework/flux-workflow-examples/tree/master/kvs-python-bindings)_** + +Use KVS Python interfaces to store user data into KVS + +**_9. [CLI/Lua: Job Ensemble Submitted with a New Flux Instance](https://github.com/flux-framework/flux-workflow-examples/tree/master/job-ensemble)_** + +Submit job bundles, print live job events, and exit when all jobs are complete + +**_10. [CLI: Hierarchical Launching](https://github.com/flux-framework/flux-workflow-examples/tree/master/hierarchical-launching)_** + +Launch a large number of sleep 0 jobs + +**_11. [C/Lua: Use a Flux Comms Module](https://github.com/flux-framework/flux-workflow-examples/tree/master/comms-module)_** + +Use a Flux Comms Module to communicate with job elements + +**_12. [C/Python: A Data Conduit Strategy](https://github.com/flux-framework/flux-workflow-examples/tree/master/data-conduit)_** + +Attach to a job that receives OS time data from compute jobs diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/README.md new file mode 100644 index 0000000..719af07 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/README.md @@ -0,0 +1,99 @@ +## Python Asynchronous Bulk Job Submission + +Parts (a) and (b) demonstrate different implementations of the same basic use-case---submitting +large numbers of jobs to Flux. For simplicity, in these examples all of the jobs are identical. + +In part (a), we use the `flux.job.submit_async` and `flux.job.wait` functions to submit jobs and wait for them. +In part (b), we use the `FluxExecutor` class, which offers a higher-level interface. It is important to note that +these two different implementations deal with very different kinds of futures. +The executor's futures fulfill in the background and callbacks added to the futures may +be invoked by different threads; the `submit_async` futures do not fulfill in the background, callbacks are always +invoked by the same thread that added them, and sharing the futures among threads is not supported. + +### Setup - Downloading the Files + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/async-bulk-job-submit +``` + +### Part (a) - Using `submit_async` + +#### Description: Asynchronously submit jobspec files from a directory and wait for them to complete in any order + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -ppdebug` + +2. Make a **jobs** directory: + +`mkdir jobs` + +3. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +4. Store the jobspec of a `sleep 0` job in the **jobs** directory: + +`flux mini run --dry-run -n1 sleep 0 > jobs/0.json` + +5. Copy the jobspec of **job0** 1024 times to create a directory of 1025 `sleep 0` jobs: + +``for i in `seq 1 1024`; do cp jobs/0.json jobs/${i}.json; done`` + +6. Run the **bulksubmit.py** script and pass all jobspec in the **jobs** directory as an argument with a shell glob `jobs/*.json`: + +`./bulksubmit.py jobs/*.json` + +``` +bulksubmit: Starting... +bulksubmit: submitted 1025 jobs in 3.04s. 337.09job/s +bulksubmit: First job finished in about 3.089s +|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 100.0% (29.4 job/s) +bulksubmit: Ran 1025 jobs in 34.9s. 29.4 job/s +``` + +### Notes to Part (a) + +- `h = flux.Flux()` creates a new Flux handle which can be used to connect to and interact with a Flux instance. + +- `job_submit_async(h, jobspec.read(), waitable=True).then(submit_cb)` submits a jobspec, returning a future which will be fulfilled when the submission of this job is complete. + +`.then(submit_cb)`, called on the returned future, will cause our callback `submit_cb()` to be invoked when the submission of this job is complete and a jobid is available. To process job submission RPC responses and invoke callbacks, the flux reactor for handle `h` must be run: + +```python +if h.reactor_run() < 0: +οΏΌ h.fatal_error("reactor start failed") +``` + +The reactor will return automatically when there are no more outstanding RPC responses, i.e., all jobs have been submitted. + +- `job.wait(h)` waits for any job submitted with the `FLUX_JOB_WAITABLE` flag to transition to the **INACTIVE** state. + + +### Part (b) - Using FluxExecutor + +#### Description: Asynchronously submit a single command repeatedly + +If continuing from part (a), skip to step 3. + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -ppdebug` + +2. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Run the **bulksubmit_executor.py** script and pass the command (`/bin/sleep 0` in this example) and the number of times to run it (default is 100): + +`./bulksubmit_executor.py -n200 /bin/sleep 0` + +``` +bulksubmit_executor: submitted 200 jobs in 0.45s. 441.15job/s +bulksubmit_executor: First job finished in about 1.035s +|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 100.0% (24.9 job/s) +bulksubmit_executor: Ran 200 jobs in 8.2s. 24.4 job/s +``` diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/bulksubmit.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/bulksubmit.py new file mode 100755 index 0000000..c1a2e9a --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/bulksubmit.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import time +import sys +import flux + +from flux import job +from flux import constants + +t0 = time.time() +jobs = [] +label = "bulksubmit" + +# open connection to broker +h = flux.Flux() + + +def log(s): + print(label + ": " + s) + + +def progress(fraction, length=72, suffix=""): + fill = int(round(length * fraction)) + bar = "\u2588" * fill + "-" * (length - fill) + s = "\r|{0}| {1:.1f}% {2}".format(bar, 100 * fraction, suffix) + sys.stdout.write(s) + if fraction == 1.0: + sys.stdout.write("\n") + + +def submit_cb(f): + jobs.append(job.submit_get_id(f)) + + +# asynchronously submit jobspec files from a directory +log("Starting...") +for file in sys.argv[1:]: + with open(file) as jobspec: + job.submit_async(h, jobspec.read(), waitable=True).then(submit_cb) + +if h.reactor_run() < 0: + h.fatal_error("reactor start failed") + +total = len(jobs) +dt = time.time() - t0 +jps = len(jobs) / dt +log("submitted {0} jobs in {1:.2f}s. {2:.2f}job/s".format(total, dt, jps)) + +count = 0 +while count < total: + # wait for jobs to complete in any order + job.wait(h) + count = count + 1 + if count == 1: + log("First job finished in about {0:.3f}s".format(time.time() - t0)) + suffix = "({0:.1f} job/s)".format(count / (time.time() - t0)) + progress(count / total, length=58, suffix=suffix) + +dt = time.time() - t0 +log("Ran {0} jobs in {1:.1f}s. {2:.1f} job/s".format(total, dt, total / dt)) + +# vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/bulksubmit_executor.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/bulksubmit_executor.py new file mode 100755 index 0000000..5280863 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/async-bulk-job-submit/bulksubmit_executor.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +import time +import sys +import argparse +import concurrent.futures as cf + +from flux.job import FluxExecutor, JobspecV1 + + +def log(label, s): + print(label + ": " + s) + + +def progress(fraction, length=72, suffix=""): + fill = int(round(length * fraction)) + bar = "\u2588" * fill + "-" * (length - fill) + s = f"\r|{bar}| {100 * fraction:.1f}% {suffix}" + sys.stdout.write(s) + if fraction == 1.0: + sys.stdout.write("\n") + + +def main(): + parser = argparse.ArgumentParser( + description="Submit a command repeatedly using FluxExecutor" + ) + parser.add_argument( + "-n", + "--njobs", + type=int, + metavar="N", + help="Set the total number of jobs to run", + default=100, + ) + parser.add_argument("command", nargs=argparse.REMAINDER) + args = parser.parse_args() + if not args.command: + args.command = ["true"] + t0 = time.perf_counter() + label = "bulksubmit_executor" + with FluxExecutor() as executor: + compute_jobspec = JobspecV1.from_command(args.command) + futures = [executor.submit(compute_jobspec) for _ in range(args.njobs)] + # wait for the jobid for each job, as a proxy for the job being submitted + for fut in futures: + fut.jobid() + # all jobs submitted - print timings + dt = time.perf_counter() - t0 + jps = args.njobs / dt + log(label, f"submitted {args.njobs} jobs in {dt:.2f}s. {jps:.2f}job/s") + # wait for jobs to complete + for i, _ in enumerate(cf.as_completed(futures)): + if i == 0: + log( + label, + f"First job finished in about {time.perf_counter() - t0:.3f}s", + ) + jps = (i + 1) / (time.perf_counter() - t0) + progress((i + 1) / args.njobs, length=58, suffix=f"({jps:.1f} job/s)") + # print time summary + dt = time.perf_counter() - t0 + log(label, f"Ran {args.njobs} jobs in {dt:.1f}s. {args.njobs / dt:.1f} job/s") + + +if __name__ == "__main__": + main() diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/Makefile b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/Makefile new file mode 100644 index 0000000..ccc018d --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/Makefile @@ -0,0 +1,19 @@ +all: capp.so ioapp.so + +FLUX_CORE_LIBS = $(shell pkg-config --libs flux-core) +FLUX_CORE_INCLUDES = $(shell pkg-config --cflags flux-core) + +ioapp.so: ioapp.o + gcc -Wl,--no-undefined --disable-static -shared -export-dynamic $^ -o $@ $(FLUX_CORE_LIBS) + +ioapp.o: app.c + gcc $(FLUX_CORE_INCLUDES) $^ -DIO_SERVICE=1 -fPIC -c -o $@ + +capp.so: capp.o + gcc -Wl,--no-undefined --disable-static -shared -export-dynamic $^ -o $@ $(FLUX_CORE_LIBS) + +capp.o: app.c + gcc $(FLUX_CORE_INCLUDES) $^ -DCOMP_SERVICE=1 -fPIC -c -o $@ + +clean: + rm *.o *.so diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/README.md new file mode 100644 index 0000000..3acdc5c --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/README.md @@ -0,0 +1,36 @@ +### Using a Flux Comms Module + +#### Description: Use a Flux comms module to communicate with job elements + +##### Setup + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/comms-module +``` + +##### Execution + +1. `salloc -N3 -ppdebug` + +2. Point to `flux-core`'s `pkgconfig` directory: + +| Shell | Command | +| ----- | ---------- | +| tcsh | `setenv PKG_CONFIG_PATH /lib/pkgconfig` | +| bash/zsh | `export PKG_CONFIG_PATH='/lib/pkgconfig'` | + +3. `make` + +4. Add the directory of the modules to `FLUX_MODULE_PATH`; if the module was +built in the current dir: + +`export FLUX_MODULE_PATH=${FLUX_MODULE_PATH}:$(pwd)` + +5. `srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +6. `flux submit -N 2 -n 2 ./compute.lua 120` + +7. `flux submit -N 1 -n 1 ./io-forwarding.lua 120` diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/app.c b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/app.c new file mode 100644 index 0000000..336ecfb --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/app.c @@ -0,0 +1,129 @@ +#include +#include +#include + +#if !defined (IO_SERVICE) && !defined (COMP_SERVICE) +# error "Either IO_SERVICE or COMP_SERVICE macro is needed" +#endif + +struct app_ctx { + flux_t *h; + int count; + flux_msg_handler_t **handlers; +}; + +static void freectx (void *arg) +{ + struct app_ctx *ctx = (struct app_ctx *)arg; + flux_msg_handler_delvec (ctx->handlers); + free (ctx); +} + +static struct app_ctx *getctx (flux_t *h) +{ +#if IO_SERVICE + struct app_ctx *ctx = flux_aux_get (h, "ioapp"); +#elif COMP_SERVICE + struct app_ctx *ctx = flux_aux_get (h, "capp"); +#endif + if (!ctx) { + ctx = malloc (sizeof (*ctx)); + ctx->count = 0; + ctx->handlers = NULL; +#if IO_SERVICE + flux_aux_set (h, "ioapp", ctx, freectx); +#elif COMP_SERVICE + flux_aux_set (h, "capp", ctx, freectx); +#endif + } + return ctx; +} + +#if IO_SERVICE +static void io_request_cb (flux_t *h, flux_msg_handler_t *w, + const flux_msg_t *msg, void *arg) +{ + const char *topic = NULL; + struct app_ctx *ctx = getctx (h); + int data = 0; + + if (flux_request_unpack (msg, &topic, "{s:i}", "data", &data)) + goto error; + ctx->count++; + if (flux_respond_pack (h, msg, "{s:i}", "count", ctx->count) < 0) + flux_log_error (h, "%s", __FUNCTION__); + flux_log (h, LOG_DEBUG, "count: %d", ctx->count); + return; + +error: + flux_log_error (h, "%s", __FUNCTION__); + if (flux_respond (h, msg, NULL) < 0) + flux_log_error (h, "%s: flux_respond", __FUNCTION__); +} +#endif + +#if COMP_SERVICE +static void comp_request_cb (flux_t *h, flux_msg_handler_t *w, + const flux_msg_t *msg, void *arg) +{ + const char *topic = NULL; + struct app_ctx *ctx = getctx (h); + int data = 0; + + flux_log (h, LOG_INFO, "comp_request_cb:"); + if (flux_request_unpack (msg, &topic, "{s:i}", "data", &data)) + goto error; + + ctx->count++; + if (flux_respond_pack (h, msg, "{s:i}", "count", ctx->count) < 0) + flux_log_error (h, "%s", __FUNCTION__); + return; + +error: + flux_log_error (h, "%s", __FUNCTION__); + if (flux_respond (h, msg, NULL) < 0) + flux_log_error (h, "%s: flux_respond", __FUNCTION__); +} +#endif + +static struct flux_msg_handler_spec htab[] = { +#if IO_SERVICE + { FLUX_MSGTYPE_REQUEST, "ioapp.io", io_request_cb, 0 }, +#endif + +#if COMP_SERVICE + { FLUX_MSGTYPE_REQUEST, "capp.comp", comp_request_cb, 0 }, +#endif + + FLUX_MSGHANDLER_TABLE_END +}; + + +int mod_main (flux_t *h, int argc, char **argv) +{ + + struct app_ctx *ctx = getctx (h); + if (flux_msg_handler_addvec (h, htab, (void *)h, + &ctx->handlers) < 0) { + flux_log (ctx->h, LOG_ERR, "flux_msg_handler_addvec: %s", strerror (errno)); + goto done; + } + + if (flux_reactor_run (flux_get_reactor (h), 0) < 0) { + flux_log (h, LOG_ERR, "flux_reactor_run: %s", strerror (errno)); + goto done; + } + +done: + return 0; +} + +#if IO_SERVICE +MOD_NAME ("ioapp"); +#elif COMP_SERVICE +MOD_NAME ("capp"); +#endif + +/* + * vi:tabstop=4 shiftwidth=4 expandtab + */ diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/compute.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/compute.lua new file mode 100755 index 0000000..f505f54 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/compute.lua @@ -0,0 +1,62 @@ +#!/usr/bin/env lua + +local f, err = require 'flux' .new () +local amount = tonumber (arg[1]) or 120 +local rank = tonumber (os.getenv('FLUX_TASK_RANK')) or 0 +local frank = tonumber (os.getenv('FLUX_LOCAL_RANKS')) or 0 +io.stdout:setvbuf ("no") + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: compute.lua seconds") + print (" Compute for seconds") + os.exit (1) +end + +-- subscribe app.io.go event +local rc, err = f:subscribe ("app.io.go") +if not rc then + print ("Failed to subscribe an event, %s", err) + os.exit (1) +end + +-- the leader rank of compute job installs app module +if rank == 0 then + os.execute ("flux module load -r " .. 0 .. " capp") + os.execute ("flux module list") +end + +-- wait for an event sent from the leader of io-forwarding job to sync +-- between io job's installing the app module and sending a request later +print ("Block until we hear go message from the an io forwarder") +local rc, err = f:recv_event () +if not rc then + print ("Failed to receive an event, %s", err) + os.exit (1) +end + +if rank == 0 then + local rc, err = f:sendevent ({ data = "please proceed" }, "app.comp.go") + if not rc then error (err) end + print ("Sent a go event") +end + +local resp, err = f:rpc ("ioapp.io", { data = rank }) +if not resp then + if err == "Function not implemented" then + print ("ioapp.io request handler isn't loaded") + else + print (err) + end +else + print ("Count so far: " .. resp.count) +end + +print ("Will compute for " .. amount .. " seconds") +sleep (amount) +f:unsubscribe ("app.io.go") + +-- vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/io-forwarding.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/io-forwarding.lua new file mode 100755 index 0000000..0f9f78f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/comms-module/io-forwarding.lua @@ -0,0 +1,57 @@ +#!/usr/bin/env lua + +local flux = require 'flux' +local f = flux.new () +local amount = tonumber (arg[1]) or 120 +local rank = tonumber (os.getenv('FLUX_TASK_RANK')) or 0 +local frank = tonumber (os.getenv('FLUX_LOCAL_RANKS')) or 0 +io.stdout:setvbuf ("no") + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: io-forward.lua seconds") + print (" Forward I/O requests for seconds") + os.exit (1) +end + +-- subscribe app.comp.go event +local rc, err = f:subscribe ("app.comp.go") +if not rc then + print ("Failed to subscribe an event, %s", err) + os.exit (1) +end + +if rank == 0 then + os.execute ("flux module load -r " .. 0 .. " ioapp") + os.execute ("flux module list") + local rc, err = f:sendevent ({ data = "please proceed" }, "app.io.go") + if not rc then error (err) end + print ("Sent a go event") +end + +-- Wait for an event sent from the leader of compute job to sync +-- between compute job's installing the app module and sending a request later +print ("Block until we hear go message from the a leader compute process") +local rc, err = f:recv_event () +if not rc then + print ("Failed to receive an, %s", err) + os.exit (1) +end + +local resp, err = f:rpc ("capp.comp", { data = rank }) +if not resp then + if err == "Function not implemented" then + print ("capp.comp request handler isn't loaded") + else + print (err) + end +end + +print ("Will forward IO requests for " .. amount .. " seconds") +sleep (amount) +f:unsubscribe ("app.comp.go") + +-- vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/conf.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/conf.py new file mode 100644 index 0000000..75f3e5c --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/conf.py @@ -0,0 +1,83 @@ +############################################################### +# Copyright 2020 Lawrence Livermore National Security, LLC +# (c.f. AUTHORS, NOTICE.LLNS, COPYING) +# +# This file is part of the Flux resource manager framework. +# For details, see https://github.com/flux-framework. +# +# SPDX-License-Identifier: LGPL-3.0 +############################################################### + +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'Flux' +copyright = '''Copyright 2020 Lawrence Livermore National Security, LLC and Flux developers. + +SPDX-License-Identifier: LGPL-3.0''' +author = 'This page is maintained by the Flux community.' + +# The full version, including alpha/beta/rc tags +release = '0.1.0' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.intersphinx', + 'sphinxcontrib.spelling', + 'recommonmark', +] + +# sphinxcontrib.spelling settings +spelling_word_list_filename = [ + 'spell.en.pws' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'README.md'] + +master_doc = 'index' +source_suffix = ['.rst', '.md'] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [ +] + +# -- Options for man output ------------------------------------------------- + +man_pages = [ +] diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/Makefile b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/Makefile new file mode 100644 index 0000000..56abc33 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/Makefile @@ -0,0 +1,13 @@ +all: conduit.so + +FLUX_CORE_LIBS = $(shell pkg-config --libs flux-core) +FLUX_CORE_INCLUDES = $(shell pkg-config --cflags flux-core) + +conduit.so: conduit.o + gcc -Wl,--no-undefined --disable-static -shared -export-dynamic $^ -o $@ $(FLUX_CORE_LIBS) + +conduit.o: conduit.c + gcc $(FLUX_CORE_INCLUDES) $^ -fPIC -c -o $@ + +clean: + rm *.o *.so diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/README.md new file mode 100644 index 0000000..a68aedb --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/README.md @@ -0,0 +1,84 @@ +## A Data Conduit Strategy + +### Description: Use a data stream to send packets through + +#### Setup + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/data-conduit +``` + +#### Execution + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -ppdebug` + +2. Point to `flux-core`'s `pkgconfig` directory: + +| Shell | Command | +| ----- | ---------- | +| tcsh | `setenv PKG_CONFIG_PATH /lib/pkgconfig` | +| bash/zsh | `export PKG_CONFIG_PATH='/lib/pkgconfig'` | + +3. `make` + +4. Add the directory of the modules to `FLUX_MODULE_PATH`, if the module was built in the current directory: + +`export FLUX_MODULE_PATH=${FLUX_MODULE_PATH}:$(pwd)` + +5. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +6. Submit the **datastore** script: + +`flux submit -N 1 -n 1 ./datastore.py` + +7. Submit and resubmit five **compute** scripts to send time data to **datastore**: + +`flux submit -N 1 -n 1 ./compute.py 1` + +`flux submit -N 1 -n 1 ./compute.py 1` + +`flux submit -N 1 -n 1 ./compute.py 1` + +`flux submit -N 1 -n 1 ./compute.py 1` + +`flux submit -N 1 -n 1 ./compute.py 1` + +8. Attach to the **datastore** job to see the data sent by the **compute.py** scripts: + +`flux job attach 1900070043648` + +``` +Starting.... +Module was loaded successfully... +finished initialize... +starting run() +Waiting for a packet +{u'test': 101} +Waiting for a packet +{u'test': 101, u'1578431137': u'os.time'} +Waiting for a packet +{u'test': 101, u'1578431137': u'os.time', u'1578431139': u'os.time'} +Waiting for a packet +{u'test': 101, u'1578431140': u'os.time', u'1578431137': u'os.time', u'1578431139': u'os.time'} +Waiting for a packet +{u'test': 101, u'1578431140': u'os.time', u'1578431137': u'os.time', u'1578431139': u'os.time', u'1578431141': u'os.time'} +Bye bye! +run finished... +``` + +--- + +### Notes + +- `f = flux.Flux()` creates a new Flux handle which can be used to connect to and interact with a Flux instance. + +- `kvs.put()` places the value of _udata_ under the key **"conduit"**. Once the key-value pair is put, the change must be committed with `kvs.commit()`. The value can then be retrieved with `kvs.get()`. + +- `f.rpc()` creates a new RPC object consisting of a specified topic and payload (along with additional flags) that are exchanged with a Flux service. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/compute.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/compute.py new file mode 100644 index 0000000..d03f871 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/compute.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import argparse +import time +import os +import re +import flux +import json +from flux import kvs +from flux.message import Message + +parser = argparse.ArgumentParser(description="compute for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +f = flux.Flux() +udata = "conduit" +kvs.put(f, "conduit", udata) +kvs.commit(f) + +cr = kvs.get(f, "conduit") +print(cr) + +os_time = int(time.time()) +payload = {str(os_time): "os.time"} +new_payload = {"data": json.dumps(payload)} +print("Sending ", json.dumps(new_payload)) + +# this data is ultimately flowed into the data store +f.rpc("conduit.put", new_payload, 0) + + +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/conduit.c b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/conduit.c new file mode 100644 index 0000000..790f84f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/conduit.c @@ -0,0 +1,182 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct conduit_ctx { + flux_t *h; + struct sockaddr_un server_sockaddr; + struct sockaddr_un client_sockaddr; + int client_sock; + bool connected; + char *sockname; + char *csockname; + flux_msg_handler_t **handlers; +}; + +static void freectx (void *arg) +{ + struct conduit_ctx *ctx = (struct conduit_ctx *)arg; + flux_msg_handler_delvec (ctx->handlers); + free (ctx->sockname); + free (ctx->csockname); + if (ctx->connected) + close (ctx->client_sock); + free (ctx); +} + +static struct conduit_ctx *getctx (flux_t *h) +{ + struct conduit_ctx *ctx = flux_aux_get (h, "conduit"); + if (!ctx) { + char *user = getenv ("USER"); + ctx = malloc (sizeof (*ctx)); + ctx->connected = false; + ctx->handlers = NULL; + asprintf (&(ctx->sockname), "/tmp/%s/mysock", user? user : ""); + asprintf (&(ctx->csockname),"/tmp/%s/mycsock", user? user : ""); + flux_aux_set (h, "conduit", ctx, freectx); + } + return ctx; +} + +/* Foward the received JSON string to the datastore.py */ +static int conduit_send (flux_t *h, const char *json_str) +{ + int rc = -1; + int n = 0; + struct conduit_ctx *ctx = getctx (h); + + n = (int) strlen (json_str); + if ((rc = send (ctx->client_sock, (void *)&n, sizeof (n), 0)) == -1) { + flux_log_error (h, "send error %s", __FUNCTION__); + return rc; + } + if ((rc = send (ctx->client_sock, (void *)json_str, n, 0)) == -1) { + flux_log_error (h, "send error %s", __FUNCTION__); + return rc; + } + flux_log (h, LOG_INFO, "conduit_send succeed"); + return 0; +} + +/* request callback called when conduit.put request is invoked */ +static void conduit_put_request_cb (flux_t *h, flux_msg_handler_t *w, + const flux_msg_t *msg, void *arg) +{ + int rc = -1; + const char *topic = NULL; + struct conduit_ctx *ctx = getctx (h); + const char *data = NULL; + + flux_log (h, LOG_INFO, "conduit_put_request_cb:"); + if (ctx->connected == false) { + flux_log (h, LOG_INFO, "conduit not connected"); + errno = ENOTCONN; + goto done; + } + if (flux_request_unpack (msg, &topic, "{s:s}", "data", &data)) { + flux_log_error (h, "%s", __FUNCTION__); + goto done; + } + if (conduit_send (h, data) < 0) + errno = EPROTO; +done: + if (flux_respond (h, msg, errno, NULL) < 0) + flux_log_error (h, "%s: flux_respond", __FUNCTION__); +} + +/* open the Unix domain socket to talk to datastore.py */ +static int conduit_open (flux_t *h) +{ + struct conduit_ctx *ctx = getctx (h); + int rc = -1; + int len = 0; + char buf[256]; + memset(&(ctx->server_sockaddr), 0, sizeof(struct sockaddr_un)); + memset(&(ctx->client_sockaddr), 0, sizeof(struct sockaddr_un)); + + if ((ctx->client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + flux_log (h, LOG_ERR, "SOCKET ERROR = %d\n", errno); + goto done; + } + + ctx->client_sockaddr.sun_family = AF_UNIX; + strcpy(ctx->client_sockaddr.sun_path, ctx->csockname); + len = sizeof(ctx->client_sockaddr); + unlink (ctx->csockname); + if ((rc = bind(ctx->client_sock, + (struct sockaddr *)&ctx->client_sockaddr, len)) == -1) { + flux_log (h, LOG_ERR, "BIND ERROR: %d\n", errno); + close(ctx->client_sock); + goto done; + } + flux_log (h, LOG_INFO, "Conduit client socket bound\n"); + + ctx->server_sockaddr.sun_family = AF_UNIX; + strcpy(ctx->server_sockaddr.sun_path, ctx->sockname); + if ((rc = connect(ctx->client_sock, + (struct sockaddr *)&ctx->server_sockaddr, len)) == -1) { + flux_log (h, LOG_ERR, "CONNECT ERROR = %d\n", errno); + close(ctx->client_sock); + goto done; + } + + ctx->connected = true; + flux_log (h, LOG_INFO, "Conduit socket connected\n"); + conduit_send (h, "{\"test\":101}"); + rc = 0; +done: + return rc; +} + + +static struct flux_msg_handler_spec htab[] = { + { FLUX_MSGTYPE_REQUEST, "conduit.put", conduit_put_request_cb, 0 }, + FLUX_MSGHANDLER_TABLE_END +}; + +int mod_main (flux_t *h, int argc, char **argv) +{ + uint32_t rank = 0; + struct conduit_ctx *ctx = getctx (h); + + if (conduit_open (h) < 0) { + flux_log (ctx->h, LOG_ERR, "conduit_open failed"); + goto done; + } + if (flux_get_rank (h, &rank) < 0) { + flux_log (ctx->h, LOG_ERR, "flux_get_rank failed"); + goto done; + } + + /* Put the rank where this module is loaded into conduit key + */ + flux_kvs_txn_t *txn = flux_kvs_txn_create (); + flux_kvs_txn_pack (txn, 0, "conduit", "i", rank); + flux_kvs_commit (h, 0, txn); + flux_kvs_txn_destroy (txn); + if (flux_msg_handler_addvec (h, htab, (void *)h, + &ctx->handlers) < 0) { + flux_log (ctx->h, LOG_ERR, "flux_msg_handler_addvec: %s", strerror (errno)); + goto done; + } + if (flux_reactor_run (flux_get_reactor (h), 0) < 0) { + flux_log (h, LOG_ERR, "flux_reactor_run: %s", strerror (errno)); + goto done; + } + +done: + return 0; +} + +MOD_NAME ("conduit"); + +/* + * vi:tabstop=4 shiftwidth=4 expandtab + */ diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/datastore.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/datastore.py new file mode 100755 index 0000000..d5fcc48 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/data-conduit/datastore.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +import socket +import struct +import json +import sys +import os + +sockdir = os.path.join("/tmp", os.environ["USER"]) +sockname = os.path.join(sockdir, "mysock") + +store = {} +sock = "" + + +def initialize(): + global sock + if not os.path.exists(sockdir): + os.mkdir(sockdir) + if os.path.exists(sockname): + os.remove(sockname) + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(sockname) + sock.listen(1) + cmd = "flux module load ./conduit.so" + os.system(cmd) + + +def run(): + global sock + global store + connection, client_address = sock.accept() + for x in range(5): + print("Waiting for a packet") + mybytes = bytearray(4) + nbytes, address = connection.recvfrom_into(mybytes, 4) + if nbytes == 0: + break + size = ( + mybytes[0] * 1 + + mybytes[1] * 256 + + mybytes[2] * 65536 + + mybytes[3] * 16777216 + ) + data = bytearray(size) + nbytes, address = connection.recvfrom_into(data, size) + dict_blob = json.loads(data.decode("ascii")) + + if dict_blob is not None: + store.update(dict_blob) + print(store) + else: + print("Mallformed data, discarding") + + connection.close() + cmd = "flux module remove conduit" + os.system(cmd) + print("Bye bye!") + + +def main(): + print("Starting....") + initialize() + run() + + +if __name__ == "__main__": + main() diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/README.md new file mode 100644 index 0000000..7f9c7ca --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/README.md @@ -0,0 +1,35 @@ +## Hierarchical Launching + +### Description: Launch an ensemble of sleep 0 tasks + +#### Setup + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/hierarchical-launching +``` + +#### Execution + +1. `salloc -N3 -ppdebug` + +2. `srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. `./parent.sh` + +``` +Mon Nov 18 15:31:08 PST 2019 +13363018989568 +13365166473216 +13367095853056 +First Level Done +Mon Nov 18 15:34:13 PST 2019 +``` + + +### Notes + +- You can increase the number of jobs by increasing `NCORES` in `parent.sh` and +`NJOBS` in `ensemble.sh`. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/ensemble.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/ensemble.sh new file mode 100755 index 0000000..0edca81 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/ensemble.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +NJOBS=750 +MAXTIME=$(expr ${NJOBS} + 2) + +for i in `seq 1 ${NJOBS}`; do + flux mini submit --nodes=1 --ntasks=1 --cores-per-task=1 sleep 0 +done + +flux jobs +flux queue drain +echo "Final Level Done" diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/parent.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/parent.sh new file mode 100755 index 0000000..84ef464 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/hierarchical-launching/parent.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +NCORES=3 + +date + +for i in `seq 1 ${NCORES}`; do + flux mini submit -N 1 -n 1 flux start ./ensemble.sh +done + +flux queue drain +echo "First Level Done" + +date diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/index.rst b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/index.rst new file mode 100644 index 0000000..aa335e9 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/index.rst @@ -0,0 +1,95 @@ +Flux Workflow Examples +---------------------- + +The examples contained here demonstrate and explain some simple use-cases with Flux, +and make use of Flux's command-line interface (CLI), Flux's C library, and the Python and Lua bindings to the C library. +The entire set of examples can be downloaded by cloning the `Github repo `_. + +The examples assume that you have installed: + +#. A recent version of Flux + +#. Python 3.6+ + +#. Lua 5.1+ + +:doc:`CLI: Job Submission ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Launch a flux instance and schedule/launch compute and io-forwarding +jobs on separate nodes using the CLI + +:doc:`Python: Job Submission ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Schedule/launch compute and io-forwarding jobs on separate nodes using +the Python bindings + +:doc:`Python: Job Submit/Wait ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Submit jobs and wait for them to complete using the Flux Python bindings + +:doc:`Python: Asynchronous Bulk Job Submission ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Asynchronously submit jobspec files from a directory and wait for them +to complete in any order + +:doc:`Python: Tracking Job Status and Events ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Submit job bundles and wait until all jobs complete + +:doc:`Python: Job Cancellation ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Cancel a running job + +:doc:`Lua: Use Events ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use events to synchronize compute and io-forwarding jobs running on +separate nodes + +:doc:`Python: Simple KVS Example ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use KVS Python interfaces to store user data into KVS + +:doc:`CLI/Lua: Job Ensemble Submitted with a New Flux Instance ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Submit job bundles, print live job events, and exit when all jobs are +complete + +:doc:`CLI: Hierarchical Launching ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Launch a large number of sleep 0 jobs + +:doc:`C/Lua: Use a Flux Comms Module ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use a Flux Comms Module to communicate with job elements + +:doc:`C/Python: A Data Conduit Strategy ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Attach to a job that receives OS time data from compute jobs + +.. toctree:: + :hidden: + + job-submit-cli/README + job-submit-api/README + job-submit-wait/README + async-bulk-job-submit/README + job-status-control/README + job-cancel/README + synchronize-events/README + kvs-python-bindings/README + job-ensemble/README + hierarchical-launching/README + comms-module/README + data-conduit/README diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-cancel/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-cancel/README.md new file mode 100644 index 0000000..af1d3b8 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-cancel/README.md @@ -0,0 +1,43 @@ +## Job Cancellation + +### Description: Cancel a running job + +#### Setup + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/job-cancel +``` + +#### Execution + +1. Launch the submitter script: + +`./submitter.py $(flux resource list -no {ncores} --state=up)` + +_note: for older versions of Flux, you might need to instead run: `./submitter.py $(flux hwloc info | awk '{print $3}')`_ + +``` +Submitted 1st job: 2241905819648 +Submitted 2nd job: 2258951471104 + +First submitted job status (2241905819648) - RUNNING +Second submitted job status (2258951471104) - PENDING + +Canceled first job: 2241905819648 + +First submitted job status (2241905819648) - CANCELED +Second submitted job status (2258951471104) - RUNNING +``` + +### Notes + +- `f = flux.Flux()` creates a new Flux handle which can be used to connect to and interact with a Flux instance. + +- `flux.job.submit(f, sleep_jobspec, waitable=True)` submits a jobspec, returning a job ID that can be used to interact with the submitted job. + +- `flux.job.cancel(f, jobid)` cancels the job. + +- `flux.job.wait_async(f, jobid)` will wait for the job to complete (or in this case, be canceled). It returns a Flux future, which can be used to process the result later. Only jobs submitted with `waitable=True` can be waited for. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-cancel/submitter.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-cancel/submitter.py new file mode 100644 index 0000000..b95584f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-cancel/submitter.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +import time +import argparse + +import flux +from flux.job import JobspecV1 + +f = flux.Flux() + +parser = argparse.ArgumentParser( + description=""" + Description: Submit two 'sleep 60' jobs that take up + all resources on a node. + """ +) +parser.add_argument(dest="cores", help="number of cores on the node") +args = parser.parse_args() + +# submit a sleep job that takes up all resources +sleep_jobspec = JobspecV1.from_command( + ["sleep", "60"], num_tasks=1, cores_per_task=int(args.cores) +) +first_jobid = flux.job.submit(f, sleep_jobspec, waitable=True) +print("Submitted 1st job: %d" % (int(first_jobid))) +time.sleep(1) + +# submit a second sleep job - will be scheduled, but not run +sleep_jobspec = JobspecV1.from_command( + ["sleep", "60"], num_tasks=1, cores_per_task=int(args.cores) +) +second_jobid = flux.job.submit(f, sleep_jobspec, waitable=True) +print("Submitted 2nd job: %d\n" % (int(second_jobid))) +time.sleep(1) + +# get list of JobInfo objects - fetch their ID's and current status +jobs = flux.job.JobList(f, max_entries=2).jobs() +print("First submitted job status (%d) - %s" % (int(jobs[1].id.dec), jobs[1].status)) +print("Second submitted job status (%d) - %s\n" % (int(jobs[0].id.dec), jobs[0].status)) + +# cancel the first job +flux.job.cancel(f, first_jobid) +future = flux.job.wait_async(f, first_jobid).wait_for(5.0) +return_id, success, errmsg = future.get_status() +print("Canceled first job: %d\n" % (int(return_id))) +time.sleep(1) + +# the second job should now run since the first was canceled +jobs = flux.job.JobList(f, max_entries=2).jobs() +print("First submitted job status (%d) - %s" % (int(jobs[1].id.dec), jobs[1].status)) +print("Second submitted job status (%d) - %s" % (int(jobs[0].id.dec), jobs[0].status)) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/README.md new file mode 100644 index 0000000..3f303ea --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/README.md @@ -0,0 +1,104 @@ +### Job Ensemble Submitted with a New Flux Instance + +#### Description: Launch a flux instance and submit one instance of an io-forwarding job and 50 compute jobs, each spanning the entire set of nodes. + +#### Setup + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/job-ensemble +``` + +#### Execution + +1. `salloc -N3 -ppdebug` + +2. `cat ensemble.sh` + +``` +#!/usr/bin/env sh + +NJOBS=10 +MAXTIME=$(expr ${NJOBS} + 2) +JOBIDS="" + +JOBIDS=$(flux mini submit --nodes=1 --ntasks=1 --cores-per-task=2 ./io-forwarding.lua ${MAXTIME}) +for i in `seq 1 ${NJOBS}`; do + JOBIDS="${JOBIDS} $(flux mini submit --nodes=2 --ntasks=4 --cores-per-task=2 ./compute.lua 1)" +done + +flux jobs +flux queue drain + +# print mock-up prevenance data +for i in ${JOBIDS}; do + echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + echo "Jobid: ${i}" + KVSJOBID=$(flux job id --from=dec --to=kvs ${i}) + flux kvs get ${KVSJOBID}.R | jq +done +``` + +3. `srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out ./ensemble.sh` + +``` +JOBID USER NAME STATE NTASKS NNODES RUNTIME RANKS +1721426247680 fluxuser compute.lu RUN 4 2 0.122s [1-2] +1718322462720 fluxuser compute.lu RUN 4 2 0.293s [0,2] +1715201900544 fluxuser compute.lu RUN 4 2 0.481s [0-1] +1712299442176 fluxuser compute.lu RUN 4 2 0.626s [1-2] +1709296320512 fluxuser compute.lu RUN 4 2 0.885s [0,2] +1706293198848 fluxuser compute.lu RUN 4 2 1.064s [0-1] +1691378253824 fluxuser io-forward RUN 1 1 1.951s 0 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Jobid: 1691378253824 +{ + "version": 1, + "execution": { + "R_lite": [ + { + "rank": "0", + "children": { + "core": "0-1" + } + } + ] + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Jobid: 1694414929920 +{ + "version": 1, + "execution": { + "R_lite": [ + { + "rank": "1-2", + "children": { + "core": "0-3" + } + } + ] + } +} +. +. +. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Jobid: 1721426247680 +{ + "version": 1, + "execution": { + "R_lite": [ + { + "rank": "1-2", + "children": { + "core": "8-11" + } + } + ] + } +} + +``` diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/compute.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/compute.lua new file mode 100755 index 0000000..e5159fd --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/compute.lua @@ -0,0 +1,18 @@ +#!/usr/bin/env lua + +local amount = tonumber (arg[1]) or 120 + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: compute.lua seconds") + print (" Compute for seconds") + os.exit (1) +end + +print ("Will compute for " .. amount .. " seconds") +sleep (amount) + +-- vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/ensemble.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/ensemble.sh new file mode 100755 index 0000000..8c76593 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/ensemble.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +NJOBS=10 +MAXTIME=$(expr ${NJOBS} + 2) +JOBIDS="" + +JOBIDS=$(flux mini submit --nodes=1 --ntasks=1 --cores-per-task=2 ./io-forwarding.lua ${MAXTIME}) +for i in `seq 1 ${NJOBS}`; do + JOBIDS="${JOBIDS} $(flux mini submit --nodes=2 --ntasks=4 --cores-per-task=2 ./compute.lua 1)" +done + +flux jobs +flux queue drain + +# print mock-up prevenance data +for i in ${JOBIDS}; do + echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + echo "Jobid: ${i}" + KVSJOBID=$(flux job id --from=dec --to=kvs ${i}) + flux kvs get ${KVSJOBID}.R | jq +done diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/io-forwarding.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/io-forwarding.lua new file mode 100755 index 0000000..3427b1e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/io-forwarding.lua @@ -0,0 +1,18 @@ +#!/usr/bin/env lua + +local amount = tonumber (arg[1]) or 120 + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: io-forward.lua seconds") + print (" Forward I/O requests for seconds") + os.exit (1) +end + +print ("Will forward IO requests for " .. amount .. " seconds") +sleep (amount) + +-- vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/kvs-watch-until.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/kvs-watch-until.lua new file mode 100755 index 0000000..16e63ae --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-ensemble/kvs-watch-until.lua @@ -0,0 +1,82 @@ +#!/usr/bin/env lua +-- +-- Exit only if/when all ranks have exited 'unknown' state +-- +local usage = [[ +Usage: kvs-wait-until [OPTIONS] KEY CODE +Watch kvs KEY until Lua code CODE returns true. +(CODE is supplied key value in variable 'v') +If -t, --timeout is provided, and the timeout expires, then +exit with non-zero exit status. + -h, --help Display this message + -v, --verbose Print value on each watch callback + -t, --timeout=T Wait at most T seconds (before exiting +]] + +local getopt = require 'flux.alt_getopt' .get_opts +local timer = require 'flux.timer'.new() +local f = require 'flux' .new() + +local function printf (...) + io.stdout:write (string.format (...)) +end +local function log_err (...) + io.stdout:write (string.format (...)) +end + +local opts, optind = getopt (arg, "hvt:", + { verbose = 'v', + timeout = 't', + help = 'h' + } + ) +if opts.h then print (usage); os.exit (0) end + +local key = arg [optind] +local callback = arg [optind+1] + +if not key or not callback then + log_err ("KVS key and callback code required\n") + print (usage) + os.exit (1) +end + +callback = "return function (v) return "..callback.." end" +local fn, err = loadstring (callback, "callback") +if not fn then + log_err ("code compile error: %s", err) + os.exit (1) +end +local cb = fn () + +local kw, err = f:kvswatcher { + key = key, + handler = function (kw, result) + if opts.v then + printf ("%4.03fs: %s = %s\n", + timer:get0(), + key, tostring (result)) + end + -- Do not pass nil result to callback: + if result == nil then return end + local ok, rv = pcall (cb, result) + if not ok then error (rv) end + if ok and rv then + os.exit (0) + end + end +} + +if opts.t then + local tw, err = f:timer { + timeout = opts.t * 1000, + handler = function (f, to) + log_err ("%4.03fs: Timeout expired!\n", timer:get0()) + os.exit (1) + end + } +end + +timer:set () +f:reactor () +-- vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/README.md new file mode 100644 index 0000000..bbd3704 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/README.md @@ -0,0 +1,69 @@ +## Using Flux Job Status and Control API + +### Description: Submit job bundles, get event updates, and wait until all jobs complete + +#### Setup + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/job-status-control +``` + +#### Execution + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -p pdebug` + +2. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Run the bookkeeper executable along with the number of jobs to be submitted (if no size is specified, 6 jobs are submitted: 3 instances of **compute.py**, and 3 instances of **io-forwarding,py**): + +`./bookkeeper.py 2` + +``` +bookkeeper: all jobs submitted +bookkeeper: waiting until all jobs complete +job 39040581632 triggered event 'submit' +job 39040581633 triggered event 'submit' +job 39040581632 triggered event 'depend' +job 39040581632 triggered event 'priority' +job 39040581632 triggered event 'alloc' +job 39040581633 triggered event 'depend' +job 39040581633 triggered event 'priority' +job 39040581633 triggered event 'alloc' +job 39040581632 triggered event 'start' +job 39040581633 triggered event 'start' +job 39040581632 triggered event 'finish' +job 39040581633 triggered event 'finish' +job 39040581633 triggered event 'release' +job 39040581633 triggered event 'free' +job 39040581633 triggered event 'clean' +job 39040581632 triggered event 'release' +job 39040581632 triggered event 'free' +job 39040581632 triggered event 'clean' +bookkeeper: all jobs completed +``` + +--- + +### Notes + +- The following constructs a job request using the **JobspecV1** class with customizable parameters for how you want to utilize the resources allocated for your job: +```python +compute_jobreq = JobspecV1.from_command( + command=["./compute.py", "10"], num_tasks=4, num_nodes=2, cores_per_task=2 +) +compute_jobreq.cwd = os.getcwd() +compute_jobreq.environment = dict(os.environ) +``` + +- `with FluxExecutor() as executor:` creates a new `FluxExecutor` which can be used to submit jobs, wait for them to complete, and get event updates. Using the executor as a context manager (`with ... as ...:`) ensures it is shut down properly. + +- `executor.submit(compute_jobreq)` returns a `concurrent.futures.Future` subclass which completes when the underlying job is done. The jobid of the underlying job can be fetched with the `.jobid([timeout])` method (which waits until the jobid is ready). + +- Throughout the course of a job, various events will occur to it. `future.add_event_callback(event, event_callback)` adds a callback which will be invoked when the given event occurs. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/bookkeeper.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/bookkeeper.py new file mode 100755 index 0000000..a7cef19 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/bookkeeper.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +import os +import argparse + +from flux.job import JobspecV1, FluxExecutor + + +def event_callback(future, event): + print(f"job {future.jobid()} triggered event {event.name!r}") + + +# main +def main(): + # set up command-line parser + parser = argparse.ArgumentParser( + description="submit and wait for the completion of " + "N bundles, each consisting of compute " + "and io-forwarding jobs" + ) + parser.add_argument( + "njobs", metavar="N", type=int, help="the number of bundles to submit and wait", + ) + args = parser.parse_args() + # set up jobspecs + compute_jobreq = JobspecV1.from_command( + command=["./compute.py", "10"], num_tasks=6, num_nodes=3, cores_per_task=2 + ) + compute_jobreq.cwd = os.getcwd() + compute_jobreq.environment = dict(os.environ) + io_jobreq = JobspecV1.from_command( + command=["./io-forwarding.py", "10"], num_tasks=3, num_nodes=3, cores_per_task=1 + ) + io_jobreq.cwd = os.getcwd() + io_jobreq.environment = dict(os.environ) + # submit jobs and register event callbacks for all events + with FluxExecutor() as executor: + futures = [executor.submit(compute_jobreq) for _ in range(args.njobs // 2)] + futures.extend( + executor.submit(io_jobreq) for _ in range(args.njobs // 2, args.njobs) + ) + print("bookkeeper: all jobs submitted") + for fut in futures: + # each event can have a different callback + for event in executor.EVENTS: + fut.add_event_callback(event, event_callback) + print("bookkeeper: waiting until all jobs complete") + # exiting the context manager waits for the executor to complete all futures + print("bookkeeper: all jobs completed") + + +main() + +# vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/compute.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/compute.py new file mode 100755 index 0000000..1f860f2 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/compute.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="compute for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will compute for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/io-forwarding.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/io-forwarding.py new file mode 100755 index 0000000..217ed0e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-status-control/io-forwarding.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="forward I/O requests for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will forward I/O requests for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/README.md new file mode 100644 index 0000000..12cf931 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/README.md @@ -0,0 +1,106 @@ +## Job Submit API + +To run the following examples, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/job-submit-api +``` + +### Part(a) - Using a direct job.submit RPC + +#### Description: Schedule and launch compute and io-forwarding jobs on separate nodes + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -p pdebug` + +2. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Run the submitter executable: + +`./submitter.py` + +4. List currently running jobs: + +`flux jobs` + +``` +JOBID USER NAME ST NTASKS NNODES RUNTIME RANKS +Ζ’5W8gVwm moussa1 io-forward R 1 1 19.15s 2 +Ζ’5Vd2kJs moussa1 compute.py R 4 2 19.18s [0-1] +``` + +5. Information about jobs, such as the submitted job specification, an eventlog, and the resource description format **R** are stored in the KVS. The data can be queried via the `job-info` module via the `flux job info` command. For example, to fetch **R** for a job which has been allocated resources: + +`flux job info Ζ’5W8gVwm R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"2","children":{"core":"0"}}]}} +``` + +`flux job info Ζ’5Vd2kJs R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"0-1","children":{"core":"0-3"}}]}} +``` + +### Part(b) - Using a direct job.submit RPC + +#### Description: Schedule and launch both compute and io-forwarding jobs across all nodes + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -p pdebug` + +2. Launch another Flux instance on the current allocation: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Run the second submitter executable: + +`./submitter2.py` + +4. List currently running jobs: + +`flux jobs` + +``` +JOBID USER NAME ST NTASKS NNODES RUNTIME RANKS +Ζ’ctYadhh moussa1 io-forward R 3 3 3.058s [0-2] +Ζ’ct1StnT moussa1 compute.py R 6 3 3.086s [0-2] +``` + +5. Fetch **R** for the jobs that have been allocated resources: + +`flux job info Ζ’ctYadhh R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"0-2","children":{"core":"0-3"}}]}} +``` + +`flux job info Ζ’ct1StnT R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"0-2","children":{"core":"0-3"}}]}} +``` + +--- + +### Notes + +- `f = flux.Flux()` creates a new Flux handle which can be used to connect to and interact with a Flux instance. + + +- The following constructs a job request using the **JobspecV1** class with customizable parameters for how you want to utilize the resources allocated for your job: +```python +compute_jobreq = JobspecV1.from_command( + command=["./compute.py", "120"], num_tasks=4, num_nodes=2, cores_per_task=2 +) +compute_jobreq.cwd = os.getcwd() +compute_jobreq.environment = dict(os.environ) +``` + +- `flux.job.submit(f, compute_jobreq)` submits the job to be run, and returns a job ID once it begins running. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/compute.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/compute.py new file mode 100755 index 0000000..1f860f2 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/compute.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="compute for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will compute for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/io-forwarding.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/io-forwarding.py new file mode 100755 index 0000000..217ed0e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/io-forwarding.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="forward I/O requests for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will forward I/O requests for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/submitter.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/submitter.py new file mode 100755 index 0000000..51f2408 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/submitter.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import json +import os +import re +import flux +from flux.job import JobspecV1 + +f = flux.Flux() + +compute_jobreq = JobspecV1.from_command( + command=["./compute.py", "120"], num_tasks=4, num_nodes=2, cores_per_task=2 +) +compute_jobreq.cwd = os.getcwd() +compute_jobreq.environment = dict(os.environ) +print(flux.job.submit(f, compute_jobreq)) + +io_jobreq = JobspecV1.from_command( + command=["./io-forwarding.py", "120"], num_tasks=1, num_nodes=1, cores_per_task=1 +) +io_jobreq.cwd = os.getcwd() +io_jobreq.environment = dict(os.environ) +print(flux.job.submit(f, io_jobreq)) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/submitter2.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/submitter2.py new file mode 100755 index 0000000..670acff --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-api/submitter2.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import json +import os +import re +import flux +from flux.job import JobspecV1 + +f = flux.Flux() + +compute_jobreq = JobspecV1.from_command( + command=["./compute.py", "120"], num_tasks=6, num_nodes=3, cores_per_task=2 +) +compute_jobreq.cwd = os.getcwd() +compute_jobreq.environment = dict(os.environ) +print(flux.job.submit(f, compute_jobreq)) + +io_jobreq = JobspecV1.from_command( + command=["./io-forwarding.py", "120"], num_tasks=3, num_nodes=3, cores_per_task=1 +) +io_jobreq.cwd = os.getcwd() +io_jobreq.environment = dict(os.environ) +print(flux.job.submit(f, io_jobreq)) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/README.md new file mode 100644 index 0000000..b50e71a --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/README.md @@ -0,0 +1,81 @@ +## Job Submit CLI + +To run the following examples, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/job-submit-cli +``` + +### Part(a) - Partitioning Schedule + +#### Description: Launch a flux instance and schedule/launch compute and io-forwarding jobs on separate nodes + +1. `salloc -N3 -ppdebug` + +2. `srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. `flux mini submit --nodes=2 --ntasks=4 --cores-per-task=2 ./compute.lua 120` + +4. `flux mini submit --nodes=1 --ntasks=1 --cores-per-task=2 ./io-forwarding.lua 120` + +5. List running jobs: + +`flux jobs` + +``` +JOBID USER NAME ST NTASKS NNODES RUNTIME RANKS +Ζ’3ETxsR9H moussa1 io-forward R 1 1 2.858s 2 +Ζ’38rBqEWT moussa1 compute.lu R 4 2 15.6s [0-1] +``` + +6. Get information about job: + +`flux job info Ζ’3ETxsR9H R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"2","children":{"core":"0-1"}}]}} +``` + +`flux job info Ζ’38rBqEWT R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"0-1","children":{"core":"0-3"}}]}} +``` + +### Part(b) - Overlapping Schedule + +#### Description: Launch a flux instance and schedule/launch both compute and io-forwarding jobs across all nodes + +1. `salloc -N3 -ppdebug` + +2. `srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. `flux mini submit --nodes=3 --ntasks=6 --cores-per-task=2 ./compute.lua 120` + +4. `flux mini submit --nodes=3 --ntasks=3 --cores-per-task=1 ./io-forwarding.lua 120` + +5. List jobs in KVS: + +`flux jobs` + +``` +JOBID USER NAME ST NTASKS NNODES RUNTIME RANKS +Ζ’3ghmgCpw moussa1 io-forward R 3 3 16.91s [0-2] +Ζ’3dSybfQ3 moussa1 compute.lu R 6 3 24.3s [0-2] + +``` + +6. Get information about job: + +`flux job info Ζ’3ghmgCpw R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"0-2","children":{"core":"4"}}]}} +``` + +`flux job info Ζ’3dSybfQ3 R` + +``` +{"version":1,"execution":{"R_lite":[{"rank":"0-2","children":{"core":"0-3"}}]}} +``` diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/compute.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/compute.lua new file mode 100755 index 0000000..4fbccc8 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/compute.lua @@ -0,0 +1,17 @@ +#!/usr/bin/env lua + +local amount = tonumber (arg[1]) or 120 + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: compute.lua seconds") + print (" Compute for seconds") + os.exit (1) +end + +print ("Will compute for " .. amount .. " seconds") +sleep (amount) + diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/compute.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/compute.py new file mode 100755 index 0000000..1f860f2 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/compute.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="compute for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will compute for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/io-forwarding.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/io-forwarding.lua new file mode 100755 index 0000000..46ccda0 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/io-forwarding.lua @@ -0,0 +1,17 @@ +#!/usr/bin/env lua + +local amount = tonumber (arg[1]) or 120 + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: io-forward.lua seconds") + print (" Forward I/O requests for seconds") + os.exit (1) +end + +print ("Will forward IO requests for " .. amount .. " seconds") +sleep (amount) + diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/io-forwarding.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/io-forwarding.py new file mode 100755 index 0000000..217ed0e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-cli/io-forwarding.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="forward I/O requests for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will forward I/O requests for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/README.md new file mode 100644 index 0000000..1f8745e --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/README.md @@ -0,0 +1,150 @@ +## Python Job Submit/Wait + +To run the following examples, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/job-submit-wait +``` + +### Part(a) - Python Job Submit/Wait + +#### Description: Submit jobs asynchronously and wait for them to complete in any order + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -ppdebug` + +2. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Submit the **submitter_wait_any.py** script, along with the number of jobs you want to run (if no argument is passed, 10 jobs are submitted): + +`./submitter_wait_any.py 10` + +``` +submit: 46912591450240 compute_jobspec +submit: 46912591450912 compute_jobspec +submit: 46912591451080 compute_jobspec +submit: 46912591363152 compute_jobspec +submit: 46912591362984 compute_jobspec +submit: 46912591451360 bad_jobspec +submit: 46912591451528 bad_jobspec +submit: 46912591451696 bad_jobspec +submit: 46912591451864 bad_jobspec +submit: 46912591452032 bad_jobspec +wait: 46912591451528 Error: job returned exit code 1 +wait: 46912591451864 Error: job returned exit code 1 +wait: 46912591451360 Error: job returned exit code 1 +wait: 46912591451696 Error: job returned exit code 1 +wait: 46912591452032 Error: job returned exit code 1 +wait: 46912591450240 Success +wait: 46912591363152 Success +wait: 46912591450912 Success +wait: 46912591451080 Success +wait: 46912591362984 Success +``` + +--- + +### Part(b) - Python Job Submit/Wait (Sliding Window) + +#### Description: Asynchronously submit jobs and keep at most a number of those jobs active + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -ppdebug` + +2. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Submit the **submitter_sliding_window.py** script, along with the number of jobs you want to run and the size of the window (if no argument is passed, 10 jobs are submitted and the window size is 2 jobs): + +`./submitter_sliding_window.py 10 3` + +``` +submit: 5624175788032 +submit: 5624611995648 +submit: 5625014648832 +wait: 5624175788032 Success +submit: 5804329533440 +wait: 5624611995648 Success +submit: 5804648300544 +wait: 5625014648832 Success +submit: 5805084508160 +wait: 5804329533440 Success +submit: 5986144223232 +wait: 5804648300544 Success +submit: 5986462990336 +wait: 5805084508160 Success +submit: 5986882420736 +wait: 5986144223232 Success +submit: 6164435697664 +wait: 5986462990336 Success +wait: 5986882420736 Success +wait: 6164435697664 Success +``` + +--- + +### Part(c) - Python Job Submit/Wait (Specific Job ID) + +#### Description: Asynchronously submit jobs, block/wait for specific jobs to complete + +1. Allocate three nodes from a resource manager: + +`salloc -N3 -ppdebug` + +2. Launch a Flux instance on the current allocation by running `flux start` once per node, redirecting log messages to the file `out` in the current directory: + +`srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. Submit the **submitter_wait_in_order.py** script, along with the number of jobs you want to run (if no argument is passed, 10 jobs are submitted): + +`./submitter_wait_in_order.py 10` + +``` +submit: 46912593818008 compute_jobspec +submit: 46912593818176 compute_jobspec +submit: 46912593818344 compute_jobspec +submit: 46912593818512 compute_jobspec +submit: 46912593738048 compute_jobspec +submit: 46912519873816 bad_jobspec +submit: 46912593818792 bad_jobspec +submit: 46912593818960 bad_jobspec +submit: 46912593819128 bad_jobspec +submit: 46912593819296 bad_jobspec +wait: 46912593818008 Success +wait: 46912593818176 Success +wait: 46912593818344 Success +wait: 46912593818512 Success +wait: 46912593738048 Success +wait: 46912519873816 Error: job returned exit code 1 +wait: 46912593818792 Error: job returned exit code 1 +wait: 46912593818960 Error: job returned exit code 1 +wait: 46912593819128 Error: job returned exit code 1 +wait: 46912593819296 Error: job returned exit code 1 +``` + +--- + +### Notes + +- The following constructs a job request using the **JobspecV1** class with customizable parameters for how you want to utilize the resources allocated for your job: + +```python +# create jobspec for compute.py +compute_jobspec = JobspecV1.from_command(command=["./compute.py", "15"], num_tasks=4, num_nodes=2, cores_per_task=2) +compute_jobspec.cwd = os.getcwd() +compute_jobspec.environment = dict(os.environ) +``` + +- Using the executor as a context manager (`with FluxExecutor() as executor`) ensures it shuts down properly. + +- `executor.submit(jobspec)` returns a future which completes when the job is done. + +- `future.exception()` blocks until the future is complete and returns (not raises) an exception if the job was canceled or was otherwise prevented from execution. Otherwise the method returns ``None``. + +- `future.result()` blocks until the future is complete and returns the return code of the job. If the job succeeded, the return code will be 0. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/compute.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/compute.py new file mode 100755 index 0000000..1f860f2 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/compute.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import argparse +import time + +parser = argparse.ArgumentParser(description="compute for seconds") +parser.add_argument( + "integer", + metavar="S", + type=int, + help="an integer for the number of seconds to compute", +) + +args = parser.parse_args() + +print("Will compute for " + str(args.integer) + " seconds.") +time.sleep(args.integer) diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_sliding_window.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_sliding_window.py new file mode 100755 index 0000000..cfec311 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_sliding_window.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +import os +import argparse +import collections +import concurrent.futures as cf + +from flux.job import JobspecV1, FluxExecutor + + +def main(): + # parse command line + parser = argparse.ArgumentParser() + parser.add_argument("njobs", nargs="?", type=int, default=10) + parser.add_argument("window_size", nargs="?", type=int, default=2) + args = parser.parse_args() + print(args) + # create jobspec for compute.py + compute_jobspec = JobspecV1.from_command( + command=["./compute.py", "5"], num_tasks=4, num_nodes=2, cores_per_task=2 + ) + compute_jobspec.cwd = os.getcwd() + compute_jobspec.environment = dict(os.environ) + # create a queue of the jobspecs to submit + jobspec_queue = collections.deque(compute_jobspec for _ in range(args.njobs)) + futures = [] # holds incomplete futures + with FluxExecutor() as executor: + while jobspec_queue or futures: + if len(futures) < args.window_size and jobspec_queue: + fut = executor.submit(jobspec_queue.popleft()) + print(f"submit: {id(fut)}") + futures.append(fut) + else: + done, not_done = cf.wait(futures, return_when=cf.FIRST_COMPLETED) + futures = list(not_done) + for fut in done: + if fut.exception() is not None: + print( + f"wait: {id(fut)} Error: job raised error " + f"{fut.exception()}" + ) + elif fut.result() == 0: + print(f"wait: {id(fut)} Success") + else: + print( + f"wait: {id(fut)} Error: job returned " + f"exit code {fut.result()}" + ) + + +if __name__ == "__main__": + main() + +# vim: tabstop=4 shiftwidth=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_wait_any.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_wait_any.py new file mode 100755 index 0000000..890a1f0 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_wait_any.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import os +import argparse +import concurrent.futures + +from flux.job import JobspecV1, FluxExecutor + + +def main(): + # parse command line + parser = argparse.ArgumentParser() + parser.add_argument("njobs", nargs="?", type=int, default=10) + args = parser.parse_args() + # create jobspec for compute.py + compute_jobspec = JobspecV1.from_command( + command=["./compute.py", "10"], num_tasks=4, num_nodes=2, cores_per_task=2 + ) + compute_jobspec.cwd = os.getcwd() + compute_jobspec.environment = dict(os.environ) + # create bad jobspec that will fail + bad_jobspec = JobspecV1.from_command(["/bin/false"]) + # create an executor to submit jobs + with FluxExecutor() as executor: + futures = [] + # submit half successful jobs and half failures + for _ in range(args.njobs // 2): + futures.append(executor.submit(compute_jobspec)) + print(f"submit: {id(futures[-1])} compute_jobspec") + for _ in range(args.njobs // 2, args.njobs): + futures.append(executor.submit(bad_jobspec)) + print(f"submit: {id(futures[-1])} bad_jobspec") + for fut in concurrent.futures.as_completed(futures): + if fut.exception() is not None: + print(f"wait: {id(fut)} Error: job raised error {fut.exception()}") + elif fut.result() == 0: + print(f"wait: {id(fut)} Success") + else: + print(f"wait: {id(fut)} Error: job returned exit code {fut.result()}") + + +if __name__ == "__main__": + main() + +# vim: tabstop=4 shiftwidth=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_wait_in_order.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_wait_in_order.py new file mode 100755 index 0000000..cad6491 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-submit-wait/submitter_wait_in_order.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import argparse +import os + +from flux.job import JobspecV1, FluxExecutor + + +def main(): + # parse command line + parser = argparse.ArgumentParser() + parser.add_argument("njobs", nargs="?", type=int, default=10) + args = parser.parse_args() + # create jobspec for compute.py + compute_jobspec = JobspecV1.from_command( + command=["./compute.py", "10"], num_tasks=4, num_nodes=2, cores_per_task=2 + ) + compute_jobspec.cwd = os.getcwd() + compute_jobspec.environment = dict(os.environ) + bad_jobspec = JobspecV1.from_command(["/bin/false"]) + # create an executor to submit jobs + with FluxExecutor() as executor: + futures = [] + # submit half successful jobs and half failures + for _ in range(args.njobs // 2): + futures.append(executor.submit(compute_jobspec)) + print(f"submit: {id(futures[-1])} compute_jobspec") + for _ in range(args.njobs // 2, args.njobs): + futures.append(executor.submit(bad_jobspec)) + print(f"submit: {id(futures[-1])} bad_jobspec") + # wait for each future in turn + for fut in futures: + if fut.exception() is not None: + print(f"wait: {id(fut)} Error: job raised error {fut.exception()}") + elif fut.result() == 0: + print(f"wait: {id(fut)} Success") + else: + print(f"wait: {id(fut)} Error: job returned exit code {fut.result()}") + + +if __name__ == "__main__": + main() + +# vim: tabstop=4 shiftwidth=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-watch/job-watch.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-watch/job-watch.sh new file mode 100755 index 0000000..7784c52 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/job-watch/job-watch.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "25 blueberry pancakes on the table... 25 blueberry pancakes! πŸ₯žοΈ" +sleep 3 +echo "Eat a stack, for a snack, 15 blueberry pancakes on the table! πŸ₯„️" +sleep 3 +echo "15 blueberry pancakes on the table... 15 blueberry pancakes! πŸ₯žοΈ" +sleep 2 +echo "Throw a stack... it makes a smack! 15 blueberry pancakes on the wall! πŸ₯žοΈ" +sleep 2 +echo "You got some cleaning to do 🧽️" diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/kvs-python-bindings/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/kvs-python-bindings/README.md new file mode 100644 index 0000000..0a67026 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/kvs-python-bindings/README.md @@ -0,0 +1,63 @@ +## KVS Python Binding Example + +### Description: Use the KVS Python interface to store user data into KVS + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/kvs-python-bindings +``` + +1. Launch a Flux instance by running `flux start`, redirecting log messages to the file `out` in the current directory: + +`flux start -s 1 -o,-S,log-filename=out` + +2. Submit the Python script: + +`flux mini submit -N 1 -n 1 ./kvsput-usrdata.py` + +``` +6705031151616 +``` + +3. Attach to the job and view output: + +`flux job attach 6705031151616` + +``` +hello world +hello world again +``` + +4. Each job is run within a KVS namespace. `FLUX_KVS_NAMESPACE` is set, which is automatically read and used by the KVS operations in the handle. To take a look at the job's KVS, convert its job ID to KVS: + +`flux job id --from=dec --to=kvs 6705031151616` + +``` +job.0000.0619.2300.0000 +``` + +5. The keys for this job will be put at the root of the namespace, which is mounted under "guest". To get the value stored under the first key "usrdata": + +`flux kvs get job.0000.0619.2300.0000.guest.usrdata` + +``` +"hello world" +``` + +6. Get the value stored under the second key "usrdata2": + +`flux kvs get job.0000.0619.2300.0000.guest.usrdata2` + +``` +"hello world again" +``` + +### Notes + +- `f = flux.Flux()` creates a new Flux handle which can be used to connect to and interact with a Flux instance. + +- `kvs.put()` places the value of _udata_ under the key **"usrdata"**. Once the key-value pair is put, the change must be committed with `kvs.commit()`. The value can then be retrieved with `kvs.get()` + +- `kvs.get()` on a directory will return a KVSDir object which supports the `with` compound statement. `with` guarantees a commit is called on the directory. diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/kvs-python-bindings/kvsput-usrdata.py b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/kvs-python-bindings/kvsput-usrdata.py new file mode 100755 index 0000000..0a5cb77 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/kvs-python-bindings/kvsput-usrdata.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import sys +import flux +import os +from flux import kvs + +f = flux.Flux() +udata = "hello world" +# using function interface +kvs.put(f, "usrdata", udata) +# commit is required to effect the above put op to the server +kvs.commit(f) +print(kvs.get(f, "usrdata")) + +# get() on a directory will return a KVSDir object which supports +# the "with" compound statement. "with" guarantees a commit is called +# on the directory. +with kvs.get(f, ".") as kd: + kd["usrdata2"] = "hello world again" + +print(kvs.get(f, "usrdata2")) + +# vi: ts=4 sw=4 expandtab diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/requirements.txt b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/requirements.txt new file mode 100644 index 0000000..1463f8f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/requirements.txt @@ -0,0 +1,3 @@ +sphinx-rtd-theme +sphinxcontrib-spelling +recommonmark diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/README.md b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/README.md new file mode 100644 index 0000000..3fa4b53 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/README.md @@ -0,0 +1,51 @@ +### Using Events with Separate Nodes + +#### Description: Using events to synchronize compute and io-forwarding jobs running on separate nodes + +If you haven't already, download the files and change your working directory: + +``` +$ git clone https://github.com/flux-framework/flux-workflow-examples.git +$ cd flux-workflow-examples/synchronize-events +``` + +1. `salloc -N3 -ppdebug` + +2. `srun --pty --mpi=none -N3 flux start -o,-S,log-filename=out` + +3. `flux mini submit --nodes=2 --ntasks=4 --cores-per-task=2 ./compute.lua 120` + +**Output -** `225284456448` + +4. `flux mini submit --nodes=1 --ntasks=1 --cores-per-task=2 ./io-forwarding.lua 120` + +**Output -** `344889229312` + +5. List running jobs: + +`flux jobs` + +``` +JOBID USER NAME ST NTASKS NNODES RUNTIME RANKS +Ζ’A4TgT7d moussa1 io-forward R 1 1 4.376s 2 +Ζ’6vEcj7M moussa1 compute.lu R 4 2 11.51s [0-1] +``` + +6. Attach to running or completed job output: + +`flux job attach Ζ’6vEcj7M` + +``` +Block until we hear go message from the an io forwarder +Block until we hear go message from the an io forwarder +Recv an event: please proceed +Recv an event: please proceed +Will compute for 120 seconds +Will compute for 120 seconds +Block until we hear go message from the an io forwarder +Block until we hear go message from the an io forwarder +Recv an event: please proceed +Recv an event: please proceed +Will compute for 120 seconds +Will compute for 120 seconds +``` diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/compute.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/compute.lua new file mode 100755 index 0000000..925be4c --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/compute.lua @@ -0,0 +1,23 @@ +#!/usr/bin/env lua + +local f, err = require 'flux' .new () + +local amount = tonumber (arg[1]) or 120 + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: compute.lua seconds") + print (" Compute for seconds") + os.exit (1) +end + +print ("Block until we hear go message from the an io forwarder") +f:subscribe ("app.iof.go") +local t, tag = f:recv_event () +print ("Recv an event: " .. t.data ) +print ("Will compute for " .. amount .. " seconds") +sleep (amount) + diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/io-forwarding.lua b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/io-forwarding.lua new file mode 100755 index 0000000..bad77f8 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/flux-workflow-examples/synchronize-events/io-forwarding.lua @@ -0,0 +1,23 @@ +#!/usr/bin/env lua + +local flux = require 'flux' +local f = flux.new () +local amount = tonumber (arg[1]) or 120 + +local function sleep (n) + os.execute ("sleep " .. n) +end + +if #arg ~= 1 then + print ("Usage: io-forward.lua seconds") + print (" Forward I/O requests for seconds") + os.exit (1) +end + +local rc, err = f:sendevent ({ data = "please proceed" }, "app.iof.go") +if not rc then error (err) end +print ("Sent a go event") + +print ("Will forward IO requests for " .. amount .. " seconds") +sleep (amount) + diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/Flux-logo.svg b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/Flux-logo.svg new file mode 100644 index 0000000..f2d126b --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/Flux-logo.svg @@ -0,0 +1 @@ +Flux-logo-3 \ No newline at end of file diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad.ipynb b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad.ipynb new file mode 100644 index 0000000..3ccdc8f --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad.ipynb @@ -0,0 +1,671 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dd3e912b-3428-4bc7-88bd-97686406b75a", + "metadata": { + "tags": [] + }, + "source": [ + "# DYAD\n", + "\n", + "DYAD is a synchronization and data movement tool for computational science workflows built on top of Flux. DYAD aims to provide the benefits of in situ and in transit tools (e.g., fine-grained synchronization between producer and consumer applications, fast data access due to spatial locality) while relying on a file-based data abstraction to maximize portability and minimize code change requirements for workflows. More specifically, DYAD aims to overcome the following challenges associated with traditional shared-storage and modern in situ and in transit data movement approaches:\n", + "\n", + "* Lack of per-file object synchronization in shared-storage approaches\n", + "* Poor temporal and spatial locality in shared-storage approaches\n", + "* Poor performance for file metadata operations in shared-storage approaches (and possibly some in situ and in transit approaches)\n", + "* Poor portability and the introduction of required code changes for in situ and in transit approaches\n", + "\n", + "In resolving these challenges, DYAD aims to provide the following to users:\n", + "\n", + "* Good performance (similar to in situ and in transit) due to on- or near-node temporary storage of data\n", + "* Transparent per-file object synchronization between producer and consumer applications\n", + "* Little to no code change to existing workflows to achieve the previous benefits\n", + "\n", + "To demonstrate DYAD's capabilities, we will use the simple demo applications found in the `dyad_demo` directory. This directory contains C and C++ implementations of a single producer application and a single consumer application. The producer application generates several files, each consisting of 10, 32-bit integers, and registers them with DYAD. The consumer application uses DYAD to wait until the desired file is produced. Then, if needed, it will use DYAD to retrieve the generated files from the Flux broker on which the producer application is running. Finally, the consumer application will read and validate the contents of each file.\n", + "\n", + "To start, specify which versions of the producer and consumer applications you would like to use by setting the `producer_program` and `consumer_program` variables. There are two versions for the producer (i.e., `c_prod` and `cpp_prod`) and two versions for the consumer (i.e., `c_cons` and `cpp_cons`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fa41e2c-80b1-498e-8ff9-7df6250c7a5d", + "metadata": {}, + "outputs": [], + "source": [ + "producer_program = \"/opt/dyad_demo/c_prod\" # Change to \"/opt/dyad_demo/cpp_prod\" for C++\n", + "consumer_program = \"/opt/dyad_demo/c_cons\" # Change to \"/opt/dyad_demo/cpp_cons\" for C++" + ] + }, + { + "cell_type": "markdown", + "id": "03cacf9d-f98a-45bb-9422-5648428c690f", + "metadata": {}, + "source": [ + "Next, specify the number of files you wish to generate and transfer by setting the `num_files_transfered` variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9b72d51-f294-4e82-ab74-e77f922dc0af", + "metadata": {}, + "outputs": [], + "source": [ + "num_files_transfered = 10" + ] + }, + { + "cell_type": "markdown", + "id": "4dad884f-449e-4b00-bbc9-955fa9f31066", + "metadata": {}, + "source": [ + "The next step is to set the directories for DYAD to track. Each DYAD-enabled application tracks two directories: a **producer-managed directory** and a **consumer-managed directory**. At least one of these directories must be specified to use DYAD.\n", + "\n", + "When a producer-managed directory is provided, DYAD will store information about any file stored in that directory (or its subdirectories) into a namespace within the Flux key-value store (KVS). This information is later used by DYAD to transfer files from producer to consumer.\n", + "\n", + "When a consumer-managed directory is provided, DYAD will block the application whenever a file inside that directory (or subdirectory) is opened. This blocking will last until DYAD sees information about the file in the Flux KVS namespace. If the information retrieved from the KVS indicates that the file is actually located elsewhere, DYAD will use Flux's remote procedure call (RPC) system to ask the Flux broker at the file's location to transfer the file. If a transfer occurs, the file's contents will be stored at the file path passed to the original file opening function (e.g., `open`, `fopen`).\n", + "\n", + "In this demo, we will use 3 different directories: one unique to the consumer (`consumer_managed_directory`), one unique to the producer (`producer_managed_directory`), and one shared between producer and consumer (`shared_managed_directory`). Set the 3 variables in the cell below to specify these directories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60b0d5e0-fcf7-4fc9-a203-cdea84cd4950", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "consumer_managed_directory = \"/tmp/cons\"\n", + "producer_managed_directory = \"/tmp/prod\"\n", + "shared_managed_directory = \"/tmp/shared\"" + ] + }, + { + "cell_type": "markdown", + "id": "5bfa6706-0af5-4da8-bbc1-3edb9bccf953", + "metadata": {}, + "source": [ + "Finally, empty these directories or create new ones if they do not already exist." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0dac6c9-43dd-4b4f-89e3-bfb180122f71", + "metadata": {}, + "outputs": [], + "source": [ + "!rm -rf {consumer_managed_directory}\n", + "!mkdir -p {consumer_managed_directory}\n", + "!chmod 755 {consumer_managed_directory}\n", + "!rm -rf {producer_managed_directory}\n", + "!mkdir -p {producer_managed_directory}\n", + "!chmod 755 {producer_managed_directory}\n", + "!rm -rf {shared_managed_directory}\n", + "!mkdir -p {shared_managed_directory}\n", + "!chmod 755 {shared_managed_directory}" + ] + }, + { + "cell_type": "markdown", + "id": "39b1aeec-d2b1-4f7e-80b1-519e4da2bff0", + "metadata": {}, + "source": [ + "## Example 1\n", + "\n", + "In this first example, we will be using DYAD to transfer data between a producer and consumer in different locations (e.g., on different nodes of a supercomputer). However, since this demo assumes we are running on a single AWS node, we will simulate the difference in locations by specifying different directories for the producer's managed directory and the consumer's managed directory. Normally, these directories would be the same and would both point to local, on-node storage.\n", + "\n", + "In this example, data will be transfered from the proudcer's managed directory to the consumer's managed directory. Additionally, each file opening call (e.g,. `open`, `fopen`) in the consumer application will be blocked until the relevant file is available in the producer's managed directory. The figure below illustrates this transfer and synchronization process." + ] + }, + { + "cell_type": "markdown", + "id": "aa5a6347-e407-47fd-9984-1a8f76b25b38", + "metadata": {}, + "source": [ + "
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "427c8a90-3d00-403d-825c-7e24d2117512", + "metadata": {}, + "source": [ + "Before running the DYAD-enabled applications, there are two things we must do:\n", + "1. Setup a namespace in the Flux KVS to be used by DYAD\n", + "2. Load DYAD's Flux module\n", + "\n", + "To begin, set the `kvs_namespace` variable to the namespace you wish to use for DYAD. This namespace can be any string value you want." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2190770-fe49-4343-b2c8-eb625eb980d2", + "metadata": {}, + "outputs": [], + "source": [ + "kvs_namespace = \"dyad_test\"" + ] + }, + { + "cell_type": "markdown", + "id": "e116b785-5bdb-441b-9171-47e0b27a6e7d", + "metadata": {}, + "source": [ + "Next, create the namespace by running `flux kvs namespace create`. The cell below also runs `flux kvs namespace list` to allow you to verify that the namespace was created successfully." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a41c3f13-7b04-4ec5-9c5d-c5033d4977ca", + "metadata": {}, + "outputs": [], + "source": [ + "!flux kvs namespace create {kvs_namespace}\n", + "!flux kvs namespace list" + ] + }, + { + "cell_type": "markdown", + "id": "0840b124-f805-432e-9764-1b167df39f64", + "metadata": {}, + "source": [ + "The next step is to load DYAD's Flux module. This module is the component of DYAD that actually sends files from producer to consumer.\n", + "\n", + "To start this step, set `dyad_module` below to the path to the DYAD module (i.e., `dyad.so`). For this demo, DYAD has already been installed under the `/usr` prefix, so the path to the DYAD module should be `/usr/lib/dyad.so`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cdb1dfc4-1f8a-434d-8be8-626382d124c6", + "metadata": {}, + "outputs": [], + "source": [ + "dyad_module = \"/usr/lib/dyad.so\"" + ] + }, + { + "cell_type": "markdown", + "id": "6c2260bc", + "metadata": {}, + "source": [ + "Next, choose the communication backend for DYAD to use. This backend is used by DYAD's data transport layer (DTL) component to move data from producer to consumer. Currently, valid values are:\n", + "* `UCX`: use Unified Communication X for data movement\n", + "* `FLUX_RPC`: use Flux Remote Procedure Call (RPC) feature for data movement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46b38519", + "metadata": {}, + "outputs": [], + "source": [ + "dtl_mode = \"UCX\"" + ] + }, + { + "cell_type": "markdown", + "id": "aab31cd8-5034-4450-bb1b-7b299fc5be86", + "metadata": {}, + "source": [ + "Finally, load the DYAD module by running `flux module load` on each broker. We load the module onto each broker because, normally, we would not know exactly which brokers the producer and consumer would be running on.\n", + "\n", + "When being loaded, the DYAD module takes a single command-line argument: the producer-managed directory. The module uses this directory to determine the path to any files it needs to transfer to consumers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edc65fdb-f746-46f6-81df-17602fd94acc", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r all flux module load {dyad_module} {producer_managed_directory} {dtl_mode}" + ] + }, + { + "cell_type": "markdown", + "id": "a71e4d07-f17e-4416-8f8d-0e36137b461a", + "metadata": {}, + "source": [ + "After loading the module, we can double check it has been loaded by running the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7061d3af-5d12-4edb-aa9e-6a678798ef14", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r all flux module list | grep dyad" + ] + }, + { + "cell_type": "markdown", + "id": "efcaef56-02e4-43fd-af28-2b6689db19e6", + "metadata": {}, + "source": [ + "Now, we will generate the shell commands that we will use to run the producer and consumer applications. These commands can be broken down into three pieces.\n", + "\n", + "First, the commands will set the `LD_PRELOAD` environment variable if running the C version of the producer or consumer. We set `LD_PRELOAD` because DYAD's C API uses the preload trick to intercept the `open`, `close`, `fopen`, and `fclose` functions.\n", + "\n", + "Second, the commands set a couple of environment variables to configure DYAD. The environment variables used in this example are:\n", + "* `DYAD_KVS_NAMESPACE`: specifies the Flux KVS namespace to use with DYAD\n", + "* `DYAD_DTL_MODE`: sets the communication backend to use for data movement\n", + "* `DYAD_PATH_PRODUCER`: sets the producer-managed path\n", + "* `DYAD_PATH_CONSUMER`: sets the consumer-managed path\n", + "\n", + "Finally, the rest of the commands are the invocation of the applications themselves.\n", + "\n", + "Run the following 2 cells to generate and see the commands for the producer and consumer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6def81f-f17a-4601-9572-b00d2959287f", + "metadata": {}, + "outputs": [], + "source": [ + "producer_launch_cmd = \"{preload} DYAD_KVS_NAMESPACE={kvs_namespace} DYAD_DTL_MODE={dtl_mode} \\\n", + "DYAD_PATH_PRODUCER={producer_managed_directory} flux exec -r 0 \\\n", + "{producer_program} {num_files_transfered} {producer_managed_directory}\".format(\n", + " preload=\"LD_PRELOAD=\\\"/usr/lib/dyad_wrapper.so\\\"\" if producer_program.split(\"/\")[-1].strip().startswith(\"c_\") else \"\",\n", + " kvs_namespace=kvs_namespace,\n", + " dtl_mode=dtl_mode,\n", + " producer_managed_directory=producer_managed_directory,\n", + " producer_program=producer_program,\n", + " num_files_transfered=num_files_transfered,\n", + ")\n", + "print(producer_launch_cmd)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42d3007b-d6b4-40b9-a4a1-153aef536c90", + "metadata": {}, + "outputs": [], + "source": [ + "consumer_launch_cmd = \"{preload} DYAD_KVS_NAMESPACE={kvs_namespace} DYAD_DTL_MODE={dtl_mode} \\\n", + "DYAD_PATH_CONSUMER={consumer_managed_directory} flux exec -r 1 \\\n", + "{consumer_program} {num_files_transfered} {consumer_managed_directory}\".format(\n", + " preload=\"LD_PRELOAD=\\\"/usr/lib/dyad_wrapper.so\\\"\" if producer_program.split(\"/\")[-1].strip().startswith(\"c_\") else \"\",\n", + " kvs_namespace=kvs_namespace,\n", + " dtl_mode=dtl_mode,\n", + " consumer_managed_directory=consumer_managed_directory,\n", + " consumer_program=consumer_program,\n", + " num_files_transfered=num_files_transfered,\n", + ")\n", + "print(consumer_launch_cmd)" + ] + }, + { + "cell_type": "markdown", + "id": "7f51f9ea-c48c-4b75-a780-92059a1c7c61", + "metadata": {}, + "source": [ + "Finally, we will run the producer and consumer applications. Thanks to DYAD's fine-grained, per-file synchronization features, the order in which we launch the applications does not matter. In this example, we will run the consumer first to illustrate DYAD's synchronization features.\n", + "\n", + "Run the cell below to run the consumer. You will see that the consumer will immediately begin waiting for data to be made available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b2d62ba-d772-40a9-ab54-cef5695fc869", + "metadata": {}, + "outputs": [], + "source": [ + "!{consumer_launch_cmd}" + ] + }, + { + "cell_type": "markdown", + "id": "c413a90d-7429-4cad-bb98-fdaf8bbe5644", + "metadata": {}, + "source": [ + "Now that the consumer is running, we will run the producer. However, Jupyter will not let us launch the producer from within this notebook for as long as the consumer is running. To get around this, we will use the Jupyter Lab terminal.\n", + "\n", + "First, copy the producer command from above. Then, from the top of the file explorer on the left, click the plus (`+`) button. In the new Jupyter Lab tab that opens, click on \"Terminal\" (in the \"Other\" category) to launch the Jupyter Lab terminal. Finally, paste the producer command into the terminal, and run it.\n", + "\n", + "We know that the applications ran successfully if the consumer outputs \"OK\" for each file it checks." + ] + }, + { + "cell_type": "markdown", + "id": "c03e13e5-f8f8-4e33-bd5a-9432309dc2e8", + "metadata": {}, + "source": [ + "To see that the files were transfered, we can check the contents of the producer-managed and consumer-managed directories. If everything worked correctly, we will see the same files in both directories.\n", + "\n", + "Run the next two cells to check the contents of these directories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b4e23bc-2f13-4bc3-bf58-7a2dc838d47c", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r 0 ls -lah {producer_managed_directory}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55ee61b5-1df8-4036-8709-23dec67de7d6", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r 1 ls -lah {consumer_managed_directory}" + ] + }, + { + "cell_type": "markdown", + "id": "1a5ca339-d930-48d5-89f6-519b01a92fc6", + "metadata": {}, + "source": [ + "Before moving onto the next example, we need to remove the KVS namespace and unload the DYAD module. We cannot just reuse the namspace and module from this example for two reasons.\n", + "\n", + "First, the keys in the KVS that DYAD uses are based on the paths to the files *relative to the producer- and consumer-managed directories.* Since we are using the same applications for the next example, these relative paths will be the same, which means the keys will already be present in the KVS. This can interfere with the synchronization of the consumer.\n", + "\n", + "Second, the DYAD module currently tracks only a single directory at a time. We will be using a different directory for the next example, so we will need to startup the DYAD module from scratch to track this new directory.\n", + "\n", + "Run the next two cells to unload the DYAD module and remove the KVS namespace." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bcea21e-f2e2-488f-bd95-8534f78c70b6", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r all flux module unload dyad" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3197840-01e4-4479-8d9b-440e03155ca9", + "metadata": {}, + "outputs": [], + "source": [ + "!flux kvs namespace remove {kvs_namespace}" + ] + }, + { + "cell_type": "markdown", + "id": "4f437928-c3b6-4ff7-8d78-7ed31b09cda0", + "metadata": {}, + "source": [ + "Run this cell to verify that the DYAD module and KVS namespace are no longer present." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d537769c-6566-48cf-a150-20d40150f059", + "metadata": {}, + "outputs": [], + "source": [ + "!echo \"Modules Post-Cleanup\"\n", + "!echo \"====================\"\n", + "!flux module list\n", + "!echo \"\"\n", + "!echo \"KVS Namespaces Post-Cleanup\"\n", + "!echo \"===========================\"\n", + "!flux kvs namespace list" + ] + }, + { + "cell_type": "markdown", + "id": "de9f7143-f22a-4eea-911e-582f6c90e529", + "metadata": {}, + "source": [ + "## Example 2\n", + "\n", + "In the second example, we will show how DYAD can help workflows even if data is in shared storage (e.g., parallel file system) by still providing built-in and transparent fine-grained synchronization.\n", + "\n", + "The figure below illustrates the data movement that will happen in this example." + ] + }, + { + "cell_type": "markdown", + "id": "90ed7911-f507-4a69-a6f9-59185887a097", + "metadata": {}, + "source": [ + "
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "1c13f27e-551c-4841-8f58-825844d88cd9", + "metadata": {}, + "source": [ + "To start, we must setup the Flux KVS namespace and DYAD module again. \n", + "\n", + "Run the cells below to setup the Flux KVS namespace and the DYAD module." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a9bcc65-0780-4652-87b3-a0b942dd48b2", + "metadata": {}, + "outputs": [], + "source": [ + "!flux kvs namespace create {kvs_namespace}\n", + "!flux kvs namespace list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c43fcd2a-9291-407c-a313-f9be8a85cf4d", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r all flux module load {dyad_module} {shared_managed_directory} {dtl_mode}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8f3db83-8b97-440a-8b5f-f7cf76656928", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r all flux module list | grep dyad" + ] + }, + { + "cell_type": "markdown", + "id": "4b4a1949-2454-4e5b-aeba-ed420e42e620", + "metadata": {}, + "source": [ + "Next, we will generate the shell commands that we will use to run the producer and consumer applications. The only differences between these commands and the ones in Example 1 are as follows:\n", + "* The `DYAD_PATH_PRODUCER`, `DYAD_PATH_CONSUMER`, and second command-line argument to the applications all have the same value (i.e., the value of `shared_managed_directory` from the top of the notebook).\n", + "* The `DYAD_SHARED_STORAGE` environment variable is provided and set to 1. This tells DYAD to only perform fine-grained synchronization, rather than both synchronization and file transfer.\n", + "\n", + "Run the next two cells to generate the commands." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37fa6ba6-375e-4aba-9253-f5e37cc701b9", + "metadata": {}, + "outputs": [], + "source": [ + "producer_launch_cmd = \"{preload} DYAD_KVS_NAMESPACE={kvs_namespace} DYAD_DTL_MODE={dtl_mode} \\\n", + "DYAD_PATH_PRODUCER={producer_managed_directory} DYAD_SHARED_STORAGE=1 \\\n", + "flux exec -r 0 \\\n", + "{producer_program} {num_files_transfered} {producer_managed_directory}\".format(\n", + " preload=\"LD_PRELOAD=\\\"/usr/lib/dyad_wrapper.so\\\"\" if producer_program.split(\"/\")[-1].strip().startswith(\"c_\") else \"\",\n", + " kvs_namespace=kvs_namespace,\n", + " dtl_mode=dtl_mode,\n", + " producer_managed_directory=shared_managed_directory,\n", + " producer_program=producer_program,\n", + " num_files_transfered=num_files_transfered,\n", + ")\n", + "print(producer_launch_cmd)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02f13978-be62-4330-a1d7-3574c1d09573", + "metadata": {}, + "outputs": [], + "source": [ + "consumer_launch_cmd = \"{preload} DYAD_KVS_NAMESPACE={kvs_namespace} DYAD_DTL_MODE={dtl_mode} \\\n", + "DYAD_PATH_CONSUMER={consumer_managed_directory} DYAD_SHARED_STORAGE=1 \\\n", + "flux exec -r 1 \\\n", + "{consumer_program} {num_files_transfered} {consumer_managed_directory}\".format(\n", + " preload=\"LD_PRELOAD=\\\"/usr/lib/dyad_wrapper.so\\\"\" if producer_program.split(\"/\")[-1].strip().startswith(\"c_\") else \"\",\n", + " kvs_namespace=kvs_namespace,\n", + " dtl_mode=dtl_mode,\n", + " consumer_managed_directory=shared_managed_directory,\n", + " consumer_program=consumer_program,\n", + " num_files_transfered=num_files_transfered,\n", + ")\n", + "print(consumer_launch_cmd)" + ] + }, + { + "cell_type": "markdown", + "id": "c11fa139-026c-4b8d-8b64-3b73ba4c1ab8", + "metadata": {}, + "source": [ + "Finally, we will run the producer and consumer applications. To show how DYAD provides fine-grained synchronization even to shared storage workflows (e.g., workflows that use the parallel file system for data movement), we will run the consumer first.\n", + "\n", + "Run the cell below to run the consumer. The consumer will immediately begin waiting for data to be made available in shared storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce54e2b4-d5fb-4451-bdf0-143450892292", + "metadata": {}, + "outputs": [], + "source": [ + "!{consumer_launch_cmd}" + ] + }, + { + "cell_type": "markdown", + "id": "ae80ba91-149e-46b6-b1ac-3742626b0664", + "metadata": {}, + "source": [ + "Now that the consumer is running, we will run the producer. Just like Example 1, we will run the producer by copying the producer command from above and running it in the Jupyter Lab terminal.\n", + "\n", + "As with Example 1, we know that the applications ran successfully if the consumer outputs \"OK\" for each file it checks." + ] + }, + { + "cell_type": "markdown", + "id": "eb92651e-ca2d-4c88-bb3f-95aef77d3938", + "metadata": {}, + "source": [ + "Finally, we need to remove the KVS namespace and unload the DYAD module.\n", + "\n", + "Run the next two cells to do this.\n", + "\n", + "Run the final code cell to verify that the DYAD module and KVS namespace are no longer present." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8be46d2-e138-4849-9b51-6a02542f0bdd", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r all flux module unload dyad" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de0bec06-3c6b-4644-b6a3-db4183bc3d46", + "metadata": {}, + "outputs": [], + "source": [ + "!flux kvs namespace remove {kvs_namespace}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16dbbcc2-aea2-4ba7-9410-4c21c5d0858f", + "metadata": {}, + "outputs": [], + "source": [ + "!echo \"Modules Post-Cleanup\"\n", + "!echo \"====================\"\n", + "!flux module list\n", + "!echo \"\"\n", + "!echo \"KVS Namespaces Post-Cleanup\"\n", + "!echo \"===========================\"\n", + "!flux kvs namespace list" + ] + }, + { + "cell_type": "markdown", + "id": "81d7d87f-1e09-42c8-b165-8902551f6847", + "metadata": {}, + "source": [ + "# This concludes the notebook tutorial for DYAD.\n", + "\n", + "## If you are interested in learning more about DYAD, check out our [ReadTheDocs page](https://dyad.readthedocs.io/en/latest/), our [GitHub repository](https://github.com/flux-framework/dyad), and our [short paper](https://dyad.readthedocs.io/en/latest/_downloads/27090817b034a89b76e5538e148fea9e/ShortPaper_2022_eScience_LLNL.pdf) and [poster](https://dyad.readthedocs.io/en/latest/_downloads/1f11761622683662c33fe0086d1d7ad2/Poster_2022_eScience_LLNL.pdf) from eScience 2022.\n", + "\n", + "## If you are interested in working with us, please reach out to Jae-Seung Yeom (yeom2@llnl.gov) or Ian Lumsden (ilumsden@vols.utk.edu)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71d04206-343f-4407-880c-d67e659656d6", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad/dyad_example1.svg b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad/dyad_example1.svg new file mode 100644 index 0000000..e24636a --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad/dyad_example1.svg @@ -0,0 +1 @@ +ProducerConsumerNode1Node2Local StorageLocal StorageWI/O intercepting wrapper lib relying on KVS + RPCR \ No newline at end of file diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad/dyad_example2.svg b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad/dyad_example2.svg new file mode 100644 index 0000000..2539695 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/dyad/dyad_example2.svg @@ -0,0 +1 @@ +RWConsumerProducerP1P2Shared FSsync \ No newline at end of file diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/flux.ipynb b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/flux.ipynb new file mode 100644 index 0000000..9fc4c85 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/flux.ipynb @@ -0,0 +1,1651 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2507d149-dcab-458a-a554-37388e0ee13a", + "metadata": { + "tags": [] + }, + "source": [ + "
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "40e867ba-f689-4301-bb60-9a448556bb84", + "metadata": { + "tags": [] + }, + "source": [ + "# Welcome to the Flux Tutorial\n", + "\n", + "> What is Flux Framework? πŸ€”οΈ\n", + " \n", + "Flux is a flexible framework for resource management, built for your site. The framework consists of a suite of projects, tools, and libraries which may be used to build site-custom resource managers for High Performance Computing centers. Flux is a next-generation resource manager and scheduler with many transformative capabilities like hierarchical scheduling and resource management (you can think of it as \"fractal scheduling\") and directed-graph based resource representations.\n", + "\n", + "> I'm ready! How do I do this tutorial? 😁️\n", + "\n", + "To step through examples in this notebook you need to execute cells. To run a cell, press Shift+Enter on your keyboard. If you prefer, you can also paste the shell commands in the JupyterLab terminal and execute them there.\n", + "Let's get started! To provide some brief, added background on Flux and a bit more motivation for our tutorial, \"Shift+Enter\" the cell below to watch our YouTube video!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d71ecd22-8552-4b4d-9bc4-61d86f8d33fe", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%html\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "15e82c38-8465-49ac-ae2b-b0bb56a79ec9", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started with Flux\n", + "\n", + "The code and examples that this tutorial is based on can be found at [flux-framework/Tutorials](https://github.com/flux-framework/Tutorials/tree/master/2023-RADIUSS-AWS). You can also find the examples one level up in the flux-workflow-examples directory in this JupyterLab instance.\n", + "\n", + "## Resources\n", + "\n", + "> Looking for other resources? We got you covered! πŸ€“οΈ\n", + "\n", + " - [https://flux-framework.org/](https://flux-framework.org/) Flux Framework portal for projects, releases, and publication.\n", + " - [Flux Documentation](https://flux-framework.readthedocs.io/en/latest/).\n", + " - [Flux Framework Cheat Sheet](https://flux-framework.org/cheat-sheet/)\n", + " - [Flux Glossary of Terms](https://flux-framework.readthedocs.io/en/latest/glossary.html)\n", + " - [Flux Comics](https://flux-framework.readthedocs.io/en/latest/comics/fluxonomicon.html) come and meet FluxBird - the pink bird who knows things!\n", + " - [Flux Learning Guide](https://flux-framework.readthedocs.io/en/latest/guides/learning_guide.html) learn about what Flux does, how it works, and real research applications \n", + " - [Getting Started with Flux and Go](https://converged-computing.github.io/flux-go/)\n", + " - [Getting Started with Flux in C](https://converged-computing.github.io/flux-c-examples/) *looking for contributors*\n", + "\n", + "To read the Flux manpages and get help, run `flux help`. To get documentation on a subcommand, run, e.g. `flux help config`. Here is an example of running `flux help` right from the notebook. Yes, did you know we are running in a Flux Instance right now?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7d616de-70cd-4090-bd43-ffacb5ade1f6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!flux help" + ] + }, + { + "cell_type": "markdown", + "id": "ae33fef6-278c-4996-8534-fd15e548b338", + "metadata": { + "tags": [] + }, + "source": [ + "Did you know you can also get help for a specific command? For example, let's run, e.g. `flux help jobs` to get information on a sub-command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e54f640-283a-4523-8dde-9617fd6ef0c5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# We have commented this out because the output is huge! Feel free to uncomment (remove the #) and run the command\n", + "#!flux help jobs" + ] + }, + { + "cell_type": "markdown", + "id": "17e435d6-0927-4966-a4d7-47a128c94158", + "metadata": { + "tags": [] + }, + "source": [ + "### You can run any of the commands and examples that follow in the JupyterLab terminal. You can find the terminal in the JupyterLab launcher.\n", + "If you do `File -> New -> Terminal` you can open a raw terminal to play with Flux. You'll see a prompt like this: \n", + "\n", + "`Ζ’(s=4,d=0) fluxuser@6e0f43fd90eb:~$`\n", + "\n", + "`s=4` indicates the number of running Flux brokers, `d=0` indicates the Flux hierarchy depth. `@6e0f43fd90eb` references the host, which is a Docker container for our tutorial." + ] + }, + { + "cell_type": "markdown", + "id": "70e3df1d-32c9-4996-b6f7-2fa85f4c02ad", + "metadata": { + "tags": [] + }, + "source": [ + "# Creating Flux Instances\n", + "\n", + "A Flux instance is a fully functional set of services which manage compute resources under its domain with the capability to launch jobs on those resources. A Flux instance may be running as the default resource manager on a cluster, a job in a resource manager such as Slurm, LSF, or Flux itself, or as a test instance launched locally.\n", + "\n", + "When run as a job in another resource manager, Flux is started like an MPI program, e.g., under Slurm we might run `srun [OPTIONS] flux start [SCRIPT]`. Flux is unique in that a test instance which mimics a multi-node instance can be started locally with simply `flux start --test-size=N`. This offers users to a way to learn and test interfaces and commands without access to an HPC cluster.\n", + "\n", + "To start a Flux session with 4 brokers in your container, run:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d568de50-f9e0-452f-8364-e52853013d83", + "metadata": {}, + "outputs": [], + "source": [ + "!flux start --test-size=4 flux getattr size" + ] + }, + { + "cell_type": "markdown", + "id": "e693f2d9-651f-4f58-bf53-62528caa83d9", + "metadata": {}, + "source": [ + "The output indicates the number of brokers started successfully." + ] + }, + { + "cell_type": "markdown", + "id": "eda1a33c-9f9e-4ba0-a013-e97601f79e41", + "metadata": {}, + "source": [ + "## Flux uptime\n", + "Flux provides an `uptime` utility to display properties of the Flux instance such as state of the current instance, how long it has been running, its size and if scheduling is disabled or stopped. The output shows how long the instance has been up, the instance owner, the instance depth (depth in the Flux hierarchy), and the size of the instance (number of brokers)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6057ce25-d1b3-4cc6-b26a-4b05a1639616", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!flux uptime" + ] + }, + { + "cell_type": "markdown", + "id": "dee2d6af-43fa-490e-88e9-10f13e660125", + "metadata": { + "tags": [] + }, + "source": [ + "# Submitting Jobs to Flux\n", + "## Submission CLI\n", + "### `flux`: the Job Submission Tool\n", + "\n", + "To submit jobs to Flux, you can use the `flux` `submit`, `run`, `bulksubmit`, `batch`, and `alloc` commands. The `flux submit` command submits a job to Flux and prints out the jobid. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a5e7d41-1d8d-426c-8198-0ad4a57e7d04", + "metadata": {}, + "outputs": [], + "source": [ + "!flux submit hostname" + ] + }, + { + "cell_type": "markdown", + "id": "a7e4c25e-3ca8-4277-bb70-a0e94bcd223b", + "metadata": {}, + "source": [ + "`submit` supports common options like `--nnodes`, `--ntasks`, and `--cores-per-task`. There are short option equivalents (`-N`, `-n`, and `-c`, respectively) of these options as well. `--cores-per-task=1` is the default." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "571d8c3d-b24a-415e-b9ac-f58b99a7e92c", + "metadata": {}, + "outputs": [], + "source": [ + "!flux submit -N1 -n2 sleep inf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc2bddee-f454-4674-80d4-4a39c5f1bee2", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's peek at the help for flux submit!\n", + "!flux submit --help | head -n 15" + ] + }, + { + "cell_type": "markdown", + "id": "ac798095", + "metadata": {}, + "source": [ + "The `flux run` command submits a job to Flux (similar to `flux submit`) but then attaches to the job with `flux job attach`, printing the job's stdout/stderr to the terminal and exiting with the same exit code as the job:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52d26496-dd1f-44f7-bb10-8a9b4b8c9c80", + "metadata": {}, + "outputs": [], + "source": [ + "!flux run hostname" + ] + }, + { + "cell_type": "markdown", + "id": "53357a9d-11d8-4c2d-87d8-c30ae38d01ba", + "metadata": {}, + "source": [ + "The output from the previous command is the hostname (a container ID string in this case). If the job exits with a non-zero exit code this will be reported by `flux job attach` (occurs implicitly with `flux run`). For example, execute the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa40cb98-a138-4771-a7ef-f1860dddf7db", + "metadata": {}, + "outputs": [], + "source": [ + "!flux run /bin/false" + ] + }, + { + "cell_type": "markdown", + "id": "6b2b5c3f-e24a-45a8-a10c-e10bfdbb7b87", + "metadata": {}, + "source": [ + "A job submitted with `run` can be canceled with two rapid `Cltr-C`s in succession, or a user can detach from the job with `Ctrl-C Ctrl-Z`. The user can then re-attach to the job by using `flux job attach JOBID`." + ] + }, + { + "cell_type": "markdown", + "id": "81e5213d", + "metadata": {}, + "source": [ + "`flux submit` and `flux run` also support many other useful flags:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02032748", + "metadata": {}, + "outputs": [], + "source": [ + "!flux run -n4 --label-io --time-limit=5s --env-remove=LD_LIBRARY_PATH hostname" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f52bb357-a7ce-458d-9c3f-4d664eca4fbd", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and run this help command if you want to see all the flags for flux run\n", + "# !flux run --help" + ] + }, + { + "cell_type": "markdown", + "id": "91e9ed6c", + "metadata": {}, + "source": [ + "The `flux bulksubmit` command enqueues jobs based on a set of inputs which are substituted on the command line, similar to `xargs` and the GNU `parallel` utility, except the jobs have access to the resources of an entire Flux instance instead of only the local system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0e82702", + "metadata": {}, + "outputs": [], + "source": [ + "!flux bulksubmit --watch --wait echo {} ::: foo bar baz" + ] + }, + { + "cell_type": "markdown", + "id": "392a8056-1661-4b76-9ca3-5e536c687e82", + "metadata": {}, + "source": [ + "The `--cc` option to `submit` makes repeated submission even easier via, `flux submit --cc=IDSET`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ea1962b-1831-4bd2-8dab-c61fd710df9c", + "metadata": {}, + "outputs": [], + "source": [ + "!flux submit --cc=1-10 --watch hostname" + ] + }, + { + "cell_type": "markdown", + "id": "27ca3706-8bb4-4fd6-a37c-e6135fb05604", + "metadata": {}, + "source": [ + "Try it in the JupyterLab terminal with a progress bar and jobs/s rate report: `flux submit --cc=1-100 --watch --progress --jps hostname`\n", + "\n", + "Note that `--wait` is implied by `--watch`." + ] + }, + { + "cell_type": "markdown", + "id": "4c5a18ff-8d6a-47e9-a164-931ed1275ef4", + "metadata": {}, + "source": [ + "Of course, Flux can launch more than just single-node, single-core jobs. We can submit multiple heterogeneous jobs and Flux will co-schedule the jobs while also ensuring no oversubscription of resources (e.g., cores).\n", + "\n", + "Note: in this tutorial, we cannot assume that the host you are running on has multiple cores, thus the examples below only vary the number of nodes per job. Varying the `cores-per-task` is also possible on Flux when the underlying hardware supports it (e.g., a multi-core node)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "brazilian-former", + "metadata": {}, + "outputs": [], + "source": [ + "!flux submit --nodes=2 --ntasks=2 --cores-per-task=1 --job-name simulation sleep inf\n", + "!flux submit --nodes=1 --ntasks=1 --cores-per-task=1 --job-name analysis sleep inf" + ] + }, + { + "cell_type": "markdown", + "id": "641f446c-b2e8-40d8-b6bd-eb6b9dba3c71", + "metadata": {}, + "source": [ + "### `flux watch` to watch jobs\n", + "\n", + "Wouldn't it be cool to submit a job and then watch it? Well, yeah! We can do this now with flux watch. Let's run a fun example, and then watch the output. We have sleeps in here interspersed with echos only to show you the live action! πŸ₯žοΈ\n", + "Also note a nice trick - you can always use `flux job last` to get the last JOBID.\n", + "Here is an example (not runnable, as notebooks don't support environment variables) for getting and saving a job id:\n", + "\n", + "```bash\n", + "flux submit hostname\n", + "JOBID=$(flux job last)\n", + "```\n", + "\n", + "And then you could use the variable `$JOBID` in your subsequent script or interactions with Flux! So what makes `flux watch` different from `flux job attach`? Aside from the fact that `flux watch` is read-only, `flux watch` can watch many (or even all (`flux watch --all`) jobs at once!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ad231c2-4cdb-4d18-afc2-7cb3a74759c2", + "metadata": {}, + "outputs": [], + "source": [ + "!flux submit ../flux-workflow-examples/job-watch/job-watch.sh\n", + "!flux watch $(flux job last)" + ] + }, + { + "cell_type": "markdown", + "id": "3f8c2af2", + "metadata": {}, + "source": [ + "### Listing job properties with `flux jobs`\n", + "\n", + "We can now list the jobs in the queue with `flux jobs` and we should see both jobs that we just submitted. Jobs that are instances are colored blue in output, red jobs are failed jobs, and green jobs are those that completed successfully. Note that the JupyterLab notebook may not display these colors. You will be able to see them in the terminal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "institutional-vocabulary", + "metadata": {}, + "outputs": [], + "source": [ + "!flux jobs" + ] + }, + { + "cell_type": "markdown", + "id": "77ca4277", + "metadata": {}, + "source": [ + "Since those jobs won't ever exit (and we didn't specify a timelimit), let's cancel them all now and free up the resources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46dd8ec8-6c64-4d8d-9a00-949f5f58c07b", + "metadata": {}, + "outputs": [], + "source": [ + "# This was previously flux cancelall -f\n", + "!flux cancel --all\n", + "!flux jobs" + ] + }, + { + "cell_type": "markdown", + "id": "544aa0a9", + "metadata": {}, + "source": [ + "We can use the `flux batch` command to easily created nested flux instances. When `flux batch` is invoked, Flux will automatically create a nested instance that spans the resources allocated to the job, and then Flux runs the batch script passed to `flux batch` on rank 0 of the nested instance. \"Rank\" refers to the rank of the Tree-Based Overlay Network (TBON) used by the Flux brokers: https://flux-framework.readthedocs.io/projects/flux-core/en/latest/man1/flux-broker.html\n", + "\n", + "While a batch script is expected to launch parallel jobs using `flux run` or `flux submit` at this level, nothing prevents the script from further batching other sub-batch-jobs using the `flux batch` interface, if desired.\n", + "\n", + "Note: Flux also provides a `flux alloc` which is an interactive version of `flux batch`, but demonstrating that in a Jupyter notebook is difficult due to the lack of pseudo-terminal." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "blank-carpet", + "metadata": {}, + "outputs": [], + "source": [ + "!flux batch --nslots=2 --cores-per-slot=1 --nodes=2 ./sleep_batch.sh\n", + "!flux batch --nslots=2 --cores-per-slot=1 --nodes=2 ./sleep_batch.sh" + ] + }, + { + "cell_type": "markdown", + "id": "da98bfa1", + "metadata": {}, + "source": [ + "The contents of `sleep_batch.sh`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "381a3f6c-0da1-4923-801f-486ca5226d3c", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Code\n", + "Code(filename='sleep_batch.sh', language='bash')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "edff8993-3c39-4f46-939d-4c8be5739fbc", + "metadata": {}, + "outputs": [], + "source": [ + "# Here we are submitting a job that generates output, and asking to write it to /tmp/cheese.txt\n", + "!flux submit --out /tmp/cheese.txt echo \"Sweet dreams 🌚️ are made of cheese, who am I to diss a brie? πŸ§€οΈ\"\n", + "\n", + "# This will show us JOBIDs\n", + "!flux jobs\n", + "\n", + "# We can even see jobs in sub-instances with \"-R\" (for recursive)\n", + "!flux jobs -R\n", + "\n", + "# You could copy a JOBID from above and paste it in the line below to examine the job's resources and output\n", + "# or get the last jobid with \"flux job last\" (this is what we will do here)\n", + "# JOBID=\"Ζ’FoRYVpt7\"\n", + "\n", + "# Note here we are using flux job last to see the last one\n", + "# The \"R\" here asks for the resource spec\n", + "!flux job info $(flux job last) R\n", + "\n", + "# When we attach it will direct us to our output file\n", + "!flux job attach $(flux job last)\n", + "\n", + "# And we can look at the output file to see our expected output!\n", + "from IPython.display import Code\n", + "Code(filename='/tmp/cheese.txt', language='text')" + ] + }, + { + "cell_type": "markdown", + "id": "f4e525e2-6c89-4c14-9fae-d87a0d4fc574", + "metadata": {}, + "source": [ + "To list all completed jobs, run `flux jobs -a`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df8a8b7c-f475-4a51-8bc6-9983dc9d78ab", + "metadata": {}, + "outputs": [], + "source": [ + "!flux jobs -a" + ] + }, + { + "cell_type": "markdown", + "id": "3e415ecc-f451-4909-a2bf-351a639cd7fa", + "metadata": {}, + "source": [ + "To restrict the output to failed (i.e., jobs that exit with nonzero exit code, time out, or are canceled or killed) jobs, run:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "032597d2-4b02-47ea-a5e5-915313cdd7f9", + "metadata": {}, + "outputs": [], + "source": [ + "!flux jobs -f failed" + ] + }, + { + "cell_type": "markdown", + "id": "04b405b1-219f-489c-abfc-e2983e82124a", + "metadata": {}, + "source": [ + "# The Flux Hierarchy\n", + "\n", + "One feature of the Flux Framework scheduler that is unique is its ability to submit jobs within instances, where an instance can be thought of as a level in a graph. Let's start with a basic image - this is what it might look like to submit to a scheduler that is not graph-based,\n", + "where all jobs go to a central job queue or database. Note that our maximum job throughput is one job per second.\n", + "\n", + "![img/single-submit.png](img/single-submit.png)\n", + "\n", + "The throughput is limited by the workload manager's ability to process a single job. We can improve upon this by simply adding another level, perhaps with three instances. For example, let's say we create a flux allocation or batch that has control of some number of child nodes. We might launch three new instances (each with its own scheduler and queue) at that level two, and all of a sudden, we get a throughput of 1x3, or three jobs per second. \n", + "\n", + "![img/instance-submit.png](img/instance-submit.png)\n", + "\n", + "\n", + "All of a sudden, the throughout can increase exponentially because we are essentially submitting to different schedulers. The example above is not impressive, but our [learning guide](https://flux-framework.readthedocs.io/en/latest/guides/learning_guide.html#fully-hierarchical-resource-management-techniques) (Figure 10) has a beautiful example of how it can scale, done via an actual experiment. We were able to submit 500 jobs/second using only three levels, vs. close to 1 job/second with one level. \n", + "\n", + "![img/scaled-submit.png](img/scaled-submit.png)\n", + "\n", + "And for an interesting detail, you can vary the scheduler algorithm or topology within each sub-instance, meaning that you can do some fairly interesting things with scheduling work, and all without stressing the top level system instance. Next, let's look at a prototype tool called `flux-tree` that you can use to see how this works.\n", + "\n", + "## Flux tree\n", + "\n", + "Flux tree is a prototype tool that allows you to easily submit work to different levels of your flux instance, or more specifically, creating a nested hierarchy of jobs that scale out. Let's run the command, look at the output, and talk about it." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2735b1ca-e761-46be-b509-a86b771628fc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TreeID Elapsed(sec) Begin(Epoch) End(Epoch) Match(usec) NJobs NNodes CPN GPN\n", + "tree 3.280890 1711671101.237483 1711671104.518378 1574.710000 4 1 4 0\n", + "tree.2 1.807340 1711671101.913623 1711671103.720962 690.937000 2 1 2 0\n", + "tree.2.2 0.116084 1711671102.753773 1711671102.869857 0.000000 1 1 1 0\n", + "tree.2.1 0.113461 1711671102.525129 1711671102.638590 0.000000 1 1 1 0\n", + "tree.1 1.823700 1711671101.837330 1711671103.661027 698.328000 2 1 2 0\n", + "tree.1.2 0.114873 1711671102.689943 1711671102.804816 0.000000 1 1 1 0\n", + "tree.1.1 0.115360 1711671102.447201 1711671102.562560 0.000000 1 1 1 0\n" + ] + } + ], + "source": [ + "!flux tree -T2x2 -J 4 -N 1 -c 4 -o ./tree.out -Q easy:fcfs hostname \n", + "! cat ./tree.out" + ] + }, + { + "cell_type": "markdown", + "id": "9d5fe7a0-af54-4c90-be6f-75f50c918dea", + "metadata": {}, + "source": [ + "In the above, we are running `flux-tree` and looking at the output file. What is happening is that the `flux tree` command is creating a hierarchy of instances. Based on their names you can tell that:\n", + "\n", + " - `2x2` in the command is the topology\n", + " - It says to create two flux instances, and make them each spawn two more.\n", + " - `tree` is the root\n", + " - `tree.1` is the first instance\n", + " - `tree.2` is the second instance\n", + " - `tree.1.1` and `tree.1.2` refer to the nested instances under `tree.1`\n", + " - `tree.2.1` and `tree.2.2` refer to the nested instances under `tree.2`\n", + " \n", + "And we provided the command `hostname` to this script, but a more complex example would generate more interested hierarchies,\n", + "and with differet functionality for each. Note that although this is just a dummy prototype, you could use `flux-tree` for actual work,\n", + "or more likely, you would want to use `flux batch` to submit multiple commands within a single flux instance to take advantage of the same\n", + "hierarchy. \n", + "\n", + "## Flux batch\n", + "\n", + "Next, let's look at an example that doesn't use `flux tree` but instead uses `flux batch`, which is how you will likely interact with your nested instances. Let's start with a batch script `hello-batch.sh`.\n", + "\n", + "##### hello-batch.sh\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "e82863e5-b2a1-456b-9ff1-f669b3525fa1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
#!/bin/bash\n",
+       "\n",
+       "flux submit --flags=waitable -N1 --out /tmp/hello-batch-1.out echo "Hello job 1 from $(hostname) πŸ’›οΈ"\n",
+       "flux submit --flags=waitable -N1 --out /tmp/hello-batch-2.out echo "Hello job 2 from $(hostname) πŸ’šοΈ"\n",
+       "flux submit --flags=waitable -N1 --out /tmp/hello-batch-3.out echo "Hello job 3 from $(hostname) πŸ’™οΈ"\n",
+       "flux submit --flags=waitable -N1 --out /tmp/hello-batch-4.out echo "Hello job 4 from $(hostname) πŸ’œοΈ"\n",
+       "# Wait for the jobs to finish\n",
+       "flux job wait --all\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{c+ch}{\\PYZsh{}!/bin/bash}\n", + "\n", + "flux\\PY{+w}{ }submit\\PY{+w}{ }\\PYZhy{}\\PYZhy{}flags\\PY{o}{=}waitable\\PY{+w}{ }\\PYZhy{}N1\\PY{+w}{ }\\PYZhy{}\\PYZhy{}out\\PY{+w}{ }/tmp/hello\\PYZhy{}batch\\PYZhy{}1.out\\PY{+w}{ }\\PY{n+nb}{echo}\\PY{+w}{ }\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{Hello job 1 from }\\PY{k}{\\PYZdl{}(}hostname\\PY{k}{)}\\PY{l+s+s2}{ πŸ’›οΈ}\\PY{l+s+s2}{\\PYZdq{}}\n", + "flux\\PY{+w}{ }submit\\PY{+w}{ }\\PYZhy{}\\PYZhy{}flags\\PY{o}{=}waitable\\PY{+w}{ }\\PYZhy{}N1\\PY{+w}{ }\\PYZhy{}\\PYZhy{}out\\PY{+w}{ }/tmp/hello\\PYZhy{}batch\\PYZhy{}2.out\\PY{+w}{ }\\PY{n+nb}{echo}\\PY{+w}{ }\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{Hello job 2 from }\\PY{k}{\\PYZdl{}(}hostname\\PY{k}{)}\\PY{l+s+s2}{ πŸ’šοΈ}\\PY{l+s+s2}{\\PYZdq{}}\n", + "flux\\PY{+w}{ }submit\\PY{+w}{ }\\PYZhy{}\\PYZhy{}flags\\PY{o}{=}waitable\\PY{+w}{ }\\PYZhy{}N1\\PY{+w}{ }\\PYZhy{}\\PYZhy{}out\\PY{+w}{ }/tmp/hello\\PYZhy{}batch\\PYZhy{}3.out\\PY{+w}{ }\\PY{n+nb}{echo}\\PY{+w}{ }\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{Hello job 3 from }\\PY{k}{\\PYZdl{}(}hostname\\PY{k}{)}\\PY{l+s+s2}{ πŸ’™οΈ}\\PY{l+s+s2}{\\PYZdq{}}\n", + "flux\\PY{+w}{ }submit\\PY{+w}{ }\\PYZhy{}\\PYZhy{}flags\\PY{o}{=}waitable\\PY{+w}{ }\\PYZhy{}N1\\PY{+w}{ }\\PYZhy{}\\PYZhy{}out\\PY{+w}{ }/tmp/hello\\PYZhy{}batch\\PYZhy{}4.out\\PY{+w}{ }\\PY{n+nb}{echo}\\PY{+w}{ }\\PY{l+s+s2}{\\PYZdq{}}\\PY{l+s+s2}{Hello job 4 from }\\PY{k}{\\PYZdl{}(}hostname\\PY{k}{)}\\PY{l+s+s2}{ πŸ’œοΈ}\\PY{l+s+s2}{\\PYZdq{}}\n", + "\\PY{c+c1}{\\PYZsh{} Wait for the jobs to finish}\n", + "flux\\PY{+w}{ }job\\PY{+w}{ }\\PY{n+nb}{wait}\\PY{+w}{ }\\PYZhy{}\\PYZhy{}all\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "#!/bin/bash\n", + "\n", + "flux submit --flags=waitable -N1 --out /tmp/hello-batch-1.out echo \"Hello job 1 from $(hostname) πŸ’›οΈ\"\n", + "flux submit --flags=waitable -N1 --out /tmp/hello-batch-2.out echo \"Hello job 2 from $(hostname) πŸ’šοΈ\"\n", + "flux submit --flags=waitable -N1 --out /tmp/hello-batch-3.out echo \"Hello job 3 from $(hostname) πŸ’™οΈ\"\n", + "flux submit --flags=waitable -N1 --out /tmp/hello-batch-4.out echo \"Hello job 4 from $(hostname) πŸ’œοΈ\"\n", + "# Wait for the jobs to finish\n", + "flux job wait --all" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Code\n", + "Code(filename='hello-batch.sh', language='bash')" + ] + }, + { + "cell_type": "markdown", + "id": "6bc17bac-2fc4-4418-8939-e930f9929976", + "metadata": {}, + "source": [ + "We would provide this script to run with `flux batch` that is going to:\n", + "\n", + "1. Create a flux instance with the top level resources you specify\n", + "2. Submit jobs to the scheduler controlled by the broker of that sub-instance\n", + "3. Run the four jobs, with `--flags=waitable` and `flux job wait --all` to wait for the output file\n", + "4. Within the batch script, you can add `--wait` or `--flags=waitable` to individual jobs, and use `flux queue drain` to wait for the queue to drain, _or_ `flux job wait --all` to wait for the jobs you flagged to finish. \n", + "\n", + "Note that when you submit a batch job, you'll get a job id back for the _batch job_, and usually when you look at the output of that with `flux job attach $jobid` you will see the output file(s) where the internal contents are written. Since we want to print the output file easily to the terminal, we are waiting for the batch job by adding the `--flags=waitable` and then waiting for it. Let's try to run our batch job now." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "72358a03-6f1f-4c5e-91eb-cab71883a232", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ζ’2LcUUanSB\n", + "Ζ’2LcUUanSB\n", + "Hello job 1 from c32cc16d4b78 πŸ’›οΈ\n", + "Hello job 2 from c32cc16d4b78 πŸ’šοΈ\n", + "Hello job 3 from c32cc16d4b78 πŸ’™οΈ\n", + "Hello job 4 from c32cc16d4b78 πŸ’œοΈ\n" + ] + } + ], + "source": [ + "! flux batch --flags=waitable --out /tmp/flux-batch.out -N2 ./hello-batch.sh\n", + "! flux job wait\n", + "! cat /tmp/hello-batch-1.out\n", + "! cat /tmp/hello-batch-2.out\n", + "! cat /tmp/hello-batch-3.out\n", + "! cat /tmp/hello-batch-4.out" + ] + }, + { + "cell_type": "markdown", + "id": "75c0ae3f-2813-4ae8-83be-00be3df92a4b", + "metadata": {}, + "source": [ + "Excellent! Now let's look at another batch example. Here we have two job scripts:\n", + "\n", + "- sub_job1.sh: Is going to be run with `flux batch` and submit sub_job2.sh\n", + "- sub_job2.sh: Is going to be submit by sub_job1.sh.\n", + "\n", + "You can see that below." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "2e6976f8-dbb6-405e-a06b-47c571aa1cdf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
#!/bin/bash\n",
+       "\n",
+       "flux batch -N1 ./sub_job2.sh\n",
+       "flux queue drain\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{c+ch}{\\PYZsh{}!/bin/bash}\n", + "\n", + "flux\\PY{+w}{ }batch\\PY{+w}{ }\\PYZhy{}N1\\PY{+w}{ }./sub\\PYZus{}job2.sh\n", + "flux\\PY{+w}{ }queue\\PY{+w}{ }drain\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "#!/bin/bash\n", + "\n", + "flux batch -N1 ./sub_job2.sh\n", + "flux queue drain\n" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Code(filename='sub_job1.sh', language='bash')" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "a0719cc9-6bf2-4285-b5d7-6cc534fc364c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
#!/bin/bash\n",
+       "\n",
+       "flux run -N1 sleep 30\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{c+ch}{\\PYZsh{}!/bin/bash}\n", + "\n", + "flux\\PY{+w}{ }run\\PY{+w}{ }\\PYZhy{}N1\\PY{+w}{ }sleep\\PY{+w}{ }\\PY{l+m}{30}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "#!/bin/bash\n", + "\n", + "flux run -N1 sleep 30\n" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Code(filename='sub_job2.sh', language='bash')" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "8640a611-38e4-42b1-a913-89e0c76c8014", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ζ’2Mgy7vtZm\n" + ] + } + ], + "source": [ + "# Submit it!\n", + "!flux batch -N1 ./sub_job1.sh" + ] + }, + { + "cell_type": "markdown", + "id": "b29c3a4a-2b77-4ab9-8e0c-9f5228e61016", + "metadata": {}, + "source": [ + "And now that we've submit, let's look at the hierarchy for all the jobs we just ran. Here is how to try flux pstree, which normally can show jobs in an instance, but it has limited functionality given we are in a notebook! So instead of just running the single command, let's add \"-a\" to indicate \"show me ALL jobs.\"\n", + "More complex jobs and in a different environment would have deeper nesting. You can [see examples here](https://flux-framework.readthedocs.io/en/latest/jobs/hierarchies.html?h=pstree#flux-pstree-command)." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "2d2b1f0b-e6c2-4583-8068-7c76fa341884", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".\n", + "β”œβ”€β”€ ./sub_job1.sh:CD\n", + "β”œβ”€β”€ 20*[./hello-batch.sh:CD]\n", + "β”œβ”€β”€ 2*[flux-tree-y7zovVRbptUhPPfV2MKXPJVWwQN9HigN:CD]\n", + "β”œβ”€β”€ 2*[flux-tree-V6rPdUViHrklYfiMxYosv7lEHKgrYLJF:CD]\n", + "β”œβ”€β”€ 2*[flux-tree-oEof1Dm3CMim8MBCpppJf6I3hdxx2aYI:CD]\n", + "β”œβ”€β”€ 2*[flux-tree-tZr1SYP6yAkYvbD7t3g9XziIRVtubVlP:CD]\n", + "└── 2*[flux-tree-5AO4EWvE6Nr1lPfb2qvVje99HKC2ZTYh:CD]\n" + ] + } + ], + "source": [ + "!flux pstree -a" + ] + }, + { + "cell_type": "markdown", + "id": "7724130f-b0db-4ccf-a01e-98907b9a27ca", + "metadata": {}, + "source": [ + "You can also try a more detailed view with `flux pstree -a -X`!" + ] + }, + { + "cell_type": "markdown", + "id": "03e2ae62-3e3b-4c82-a0c7-4c97ff1376d2", + "metadata": {}, + "source": [ + "# Flux Process and Job Utilities\n", + "## Flux top\n", + "Flux provides a feature-full version of `top` for nested Flux instances and jobs. In the JupyterLab terminal, invoke `flux top` to see the \"sleep\" jobs. If they have already completed you can resubmit them. \n", + "\n", + "We recommend not running `flux top` in the notebook as it is not designed to display output from a command that runs continuously.\n", + "\n", + "## Flux pstree\n", + "In analogy to `top`, Flux provides `flux pstree`. Try it out in the JupyterLab terminal or here in the notebook.\n", + "\n", + "## Flux proxy\n", + "\n", + "### Interacting with a job hierarchy with `flux proxy`\n", + "\n", + "Flux proxy is used to route messages to and from a Flux instance. We can use `flux proxy` to connect to a running Flux instance and then submit more nested jobs inside it. You may want to edit `sleep_batch.sh` with the JupyterLab text editor (double click the file in the window on the left) to sleep for `60` or `120` seconds. Then from the JupyterLab terminal, run, you'll want to run the below. Yes, we really want you to open a terminal in the Jupyter launcher FILE-> NEW -> TERMINAL and run the commands below!" + ] + }, + { + "cell_type": "markdown", + "id": "a609b2f8-e24d-40c7-b022-ce02e91a49f8", + "metadata": {}, + "source": [ + "```bash\n", + "# The terminal will start at the root, ensure you are in the right spot!\n", + "# jovyan - that's you! \n", + "cd /home/jovyan/flux-radiuss-tutorial-2023/notebook/\n", + "\n", + "# Outputs the JOBID\n", + "flux batch --nslots=2 --cores-per-slot=1 --nodes=2 ./sleep_batch.sh\n", + "\n", + "# Put the JOBID into an environment variable\n", + "JOBID=$(flux job last)\n", + "\n", + "# See the flux process tree\n", + "flux pstree -a\n", + "\n", + "# Connect to the Flux instance corresponding to JOBID above\n", + "flux proxy ${JOBID}\n", + "\n", + "# Note the depth is now 1 and the size is 2: we're one level deeper in a Flux hierarchy and we have only 2 brokers now.\n", + "flux uptime\n", + "\n", + "# This instance has 2 \"nodes\" and 2 cores allocated to it\n", + "flux resource list\n", + "\n", + "# Have you used the top command in your terminal? We have one for flux!\n", + "flux top\n", + "```\n", + "\n", + "`flux top` was pretty cool, right? 😎️" + ] + }, + { + "cell_type": "markdown", + "id": "997faffc", + "metadata": {}, + "source": [ + "## Submission API\n", + "Flux also provides first-class python bindings which can be used to submit jobs programmatically. The following script shows this with the `flux.job.submit()` call:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "third-comment", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "import flux\n", + "from flux.job import JobspecV1\n", + "from flux.job.JobID import JobID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "selective-uganda", + "metadata": {}, + "outputs": [], + "source": [ + "f = flux.Flux() # connect to the running Flux instance\n", + "compute_jobreq = JobspecV1.from_command(\n", + " command=[\"./compute.py\", \"120\"], num_tasks=1, num_nodes=1, cores_per_task=1\n", + ") # construct a jobspec\n", + "compute_jobreq.cwd = os.path.expanduser(\"~/flux-tutorial/flux-workflow-examples/job-submit-api/\") # set the CWD\n", + "print(JobID(flux.job.submit(f,compute_jobreq)).f58) # submit and print out the jobid (in f58 format)" + ] + }, + { + "cell_type": "markdown", + "id": "0c4b260f-f08a-46ae-ad66-805911a857a7", + "metadata": {}, + "source": [ + "### `flux.job.get_job(handle, jobid)` to get job info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed65cb46-8d8a-41f0-bec1-92b9a89e6db2", + "metadata": {}, + "outputs": [], + "source": [ + "# This is a new command to get info about your job from the id!\n", + "fluxjob = flux.job.submit(f,compute_jobreq)\n", + "fluxjobid = JobID(fluxjob.f58)\n", + "print(f\"πŸŽ‰οΈ Hooray, we just submitted {fluxjobid}!\")\n", + "\n", + "# Here is how to get your info. The first argument is the flux handle, then the jobid\n", + "jobinfo = flux.job.get_job(f, fluxjobid)\n", + "print(json.dumps(jobinfo, indent=4))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d679897-7054-4f96-b340-7f39245aca89", + "metadata": {}, + "outputs": [], + "source": [ + "!flux jobs -a | grep compute" + ] + }, + { + "cell_type": "markdown", + "id": "d332f9c9", + "metadata": {}, + "source": [ + "Under the hood, the `Jobspec` class is creating a YAML document that ultimately gets serialized as JSON and sent to Flux for ingestion, validation, queueing, scheduling, and eventually execution. We can dump the raw JSON jobspec that is submitted, where we can see the exact resources requested and the task set to be executed on those resources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efa06478", + "metadata": {}, + "outputs": [], + "source": [ + "print(compute_jobreq.dumps(indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "73bbc90e", + "metadata": {}, + "source": [ + "We can then replicate our previous example of submitting multiple heterogeneous jobs and testing that Flux co-schedules them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "industrial-privacy", + "metadata": {}, + "outputs": [], + "source": [ + "compute_jobreq = JobspecV1.from_command(\n", + " command=[\"./compute.py\", \"120\"], num_tasks=4, num_nodes=2, cores_per_task=2\n", + ")\n", + "compute_jobreq.cwd = os.path.expanduser(\"~/flux-tutorial/flux-workflow-examples/job-submit-api/\")\n", + "print(JobID(flux.job.submit(f, compute_jobreq)))\n", + "\n", + "io_jobreq = JobspecV1.from_command(\n", + " command=[\"./io-forwarding.py\", \"120\"], num_tasks=1, num_nodes=1, cores_per_task=1\n", + ")\n", + "io_jobreq.cwd = os.path.expanduser(\"~/flux-tutorial/flux-workflow-examples/job-submit-api/\")\n", + "print(JobID(flux.job.submit(f, io_jobreq)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "pregnant-creativity", + "metadata": {}, + "outputs": [], + "source": [ + "!flux jobs -a | grep compute" + ] + }, + { + "cell_type": "markdown", + "id": "a8051640", + "metadata": {}, + "source": [ + "We can use the FluxExecutor class to submit large numbers of jobs to Flux. This method uses python's `concurrent.futures` interface. Example snippet from `~/flux-workflow-examples/async-bulk-job-submit/bulksubmit_executor.py`:" + ] + }, + { + "cell_type": "markdown", + "id": "binary-trace", + "metadata": {}, + "source": [ + "``` python \n", + "with FluxExecutor() as executor:\n", + " compute_jobspec = JobspecV1.from_command(args.command)\n", + " futures = [executor.submit(compute_jobspec) for _ in range(args.njobs)]\n", + " # wait for the jobid for each job, as a proxy for the job being submitted\n", + " for fut in futures:\n", + " fut.jobid()\n", + " # all jobs submitted - print timings\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cleared-lawsuit", + "metadata": {}, + "outputs": [], + "source": [ + "# Submit a FluxExecutor based script.\n", + "%run ../flux-workflow-examples/async-bulk-job-submit/bulksubmit_executor.py -n200 /bin/sleep 0" + ] + }, + { + "cell_type": "markdown", + "id": "e1f041b1-ebe3-49d7-b522-79013e29acfa", + "metadata": {}, + "source": [ + "# Flux Archive\n", + "\n", + "As Flux is more increasingly used in cloud environments, you might find yourself in a situation of having a cluster without a shared filesystem! Have no fear, the `flux archive` command is here to help!\n", + "At a high level, `flux archive` is allowing you to save named pieces of data (text or data files) to the Flux key value store (KVS) for later retrieval.\n", + "Since this tutorial is running on one node it won't make a lot of sense, but we will show you how to use it. The first thing you'll want to do is make a named archive." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0114079f-26a3-4614-a8b2-6422ee2170a2", + "metadata": {}, + "outputs": [], + "source": [ + "! touch shared-file.txt\n", + "! flux archive create --name myshare --directory $(pwd) shared-file.txt" + ] + }, + { + "cell_type": "markdown", + "id": "e33173df-adbf-4028-8795-7f68d7dc66ba", + "metadata": {}, + "source": [ + "We would then want to send this file to the other nodes, and from the same node. We can combine two commands to do that:\n", + "\n", + "- `flux exec` executes commands on instance nodes, optionally excluding ranks with `-x`\n", + "- `flux archive extract` does the extraction\n", + "\n", + "So we might put them together to look like this - asking for all ranks, but excluding (`-x`) rank 0 where we are currently sitting and the file already exists." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "05769493-54a9-453c-9c5e-516123a274c2", + "metadata": {}, + "outputs": [], + "source": [ + "! flux exec --rank all -x 0 flux archive extract --name myshare --directory $(pwd) shared-file.txt" + ] + }, + { + "cell_type": "markdown", + "id": "4df4ee23-4cce-4df8-9c99-e5cd3a4ae277", + "metadata": {}, + "source": [ + "If the extraction directory doesn't exist on the other nodes yet? No problem! We can use `flux exec` to execute a command to the other nodes to create it, again with `-x 0` to exclude rank 0 (where the directory already exists). Note that you'd run this _before_ `flux archive extract` and we are using `-r` as a shorthand for `--rank`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "351415e0-4644-49bc-b4b1-b3ab3544d527", + "metadata": {}, + "outputs": [], + "source": [ + "! flux exec -r all -x 0 mkdir -p $(pwd)" + ] + }, + { + "cell_type": "markdown", + "id": "781bb105-4977-4022-a0bf-0bc53d73b2e4", + "metadata": {}, + "source": [ + "When you are done, it's good practice to clean up and remove the archive. Also note that for larger files, you can use `--mmap` to memory map content." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "acde2ba8-ade9-450e-8ff9-2b0f094166b9", + "metadata": {}, + "outputs": [], + "source": [ + "! flux archive remove --name myshare" + ] + }, + { + "cell_type": "markdown", + "id": "ec052119", + "metadata": {}, + "source": [ + "Finally, note that older versions of flux used `flux filemap` instead of flux archive. It's largely the same command with a rename.\n", + "\n", + "# Diving Deeper Into Flux's Internals\n", + "\n", + "Flux uses [hwloc](https://github.com/open-mpi/hwloc) to detect the resources on each node and then to populate its resource graph.\n", + "\n", + "You can access the topology information that Flux collects with the `flux resource` subcommand:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "scenic-chassis", + "metadata": {}, + "outputs": [], + "source": [ + "!flux resource list" + ] + }, + { + "cell_type": "markdown", + "id": "0086e47e", + "metadata": {}, + "source": [ + "Flux can also bootstrap its resource graph based on static input files, like in the case of a multi-user system instance setup by site administrators. [More information on Flux's static resource configuration files](https://flux-framework.readthedocs.io/en/latest/adminguide.html#resource-configuration). Flux provides a more standard interface to listing available resources that works regardless of the resource input source: `flux resource`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "prime-equilibrium", + "metadata": {}, + "outputs": [], + "source": [ + "# To view status of resources\n", + "!flux resource status" + ] + }, + { + "cell_type": "markdown", + "id": "5ee1c49d", + "metadata": {}, + "source": [ + "Flux has a command for controlling the queue within the `job-manager`: `flux queue`. This includes disabling job submission, re-enabling it, waiting for the queue to become idle or empty, and checking the queue status:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "800de4eb", + "metadata": {}, + "outputs": [], + "source": [ + "!flux queue disable \"maintenance outage\"\n", + "!flux queue enable\n", + "!flux queue -h" + ] + }, + { + "cell_type": "markdown", + "id": "67aa7559", + "metadata": {}, + "source": [ + "Each Flux instance has a set of attributes that are set at startup that affect the operation of Flux, such as `rank`, `size`, and `local-uri` (the Unix socket usable for communicating with Flux). Many of these attributes can be modified at runtime, such as `log-stderr-level` (1 logs only critical messages to stderr while 7 logs everything, including debug messages)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "biblical-generic", + "metadata": {}, + "outputs": [], + "source": [ + "!flux getattr rank\n", + "!flux getattr size\n", + "!flux getattr local-uri\n", + "!flux setattr log-stderr-level 3\n", + "!flux lsattr -v" + ] + }, + { + "cell_type": "markdown", + "id": "d74fdfcf", + "metadata": {}, + "source": [ + "Services within a Flux instance are implemented by modules. To query and manage broker modules, use `flux module`. Modules that we have already directly interacted with in this tutorial include `resource` (via `flux resource`), `job-ingest` (via `flux` and the Python API) `job-list` (via `flux jobs`) and `job-manager` (via `flux queue`), and we will interact with the `kvs` module in a few cells. For the most part, services are implemented by modules of the same name (e.g., `kvs` implements the `kvs` service and thus the `kvs.lookup` RPC). In some circumstances, where multiple implementations for a service exist, a module of a different name implements a given service (e.g., in this instance, `sched-fluxion-qmanager` provides the `sched` service and thus `sched.alloc`, but in another instance `sched-simple` might provide the `sched` service)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "spatial-maintenance", + "metadata": {}, + "outputs": [], + "source": [ + "!flux module list" + ] + }, + { + "cell_type": "markdown", + "id": "ad7090eb", + "metadata": {}, + "source": [ + "We can actually unload the Fluxion modules (the scheduler modules from flux-sched) and replace them with `sched-simple` (the scheduler that comes built-into flux-core) as a demonstration of this functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df4bc2d5", + "metadata": {}, + "outputs": [], + "source": [ + "!flux module unload sched-fluxion-qmanager\n", + "!flux module unload sched-fluxion-resource\n", + "!flux module load sched-simple\n", + "!flux module list" + ] + }, + { + "cell_type": "markdown", + "id": "722c4ecf", + "metadata": {}, + "source": [ + "We can now reload the Fluxion scheduler, but this time, let's pass some extra arguments to specialize our Flux instance. In particular, let's populate our resource graph with nodes, sockets, and cores and limit the scheduling depth to 4." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c34899ba", + "metadata": {}, + "outputs": [], + "source": [ + "!flux dmesg -C\n", + "!flux module unload sched-simple\n", + "!flux module load sched-fluxion-resource load-allowlist=node,socket,core\n", + "!flux module load sched-fluxion-qmanager queue-params=queue-depth=4\n", + "!flux module list\n", + "!flux dmesg | grep queue-depth" + ] + }, + { + "cell_type": "markdown", + "id": "ed4b0e04", + "metadata": {}, + "source": [ + "The key-value store (KVS) is a core component of a Flux instance. The `flux kvs` command provides a utility to list and manipulate values of the KVS. Modules of Flux use the KVS to persistently store information and retrieve it later on (potentially after a restart of Flux). One example of KVS use by Flux is the `resource` module, which stores the resource set `R` of the current Flux instance:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "nervous-broadcast", + "metadata": {}, + "outputs": [], + "source": [ + "!flux kvs ls \n", + "!flux kvs ls resource\n", + "!flux kvs get resource.R | jq" + ] + }, + { + "cell_type": "markdown", + "id": "c3920f9e", + "metadata": {}, + "source": [ + "Flux provides a built-in mechanism for executing commands on nodes without requiring a job or resource allocation: `flux exec`. `flux exec` is typically used by sys admins to execute administrative commands and load/unload modules across multiple ranks simultaneously." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9507c7b-de5c-4129-9a99-c943614a9ba2", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec -r 2 flux getattr rank # only execute on rank 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a9de119-abc4-4917-a339-2010ccc7b9b7", + "metadata": {}, + "outputs": [], + "source": [ + "!flux exec flux getattr rank # execute on all ranks" + ] + }, + { + "cell_type": "markdown", + "id": "c9c3e767-0459-4218-a8cf-0f98bd32d6bf", + "metadata": {}, + "source": [ + "# This concludes the notebook tutorial. πŸ˜­οΈπŸ˜„οΈ\n", + "\n", + "Don't worry, you'll have more opportunities for using Flux! We hope you reach out to us on any of our [project repositories](https://flux-framework.org) and ask any questions that you have. We'd love your contribution to code, documentation, or just saying hello! πŸ‘‹οΈ If you have feedback on the tutorial, please let us know so we can improve it for next year. \n", + "\n", + "> But what do I do now?\n", + "\n", + "Feel free to experiment more with Flux here, or (for more freedom) in the terminal. You can try more of the examples in the flux-workflow-examples directory one level up in the window to the left. If you're using a shared system like the one on the RADIUSS AWS tutorial please be mindful of other users and don't run compute intensive workloads. If you're running the tutorial in a job on an HPC cluster... compute away! ⚾️\n", + "\n", + "> Where can I learn to set this up on my own?\n", + "\n", + "If you're interested in installing Flux on your cluster, take a look at the [system instance instructions](https://flux-framework.readthedocs.io/en/latest/adminguide.html). If you are interested in running Flux on Kubernetes, check out the [Flux Operator](https://github.com/flux-framework/flux-operator). " + ] + }, + { + "cell_type": "markdown", + "id": "82657547-fc4a-459c-b628-3a60fea84c8d", + "metadata": {}, + "source": [ + "![https://flux-framework.org/flux-operator/_static/images/flux-operator.png](https://flux-framework.org/flux-operator/_static/images/flux-operator.png)\n", + "\n", + "> See you next year! πŸ‘‹οΈπŸ˜ŽοΈ" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/hello-batch.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/hello-batch.sh new file mode 100755 index 0000000..cc6427b --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/hello-batch.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +flux submit --flags=waitable -N1 --out /tmp/hello-batch-1.out echo "Hello job 1 from $(hostname) πŸ’›οΈ" +flux submit --flags=waitable -N1 --out /tmp/hello-batch-2.out echo "Hello job 2 from $(hostname) πŸ’šοΈ" +flux submit --flags=waitable -N1 --out /tmp/hello-batch-3.out echo "Hello job 3 from $(hostname) πŸ’™οΈ" +flux submit --flags=waitable -N1 --out /tmp/hello-batch-4.out echo "Hello job 4 from $(hostname) πŸ’œοΈ" +# Wait for the jobs to finish +flux job wait --all \ No newline at end of file diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/instance-submit.png b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/instance-submit.png new file mode 100644 index 0000000..84ce558 Binary files /dev/null and b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/instance-submit.png differ diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/scaled-submit.png b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/scaled-submit.png new file mode 100644 index 0000000..a5dc346 Binary files /dev/null and b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/scaled-submit.png differ diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/single-submit.png b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/single-submit.png new file mode 100644 index 0000000..0592def Binary files /dev/null and b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/img/single-submit.png differ diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sleep_batch.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sleep_batch.sh new file mode 100644 index 0000000..19c1560 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sleep_batch.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "Starting my batch job" +echo "Print the resources allocated to this batch job" +flux resource list + +echo "Use sleep to emulate a parallel program" +echo "Run the program at a total of 2 processes each requiring" +echo "1 core. These processes are equally spread across 2 nodes." +flux run -N 2 -n 2 sleep 30 +flux run -N 2 -n 2 sleep 30 + diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sub_job1.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sub_job1.sh new file mode 100755 index 0000000..6ec9cf8 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sub_job1.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +flux batch -N1 ./sub_job2.sh +flux queue drain + diff --git a/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sub_job2.sh b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sub_job2.sh new file mode 100755 index 0000000..d947f19 --- /dev/null +++ b/2024-RIKEN-AWS/JupyterNotebook/tutorial/notebook/sub_job2.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +flux run -N1 sleep 30 +