Observability Stack Configuration#

The BCM cm-kubernetes-setup wizard deploys the foundational observability components in Kubernetes by default. Additional configuration enables time-series and logging data sources from BCM.

Configure storage and retention for Prometheus#

  1. Create a new YAML file on the BCM head node for this setup:

    cat << EOF > prom_config.yaml
    prometheus:
      prometheusSpec:
        retention: 90d
        additionalArgs:
          - name: storage.tsdb.no-lockfile
            value: ""
        storageSpec:
          volumeClaimTemplate:
            spec:
              accessModes: ["ReadWriteOnce"]
              storageClassName: local-path
              resources:
                requests:
                  storage: 300Gi
    EOF
    

Note

The preceding prom_config.yaml sets a 90-day retention period. The local-path StorageClass uses the shared storage pool selected during Kubernetes installation. Estimate your storage requirements before setting a longer retention period.

  1. Install/upgrade CRDs:

    helm pull oci://master.cm.cluster:5000/helm-charts/kube-prometheus-stack --untar \
        --ca-file /cm/local/apps/containerd/var/etc/certs.d/master.cm.cluster:5000/ca.crt
    kubectl apply -f kube-prometheus-stack/charts/crds/crds/ --server-side
    
  2. With the configuration file in place, apply these values to the K8s cluster:

    helm upgrade kube-prometheus-stack ./kube-prometheus-stack \
        -n prometheus -f prom_config.yaml --reuse-values
    

Configure storage and retention for Loki#

This will require a redeployment of the Helm chart for Loki as we need to set a new storage value and we can’t resize this live.

  1. Create a new YAML file on the BCM head node for this setup:

    cat << EOF > loki_config.yaml
    global:
      image:
        registry: master.cm.cluster:5000
    memcached:
      image:
        repository: library/memcached
    backend:
      replicas: 3
    chunksCache:
      writebackSizeLimit: 500MB
    commonConfig:
      replication_factor: 3
    compactor:
      replicas: 0
    deploymentMode: SimpleScalable
    distributor:
      maxUnavailable: 0
      replicas: 0
    indexGateway:
      maxUnavailable: 0
      replicas: 0
    ingester:
      replicas: 0
    loki:
      limits_config:
        ingestion_rate_mb: 75
        ingestion_burst_size_mb: 150
        per_stream_rate_limit: 16MB
        per_stream_rate_limit_burst: 64MB
        retention_period: 90d
      auth_enabled: false
      commonConfig:
        replication_factor: 3
      schemaConfig:
        configs:
        - from: '2025-09-04'
          index:
            period: 24h
            prefix: loki_index_
          object_store: s3
          schema: v13
          store: tsdb
      storage:
        type: s3
        bucketNames:
         chunks: loki-chunks-bucket
         ruler: loki-ruler-bucket
         admin: loki-admin-bucket
    minio:
      enabled: true
      persistence:
        size: 500Gi
      image:
        repository: master.cm.cluster:5000/pgsty/minio
      mcImage:
        repository: master.cm.cluster:5000/pgsty/mc
    querier:
      maxUnavailable: 0
      replicas: 0
    queryFrontend:
      maxUnavailable: 0
      replicas: 0
    queryScheduler:
      replicas: 0
    read:
      replicas: 3
    singleBinary:
      replicas: 0
    write:
      replicas: 3
    EOF
    
  2. Uninstall the existing Helm chart:

    helm uninstall loki -n loki
    
  3. Delete any lingering PVCs:

    kubectl delete pvc --all -n loki
    
  4. Install the Helm chart for Loki, using the new values:

    helm install loki oci://master.cm.cluster:5000/helm-charts/loki --ca-file \
        /cm/local/apps/containerd/var/etc/certs.d/master.cm.cluster:5000/ca.crt \
        -n loki -f loki_config.yaml
    

Configure Observability#

Select the Ubuntu release of the DGX software image:

The datacenter-gpu-manager-exporter package is already installed. No package download or installation is required.

On an internet-connected host, run the command for the target DGX node architecture, regardless of the download host architecture. Replace the example package version in the URL if your deployment requires a different version.

For x86_64 DGX nodes:

curl -fLO "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2604/x86_64/datacenter-gpu-manager-exporter_4.8.4.3955-1_amd64.deb"

For aarch64 DGX nodes:

