Skip to content

Commit 49fedfd

Browse files
[AXT-151] kai_scheduler: Emit KAI PodGroup lifecycle events (#25939)
* [AXT-151] kai_scheduler: Emit KAI PodGroup lifecycle events * [AXT-151] Match eviction details to transition * Address review comments
1 parent 8c5a7ec commit 49fedfd

16 files changed

Lines changed: 1110 additions & 6 deletions

File tree

‎kai_scheduler/README.md‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This check monitors [KAI Scheduler][1] through the Datadog Agent.
66

7-
KAI Scheduler is a Kubernetes scheduler for AI and GPU workloads. It schedules pods into hierarchical queues with quotas, fair share, priorities, and preemption. This integration collects metrics from the KAI scheduler and queue-controller to help you monitor queue quotas and allocation, fair share, scheduling latency, and preemption.
7+
KAI Scheduler is a Kubernetes scheduler for AI and GPU workloads. It schedules pods into hierarchical queues with quotas, fair share, priorities, and preemption. This integration collects metrics from the KAI scheduler and queue-controller to help you monitor queue quotas and allocation, fair share, scheduling latency, and preemption, and sends events for PodGroup life cycle transitions.
88

99
## Setup
1010

@@ -45,6 +45,7 @@ clusterAgent:
4545
```
4646

4747
If you run several scheduling shards, add one `scheduler_endpoint` instance per shard service.
48+
If you enable `collect_podgroup_events` on more than one instance, each instance polls every PodGroup in the cluster and reports the same [events](#events). Set `collect_podgroup_events: false` on all instances except one.
4849

4950
#### Binder metrics (optional)
5051

@@ -68,15 +69,37 @@ See [metadata.csv][7] for a list of metrics provided by this integration.
6869

6970
Queue metrics from the scheduler and the queue-controller share the `kai_queue` tag, which holds the queue's `metadata.name`, so that quota, allocation, fair share, and usage can be compared per queue. Queues with a `spec.displayName` are also tagged with `kai_queue_display_name`. Tags without a value, such as labels that KAI leaves empty, are not submitted. Memory metrics are reported in bytes, CPU metrics in cores, and GPU metrics in devices.
7071

71-
KAI counts evicted pods per pod group and never deletes those series. To keep the number of contexts bounded, `kai.evicted_pods.count` reports the pods evicted since the previous check run, summed by `kube_namespace`, `kai_nodepool`, and `action`. Evictions that happened before the first successful check run are not reported. The check keeps the previous value of up to 10,000 pod group series. Beyond that, it reports the increase of the summed counters instead, which undercounts evictions in a group whose pod group series KAI removed during that check run. To see which workloads were evicted and by which preemptor, collect the Kubernetes `Evict` events that KAI records on each evicted PodGroup.
72+
KAI counts evicted pods per pod group and never deletes those series. To keep the number of contexts bounded, `kai.evicted_pods.count` reports the pods evicted since the previous check run, summed by `kube_namespace`, `kai_nodepool`, and `action`. Evictions that happened before the first successful check run are not reported. The check keeps the previous value of up to 10,000 pod group series. Beyond that, it reports the increase of the summed counters instead, which undercounts evictions in a group whose pod group series KAI removed during that check run. To see which workloads were evicted and by which preemptor, use the `kai.podgroup.evicted` [events](#events).
7273

7374
The `kai.scheduling_cycle.latency.milliseconds`, `kai.session.*.latency.milliseconds`, `kai.action.latency.milliseconds`, and `kai.plugin.latency.milliseconds` gauges hold the duration of the most recent scheduling cycle only. The scheduler runs a cycle every second by default, so each check run samples one cycle and short spikes between check runs are not visible. A scheduler that is consistently slow is still visible in every sample.
7475

7576
Histograms, such as `kai.task.scheduling_latency.milliseconds` and `kai.task.bind_latency.milliseconds`, are submitted as [distributions][12] so that percentiles can be queried, along with their `.count` and `.sum` counters. Set `histogram_buckets_as_distributions: false` to submit the buckets as `.bucket` counts instead.
7677

7778
### Events
7879

79-
The KAI Scheduler integration does not include any events.
80+
By default, the KAI Scheduler integration polls the KAI PodGroup custom resources and sends Datadog events for life cycle transitions:
81+
82+
- **Created** (`kai.podgroup.created`): A PodGroup appears after the check has initialized its state.
83+
- **Pending** (`kai.podgroup.pending`): The scheduler cannot schedule the PodGroup on a node pool and adds an `UnschedulableOnNodePool` scheduling condition for it.
84+
- **Admitted** (`kai.podgroup.admitted`): The scheduler allocates pods to a PodGroup that has none allocated, such as a new PodGroup, an evicted one, or a Job that starts its next pod, and updates its `kai.scheduler/last-start-timestamp` annotation.
85+
- **Evicted** (`kai.podgroup.evicted`): The scheduler evicts some or all of the PodGroup's pods to preempt, reclaim, or consolidate, or because they are background pods whose capacity it gives to other pods, and updates its `kai.scheduler/last-eviction-timestamp` annotation. KAI sets this annotation starting with version `0.17.0`. Stale gang evictions do not update it, so they are only counted in `kai.evicted_pods.count`.
86+
87+
KAI does not record quota reservation, pod readiness, or completion on the PodGroup, so there are no equivalents of the Kueue `quota_reserved`, `running`, and `finished` Workload events.
88+
89+
Events are tagged with `kube_namespace`, `kai_workload` (the PodGroup name), `kai_workload_uid`, `kai_queue`, `kai_nodepool`, `kai_transition`, and `kai_workload_priority_class` when the PodGroup sets a priority class. The `kai_queue` tag matches the tag the Agent adds to the metrics of the PodGroup's pods, and so does `kai_workload` when the Agent collects tags with `orchestrator` cardinality. Pending events take the node pool from the scheduling condition, and other events from the `kai.scheduler/node-pool` PodGroup label, so other events are tagged `kai_nodepool:default` when KAI is configured with another `global.nodePoolLabelKey`. Instance tags, including the tags you set in `tags` and the tags Autodiscovery adds from the KAI pods, apply to metrics only, not to events.
90+
91+
Pending events include the scheduling error and a `kai_pending_reason` tag for each reason KAI reports, such as `OverLimit`. Admitted events include the queued wait time when the check can determine it, counted from the PodGroup creation or from the latest eviction. After an eviction of only some of the pods, it can also include time the PodGroup spent running. Eviction events include the message and the eviction action, and are tagged with `kai_eviction_action` (such as `preempt` or `reclaim`) and `kai_evicted_by` (the preemptor PodGroup), when available from the `Evict` Kubernetes event that KAI records on the PodGroup.
92+
93+
Each event also includes `kai_workload_event_index`, an integer that goes up by one for each event the check emits for a PodGroup. KAI timestamps transitions to the second, so several events can have the same timestamp. To put events in order, sort by timestamp first, then by `kai_workload_event_index` as an integer.
94+
95+
The Agent stores the index in its persistent cache. After a process restart, the sequence picks up where it left off for PodGroups that still exist in the API. The sequence restarts at 0 in two cases:
96+
97+
- The check configuration changes, which clears the cache.
98+
- A new pod starts. The run directory doesn't survive container recreation.
99+
100+
The first collection run records the current state of each PodGroup. It doesn't emit events for transitions that happened before that run.
101+
102+
Collecting events requires `list` role-based access control (RBAC) permission on `podgroups.scheduling.run.ai`, and `list` on `events` for the eviction details. Every instance with `collect_podgroup_events` enabled polls all the PodGroups of the cluster, so when you configure several instances, such as one per scheduling shard, set `collect_podgroup_events: false` on all but one of them. Set it to `false` on every instance to disable events.
80103

81104
### Service Checks
82105

‎kai_scheduler/assets/configuration/spec.yaml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,35 @@ files:
3939
display_default: null
4040
example: http://binder.kai-scheduler.svc:8080/metrics
4141
type: string
42+
- name: collect_podgroup_events
43+
fleet_configurable: true
44+
description: |
45+
Submit events for KAI PodGroup life cycle transitions by polling PodGroup custom resources from the Kubernetes
46+
API.
47+
48+
This requires `list` role-based access control (RBAC) permission on `podgroups.scheduling.run.ai`, and `list`
49+
on `events` to add the eviction reason and preemptor to eviction events.
50+
51+
Every instance with this option enabled polls all the PodGroups of the cluster. When you configure several
52+
instances, such as one per scheduling shard, set it to `false` on all but one of them to avoid duplicate
53+
events.
54+
value:
55+
type: boolean
56+
example: true
57+
default: true
58+
- name: kube_config_dict
59+
description: |
60+
Optional kubeconfig dictionary used to configure the Kubernetes API client.
61+
62+
If unset, the check uses in-cluster Kubernetes configuration.
63+
value:
64+
type: object
65+
example:
66+
apiVersion: v1
67+
kind: Config
68+
clusters: []
69+
contexts: []
70+
users: []
4271
- name: cluster_check
4372
description: |
4473
Set to true when configuring this integration as a Cluster Agent cluster check.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add KAI PodGroup life cycle events: `kai.podgroup.created`, `kai.podgroup.pending`, `kai.podgroup.admitted`, and `kai.podgroup.evicted`.

‎kai_scheduler/datadog_checks/kai_scheduler/check.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
QUEUE_CONTROLLER_METRICS,
2525
SCHEDULER_METRICS,
2626
)
27+
from .podgroup_events import PodGroupEventCollector
2728

2829
DEFAULT_RENAME_LABELS = {
2930
'queue_metadata_name': 'kai_queue',
@@ -67,6 +68,20 @@ def __init__(self, name, init_config, instances):
6768
super().__init__(name, init_config, instances)
6869
self.check_initializations.appendleft(self.parse_config)
6970
self.check_initializations.append(self.configure_scheduler_transformers)
71+
self.check_initializations.append(self.configure_podgroup_events)
72+
self.podgroup_events: PodGroupEventCollector | None = None
73+
74+
def check(self, instance):
75+
try:
76+
super().check(instance)
77+
finally:
78+
# Events come from the Kubernetes API, so a failing metrics endpoint must not stop them, and a failure
79+
# to collect events must not fail the metrics run.
80+
if self.podgroup_events:
81+
try:
82+
self.podgroup_events.collect()
83+
except Exception as e:
84+
self.log.warning('Cannot collect KAI PodGroup events: %s', e, exc_info=True)
7085

7186
def get_default_config(self):
7287
return {
@@ -121,6 +136,10 @@ def fair_share_memory_transformer(metric, sample_data, runtime_data):
121136
scraper.metric_transformer.add_custom_transformer(FAIR_SHARE_MEMORY_METRIC, fair_share_memory_transformer)
122137
scraper.metric_transformer.add_custom_transformer(EVICTED_PODS_METRIC, EvictedPodsTransformer(self, scraper))
123138

139+
def configure_podgroup_events(self):
140+
if self.config.collect_podgroup_events:
141+
self.podgroup_events = PodGroupEventCollector(self, self.config.kube_config_dict)
142+
124143

125144
class KaiOpenMetricsScraper(OpenMetricsScraper):
126145
def __init__(self, check, config):

‎kai_scheduler/datadog_checks/kai_scheduler/config_models/defaults.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def instance_collect_histogram_buckets():
4444
return True
4545

4646

47+
def instance_collect_podgroup_events():
48+
return True
49+
50+
4751
def instance_disable_generic_tags():
4852
return False
4953

‎kai_scheduler/datadog_checks/kai_scheduler/config_models/instance.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class InstanceConfig(BaseModel):
101101
cluster_check: Optional[bool] = None
102102
collect_counters_with_distributions: Optional[bool] = None
103103
collect_histogram_buckets: Optional[bool] = None
104+
collect_podgroup_events: Optional[bool] = None
104105
connect_timeout: Optional[float] = None
105106
disable_generic_tags: Optional[bool] = None
106107
empty_default_hostname: Optional[bool] = None
@@ -125,6 +126,7 @@ class InstanceConfig(BaseModel):
125126
kerberos_hostname: Optional[str] = None
126127
kerberos_keytab: Optional[str] = None
127128
kerberos_principal: Optional[str] = None
129+
kube_config_dict: Optional[MappingProxyType[str, Any]] = None
128130
log_requests: Optional[bool] = None
129131
metric_patterns: Optional[MetricPatterns] = None
130132
metrics: Optional[tuple[Union[str, MappingProxyType[str, Union[str, Metrics]]], ...]] = None

‎kai_scheduler/datadog_checks/kai_scheduler/data/conf.yaml.example‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ instances:
6868
#
6969
# binder_endpoint: http://binder.kai-scheduler.svc:8080/metrics
7070

71+
## @param collect_podgroup_events - boolean - optional - default: true
72+
## Submit events for KAI PodGroup life cycle transitions by polling PodGroup custom resources from the Kubernetes
73+
## API.
74+
##
75+
## This requires `list` role-based access control (RBAC) permission on `podgroups.scheduling.run.ai`, and `list`
76+
## on `events` to add the eviction reason and preemptor to eviction events.
77+
##
78+
## Every instance with this option enabled polls all the PodGroups of the cluster. When you configure several
79+
## instances, such as one per scheduling shard, set it to `false` on all but one of them to avoid duplicate
80+
## events.
81+
#
82+
# collect_podgroup_events: true
83+
84+
## @param kube_config_dict - mapping - optional
85+
## Optional kubeconfig dictionary used to configure the Kubernetes API client.
86+
##
87+
## If unset, the check uses in-cluster Kubernetes configuration.
88+
#
89+
# kube_config_dict:
90+
# apiVersion: v1
91+
# kind: Config
92+
# clusters: []
93+
# contexts: []
94+
# users: []
95+
7196
## @param cluster_check - boolean - optional - default: true
7297
## Set to true when configuring this integration as a Cluster Agent cluster check.
7398
#
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# (C) Datadog, Inc. 2026-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from collections.abc import Mapping
5+
from typing import Any
6+
7+
from kubernetes import client, config
8+
9+
# The Kubernetes client waits forever by default, which would block the check run on a stalled API server.
10+
REQUEST_TIMEOUT = 10
11+
12+
13+
class KubernetesAPIClient:
14+
def __init__(self, kube_config_dict: Mapping[str, Any] | None = None):
15+
if kube_config_dict:
16+
self.api_client = config.new_client_from_config_dict(to_mutable_config(kube_config_dict))
17+
else:
18+
config.load_incluster_config()
19+
self.api_client = client.ApiClient()
20+
self.custom_objects = client.CustomObjectsApi(self.api_client)
21+
self.core = client.CoreV1Api(self.api_client)
22+
23+
def list_podgroups(self) -> list[dict]:
24+
return self.custom_objects.list_cluster_custom_object(
25+
group='scheduling.run.ai', version='v2alpha2', plural='podgroups', _request_timeout=REQUEST_TIMEOUT
26+
)['items']
27+
28+
def list_evict_events(self, namespace: str) -> list[dict]:
29+
"""Return the `Evict` Kubernetes events that the KAI scheduler recorded on the PodGroups of a namespace."""
30+
events = self.core.list_namespaced_event(
31+
namespace, field_selector='involvedObject.kind=PodGroup,reason=Evict', _request_timeout=REQUEST_TIMEOUT
32+
)
33+
return self.api_client.sanitize_for_serialization(events)['items']
34+
35+
36+
def to_mutable_config(value: Any) -> Any:
37+
# The configuration models freeze mappings and lists, which the kubernetes client cannot load.
38+
if isinstance(value, Mapping):
39+
return {key: to_mutable_config(item) for key, item in value.items()}
40+
if isinstance(value, tuple):
41+
return [to_mutable_config(item) for item in value]
42+
return value

0 commit comments

Comments
 (0)