curl -fLO "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2604/sbsa/datacenter-gpu-manager-exporter_4.8.4.3955-1_arm64.deb"

Transfer the downloaded package alongside the bundle to the air-gapped environment. Keep this separately downloaded file outside bundle: nmc-airgap verify bundle checks only artifacts recorded in the bundle manifest. The download host does not need to run BCM or match the DGX node architecture.

On the BCM head node, copy the transferred package to /cm/images/<image name>/tmp/dcgm-exporter.deb. Then install it inside that image before configuring DCGM metric scraping:

cm-chroot /cm/images/<image name>
dpkg -i /tmp/dcgm-exporter.deb
exit

Enable the dcgm-exporter service in the DGX software image for either Ubuntu release:

cm-chroot-sw-img /cm/images/<image name> << EOF
  systemctl enable nvidia-dcgm-exporter.service
  exit
EOF

Follow the instructions in the following sections in Observability Stack Configuration:

  1. Enable Prometheus scrape endpoint in BCM

  2. Configure Prometheus to scrape metrics from BCM endpoint

  3. Configure Prometheus to scrape metrics from DCGM endpoints

  4. Grafana and querying BCM metrics

  5. Reduce BCM monitoring directory size

  6. Configure Grafana Single Sign-On with Keycloak

Install NMC Grafana dashboards#

As part of NVIDIA Mission Control, a set of Grafana dashboards are provided that consolidate monitoring data across the cluster. These dashboards help track ongoing operations and support troubleshooting. This dashboard serves as a starting point for visualizing infrastructure telemetry. It is encouraged to modify or extend it based on the facility’s capabilities.

Prerequisites#

To take advantage of all dashboard features, install and configure the Infinity plugin and data sources to process data from the BCM REST API and UFM REST API.

  1. Create a user to access the BCM REST API. Create user apiuser with a unique password of your choosing (do not reuse a shared or previously published value) with a read-only BCM profile:

    cmsh -c "user; add apiuser; set password <your-bcm-api-password>; set profile readonly; commit"
    
  2. Generate a Kubernetes secret for the registry CA certificate so Grafana can access the local registry to install plugins.

    kubectl create secret generic registry-ca -n prometheus --from-file \
        ca.crt=/cm/local/apps/containerd/var/etc/certs.d/master.cm.cluster:5000/ca.crt
    

    Note

    The Grafana plugin zips (including yesoreyeram-infinity-datasource) were already downloaded into bundle/files/ by the grafana-plugins component of the nmc-airgap fetch. The script in the next step scans that directory and pushes each plugin to the local registry. See Manifest and bundle structure for the bundle layout.

  3. Save the Grafana plugin upload script to a file called push-plugins.sh in the current directory

    #!/usr/bin/env bash
    set -euo pipefail
    PROG="$(basename "$0")"
    die() { echo "$PROG: error: $*" >&2; exit 1; }
    log() { echo "$PROG: $*" >&2; }
    REGISTRY="master.cm.cluster:5000"
    SCHEME="https"
    CACERT="/cm/local/apps/containerd/var/etc/certs.d/master.cm.cluster:5000/ca.crt"
    PLUGIN_PATTERNS=("grafana-metricsdrilldown-app" "grafana-lokiexplore-app" "yesoreyeram-infinity-datasource")
    VERBOSE="${VERBOSE:-0}"
    BUNDLE=""
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --bundle) BUNDLE="$2"; shift 2 ;;
            -v) VERBOSE=1; shift ;;
            -h|--help) echo "Usage: $PROG --bundle <dir> <repository>" >&2; exit 0 ;;
            --) shift; break ;;
            -*) die "Unknown option: $1" ;;
            *) break ;;
        esac
    done
    [[ $# -eq 1 ]]    || { echo "Usage: $PROG --bundle <dir> <repository>" >&2; exit 1; }
    [[ -n "$BUNDLE" ]] || die "--bundle is required"
    REPOSITORY="$1"
    [[ -d "$BUNDLE" ]]       || die "Bundle directory not found: $BUNDLE"
    [[ -d "$BUNDLE/files" ]] || die "No files/ subdirectory in bundle: $BUNDLE"
    [[ -f "$CACERT" ]]       || die "CA certificate not found: $CACERT"
    sha256_file() { command -v sha256sum &>/dev/null && sha256sum "$1" | awk '{print $1}' || shasum -a 256 "$1" | awk '{print $1}'; }
    sha256_str()  { command -v sha256sum &>/dev/null && printf '%s' "$1" | sha256sum | awk '{print $1}' || printf '%s' "$1" | shasum -a 256 | awk '{print $1}'; }
    file_size()   { stat -c%s "$1" 2>/dev/null || stat -f%z "$1"; }
    TMPDIR_WORK="$(mktemp -d)"
    trap 'rm -rf "$TMPDIR_WORK"' EXIT
    _curl() { curl -s --cacert "$CACERT" -o "$TMPDIR_WORK/response_body" -D "$TMPDIR_WORK/response_headers" -w "%{http_code}" "$@" 2>/dev/null; }
    _hdr()  { grep -i "^${1}:" "$TMPDIR_WORK/response_headers" 2>/dev/null | head -1 | sed 's/^[^:]*:\s*//' | tr -d '\r'; }
    _api()  { local m="$1" u="$2"; shift 2; local c; c=$(_curl -X "$m" "$@" "$u") || c="000"; [[ "${VERBOSE:-0}" == "1" ]] && echo "$PROG: [debug] $m $u → $c" >&2; echo "$c"; }
    blob_exists() { [[ "$(_api HEAD "${SCHEME}://${REGISTRY}/v2/${REPOSITORY}/blobs/$1")" == "200" ]]; }
    push_blob_file() {
        local file="$1" digest="$2" size="$3" label="${4:-blob}"
        if blob_exists "$digest"; then log "$label already present, skipping"; return 0; fi
        log "Uploading $label (${size} bytes)..."
        local code loc
        code=$(_api POST "${SCHEME}://${REGISTRY}/v2/${REPOSITORY}/blobs/uploads/")
        [[ "$code" == "202" ]] || die "Failed to initiate blob upload (HTTP $code)"
        loc=$(_hdr "Location")
        [[ -n "$loc" ]] || die "No Location header in upload response"
        [[ "$loc" == http* ]] || loc="${SCHEME}://${REGISTRY}${loc}"
        local sep="?"; [[ "$loc" == *"?"* ]] && sep="&"
        code=$(_curl -X PUT -H "Content-Type: application/octet-stream" -H "Content-Length: ${size}" --data-binary "@${file}" "${loc}${sep}digest=${digest}") || code="000"
        [[ "$code" == "201" ]] || die "Blob upload failed (HTTP $code): $(cat "$TMPDIR_WORK/response_body" 2>/dev/null)"
        log "$label uploaded"
    }
    push_blob_str() {
        blob_exists "$2" && return 0
        printf '%s' "$1" > "$TMPDIR_WORK/blob_str"
        push_blob_file "$TMPDIR_WORK/blob_str" "$2" "$3" "config blob"
    }
    push_manifest() {
        printf '%s' "$1" > "$TMPDIR_WORK/manifest.json"
        local code
        code=$(_curl -X PUT -H "Content-Type: application/vnd.oci.image.manifest.v1+json" --data-binary "@$TMPDIR_WORK/manifest.json" "${SCHEME}://${REGISTRY}/v2/${REPOSITORY}/manifests/$2") || code="000"
        [[ "$code" == "201" ]] || die "Manifest push failed (HTTP $code): $(cat "$TMPDIR_WORK/response_body" 2>/dev/null)"
    }
    push_plugin() {
        local zipfile="$1" plugin_name="$2"
        local filename; filename="$(basename "$zipfile")"
        local tag="${filename%.zip}"; tag="${tag//+/-}"
        log "=== ${filename} ==="
        local layer_digest="sha256:$(sha256_file "$zipfile")"
        local layer_size; layer_size=$(file_size "$zipfile")
        local cfg='{}' cfg_digest cfg_size
        cfg_digest="sha256:$(sha256_str "$cfg")"
        cfg_size=${#cfg}
        local manifest
        manifest="{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"artifactType\":\"application/vnd.oci.artifact.v1\",\"config\":{\"mediaType\":\"application/vnd.oci.empty.v1+json\",\"digest\":\"${cfg_digest}\",\"size\":${cfg_size}},\"layers\":[{\"mediaType\":\"application/zip\",\"digest\":\"${layer_digest}\",\"size\":${layer_size},\"annotations\":{\"org.opencontainers.image.title\":\"${filename}\"}}]}"
        push_blob_str "$cfg" "$cfg_digest" "$cfg_size"
        push_blob_file "$zipfile" "$layer_digest" "$layer_size" "$filename"
        push_manifest "$manifest" "$tag"
        echo "${SCHEME}://${REGISTRY}/v2/${REPOSITORY}/blobs/${layer_digest};${plugin_name}"
    }
    command -v curl &>/dev/null || die "curl is required"
    log "Scanning ${BUNDLE}/files/ for matching plugins..."
    urls=()
    while IFS= read -r -d '' zipfile; do
        name="$(basename "$zipfile")"
        matched=""
        for p in "${PLUGIN_PATTERNS[@]}"; do [[ "$name" == *"${p}"* ]] && matched="$p" && break; done
        [[ -n "$matched" ]] && urls+=("$(push_plugin "$zipfile" "$matched")")
    done < <(find "$BUNDLE/files" -maxdepth 1 -name "*.zip" -print0 | sort -z)
    [[ ${#urls[@]} -gt 0 ]] || die "No matching plugin zips found in ${BUNDLE}/files/"
    log "Done. Pushed ${#urls[@]} plugin(s)."
    GRAFANA_IMAGE="$(kubectl get deploy kube-prometheus-stack-grafana -n prometheus \
        -o jsonpath='{range .spec.template.spec.containers[?(@.name=="grafana")]}{.image}{end}' 2>/dev/null || true)"
    [[ -n "$GRAFANA_IMAGE" ]] || die "Could not determine Grafana image; is kube-prometheus-stack-grafana deployed in namespace prometheus?"
    {
      echo "grafana:"
      echo "  extraInitContainers:"
      echo "    - name: install-airgap-plugins"
      echo "      image: ${GRAFANA_IMAGE}"
      echo '      command: ["/bin/sh", "-c"]'
      echo "      args:"
      echo "        - |"
      echo "            set -e"
      echo "            PDIR=/var/lib/grafana/plugins"
      echo "            install() {"
      echo '              if [ -d "$PDIR/$1" ]; then echo "$1 already present, skipping"; return; fi'
      echo '              echo "Installing $1 from $2"'
      echo '              grafana cli --insecure --pluginUrl "$2" --pluginsDir "$PDIR" plugins install "$1"'
      echo "            }"
      for entry in "${urls[@]}"; do
        echo "            install ${entry##*;} ${entry%;*}"
      done
      echo "      volumeMounts:"
      echo "        - name: storage"
      echo "          mountPath: /var/lib/grafana"
    } > grafana_plugins.yaml
    
  4. Run the plugin upload script

    Note

    These steps assume that the bundle is present in the current directory. If it is elsewhere, update the --bundle argument to the correct path. The script also requires kubectl access to the cluster, which it uses to detect the deployed Grafana image for the init container.

    chmod +x push-plugins.sh && ./push-plugins.sh --bundle ./bundle grafana-plugins
    
  5. The script pushes each plugin to the local registry and generates a file called grafana_plugins.yaml. This file defines a Grafana init container that installs the plugins from the local registry into Grafana’s plugin directory (/var/lib/grafana/plugins) at startup, so Grafana loads them without contacting grafana.com. It is applied in a later step. If successful, the script’s output should look like the following:

    push-plugins.sh: Scanning ./bundle/files/ for matching plugins...
    push-plugins.sh: === grafana-lokiexplore-app-1.0.40.zip ===
    push-plugins.sh: Uploading config blob (2 bytes)...
    push-plugins.sh: config blob uploaded
    push-plugins.sh: Uploading grafana-lokiexplore-app-1.0.40.zip (10480908 bytes)...
    push-plugins.sh: grafana-lokiexplore-app-1.0.40.zip uploaded
    push-plugins.sh: === grafana-metricsdrilldown-app-1.0.34.zip ===
    push-plugins.sh: Uploading config blob (2 bytes)...
    push-plugins.sh: config blob uploaded
    push-plugins.sh: Uploading grafana-metricsdrilldown-app-1.0.34.zip (4146320 bytes)...
    push-plugins.sh: grafana-metricsdrilldown-app-1.0.34.zip uploaded
    push-plugins.sh: === yesoreyeram-infinity-datasource-3.7.4.zip ===
    push-plugins.sh: Uploading config blob (2 bytes)...
    push-plugins.sh: config blob uploaded
    push-plugins.sh: Uploading yesoreyeram-infinity-datasource-3.7.4.zip (81446703 bytes)...
    push-plugins.sh: yesoreyeram-infinity-datasource-3.7.4.zip uploaded
    push-plugins.sh: Done. Pushed 3 plugin(s).
    
  6. Create a new file with the following values, substituting the BCM password you set above and your UFM REST API credentials. Keep each Infinity datasource’s allowed host scoped to its own API path:

    cat << EOF > grafana_infinity.yaml
    grafana:
      grafana.ini:
        plugins:
          preinstall_disabled: true
          public_key_retrieval_disabled: true
        analytics:
          check_for_updates: false
          check_for_plugin_updates: false
      enabled: true
      extraSecretMounts:
        - name: registry-ca
          mountPath: /etc/ssl/certs/ca-cert-registry.crt
          secretName: registry-ca
          readOnly: true
          optional: false
          subPath: ca.crt
      additionalDataSources:
        - name: BCM API
          type: yesoreyeram-infinity-datasource
          access: proxy
          isDefault: false
          basicAuth: true
          basicAuthUser: apiuser
          jsonData:
            auth_method: 'basicAuth'
            allowedHosts:
              - https://master:8081/rest/v1/
            tlsSkipVerify: true
            timeoutInSeconds: 60
          secureJsonData:
            basicAuthPassword: <your-bcm-api-password>
        - name: UFM API
          type: yesoreyeram-infinity-datasource
          access: proxy
          isDefault: false
          basicAuth: true
          basicAuthUser: <your-ufm-api-user>
          jsonData:
            auth_method: 'basicAuth'
            allowedHosts:
              - https://master:11443/ufmRest/
            tlsSkipVerify: true
            timeoutInSeconds: 60
          secureJsonData:
            basicAuthPassword: <your-ufm-api-password>
        - name: Loki
          type: loki
          uid: loki
          url: http://loki-gateway.loki.svc.cluster.local:80
          access: proxy
          isDefault: false
    EOF
    
  7. Run the following command to apply the values:

    helm upgrade kube-prometheus-stack ./kube-prometheus-stack -n prometheus \
      --reuse-values -f grafana_infinity.yaml -f grafana_plugins.yaml
    
  8. To access the UFM REST API, we need to install a port-forwarding rule within BCM as well. To configure this, you need to gather the IP address of the UFM appliance and replace it in the following command sequence:

    cmsh -c "device; portforward create UFM_IP_ADDRESS 443 11443"
    

    This should be repeated on your secondary BCM head node, if you have one configured.

Note

The port-forwarding rules are not persisted on restarts of the BCM daemon process. If your BCM head node reboots or the daemon restarts for any reason, you will need to rerun this port-forwarding sequence.

Installation#

Note

Run these commands from the directory that contains the uploaded dashboard chart.

  1. Upload the dashboard chart (.tgz file) to the BCM head node.

  2. Enable dashboard folders in Grafana:

    helm upgrade -n prometheus kube-prometheus-stack ./kube-prometheus-stack \
      --set grafana.sidecar.dashboards.provider.foldersFromFilesStructure=true \
      --set grafana.sidecar.dashboards.enabled=true --reuse-values
    
  1. Extract the chart, keep only the dashboards for this platform, and install the repackaged chart. Update the input filename if needed:

    tar -xzvf nmc-grafana-dashboards-27.3.8.tgz -C /tmp/
    find /tmp/nmc-grafana-dashboards/dashboards -mindepth 1 -maxdepth 1 -type d ! -name 'b200' -exec rm -rf -- {} +
    tar -czvf /tmp/nmc-grafana-dashboards.tar.gz -C /tmp/ nmc-grafana-dashboards
    helm upgrade --install -n prometheus nmc-dashboards /tmp/nmc-grafana-dashboards.tar.gz --set dgx_system=b200
    
  1. Extract the chart, keep only the dashboards for this platform, and install the repackaged chart. Update the input filename if needed:

    tar -xzvf nmc-grafana-dashboards-27.3.8.tgz -C /tmp/
    find /tmp/nmc-grafana-dashboards/dashboards -mindepth 1 -maxdepth 1 -type d ! -name 'gb200' -exec rm -rf -- {} +
    tar -czvf /tmp/nmc-grafana-dashboards.tar.gz -C /tmp/ nmc-grafana-dashboards
    helm upgrade --install -n prometheus nmc-dashboards /tmp/nmc-grafana-dashboards.tar.gz --set dgx_system=gb200
    
  1. Extract the chart, keep only the dashboards for this platform, and install the repackaged chart. Update the input filename if needed:

    tar -xzvf nmc-grafana-dashboards-27.3.8.tgz -C /tmp/
    find /tmp/nmc-grafana-dashboards/dashboards -mindepth 1 -maxdepth 1 -type d ! -name 'b300' -exec rm -rf -- {} +
    tar -czvf /tmp/nmc-grafana-dashboards.tar.gz -C /tmp/ nmc-grafana-dashboards
    helm upgrade --install -n prometheus nmc-dashboards /tmp/nmc-grafana-dashboards.tar.gz --set dgx_system=b300
    
  1. Extract the chart, keep only the dashboards for this platform, and install the repackaged chart. Update the input filename if needed:

    tar -xzvf nmc-grafana-dashboards-27.3.8.tgz -C /tmp/
    find /tmp/nmc-grafana-dashboards/dashboards -mindepth 1 -maxdepth 1 -type d ! -name 'gb300' -exec rm -rf -- {} +
    tar -czvf /tmp/nmc-grafana-dashboards.tar.gz -C /tmp/ nmc-grafana-dashboards
    helm upgrade --install -n prometheus nmc-dashboards /tmp/nmc-grafana-dashboards.tar.gz --set dgx_system=gb300
    
  1. Extract the chart, keep only the dashboards for this platform, and install the repackaged chart. Update the input filename if needed:

    tar -xzvf nmc-grafana-dashboards-27.3.8.tgz -C /tmp/
    find /tmp/nmc-grafana-dashboards/dashboards -mindepth 1 -maxdepth 1 -type d ! -name 'vrnvl72' -exec rm -rf -- {} +
    tar -czvf /tmp/nmc-grafana-dashboards.tar.gz -C /tmp/ nmc-grafana-dashboards
    helm upgrade --install -n prometheus nmc-dashboards /tmp/nmc-grafana-dashboards.tar.gz --set dgx_system=vrnvl72
    

Accessing dashboards#

  1. In a browser, open: https://<headnode>/grafana

  2. Login using default credentials. Get Grafana ‘admin’ user password by running:

    kubectl get secret -n prometheus kube-prometheus-stack-grafana \
        -o jsonpath="{.data.admin-password}" | base64 -d ; echo
    
  3. Navigate to Dashboards in the Grafana UI.

    Deployed dashboards will be denoted with a number, for example:

    1. 01. SuperPOD Overall Dashboard

Customization and BMS integration#

The dashboards included in this Helm chart are provided as examples. They are fully functional out of the box but are intended to be customized to match the customer’s specific cluster environment.

One dashboard, BMS View of Cluster, is configured to use NVIDIA Cronus as the Building Management System (BMS) data source. It displays power, liquid cooling, environmental, and other facility-level metrics.

Most customers will need to:

  1. Integrate their own BMS with NVIDIA BCM.

  2. Update the dashboard’s data source and queries to reflect their own BMS setup.

These dashboards serve as a starting point for visualizing infrastructure telemetry. The customer is encouraged to modify or extend them based on the facility’s capabilities and telemetry available. In some dashboards we show what filtering by rack can look like, but you might need to customize the filters to fit your situation.

On OEM and other non-DGX systems, the dashboards need additional configuration before their panels show data. The steps are the same for air-gapped and connected installations. Refer to Adapt Dashboards to Non-DGX Systems.

Troubleshooting#

Error: Could not load plugin#

On rare occasions, some dashboards may fail to load properly and display an error:

BCM|UFM API plugin failed
Error: Could not load plugin

This fix for this is to scale the Grafana deployment down to 0 and back up again.

kubectl scale deployment kube-prometheus-stack-grafana -n prometheus --replicas=0
sleep 30
kubectl scale deployment kube-prometheus-stack-grafana -n prometheus --replicas=1

This will restart the Grafana deployment and the dashboards should load properly.