Skip to content

Commit ca5d310

Browse files
author
The kauldron Authors
committed
Extend KD cli commands.
PiperOrigin-RevId: 885246572
1 parent 322a66d commit ca5d310

6 files changed

Lines changed: 262 additions & 3 deletions

File tree

kauldron/cli/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,15 @@ kd.cli.data.ElementSpec(cfg).execute()
3535

3636
* `data`
3737
- `element_spec`: Display the element spec of the training data pipeline.
38+
- `batch`: Display the stats (shapes, dtype, min, max, mean) of an actual fetched batch.
39+
40+
* `run`
41+
- `eval_shape`: Run a train step through `jax.eval_shape` (shapes only, no compute).
42+
- `train`: Run `trainer.train()` directly to test the training loop.
43+
- `eval`: Run `trainer.eval()` and print the metrics output.
44+
45+
* `inspect`
46+
- `model_overview`: Display the model overview (parameters, inputs, shapes) similar to the one used in the colab environments.
47+
48+
* `multi`
49+
- `execute`: Run multiple commands sequentially. For example, use `--cmds="data batch, run eval, inspect model_overview"` for layered execution.

kauldron/cli/data.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import dataclasses
2020
from typing import Union
2121

22+
from kauldron import inspect as kd_inspect
2223
from kauldron import kontext
2324
from kauldron.cli import cmd_utils as cu
2425
import tensorflow_datasets as tfds
@@ -48,8 +49,39 @@ def __call__(self):
4849
print(f" {k}: {v.dtype}{list(v.shape)}")
4950

5051

52+
@dataclasses.dataclass(frozen=True, kw_only=True)
53+
class Batch(cu.SubCommand):
54+
"""Display the batch statistics (shapes, dtype, min, max, mean)."""
55+
56+
ds_path: str = "train_ds"
57+
58+
def __call__(self):
59+
self.print_config_origin()
60+
trainer = self.trainer # trigger config resolution
61+
ds = kontext.get_by_path(trainer, self.ds_path)
62+
63+
with cu.timed("Getting real batch"):
64+
batch = next(iter(ds))
65+
66+
print("")
67+
print(f"Dataset: {self.ds_path}")
68+
# NOTE: kd_inspect is a konfig.import
69+
stats_df = kd_inspect.get_batch_stats(batch)
70+
if hasattr(stats_df, "to_markdown"):
71+
print(stats_df.to_markdown())
72+
else:
73+
print(stats_df.to_string(max_rows=None, max_cols=None))
74+
75+
_SUBCOMMANDS = {
76+
"element_spec": ElementSpec,
77+
"batch": Batch,
78+
}
79+
80+
5181
@dataclasses.dataclass(frozen=True, kw_only=True)
5282
class Data(cu.CommandGroup):
5383
"""Data commands."""
5484

55-
sub_command: Union[ElementSpec] # Union required for simple_parsing
85+
sub_command: Union[ElementSpec, Batch] = dataclasses.field(
86+
metadata={"subparsers": _SUBCOMMANDS}
87+
)

kauldron/cli/inspect_cli.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2026 The kauldron Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Inspect-related CLI commands."""
16+
17+
from __future__ import annotations
18+
19+
import dataclasses
20+
from typing import Union
21+
22+
from kauldron import inspect as kd_inspect
23+
from kauldron.cli import cmd_utils as cu
24+
25+
@dataclasses.dataclass(frozen=True, kw_only=True)
26+
class ModelOverview(cu.SubCommand):
27+
"""Display the model overview (parameters, inputs, shapes, etc.)."""
28+
29+
def __call__(self):
30+
self.print_config_origin()
31+
trainer = self.trainer # trigger config resolution
32+
33+
with cu.timed("Getting model overview"):
34+
df = kd_inspect.get_colab_model_overview(
35+
model=trainer.model,
36+
train_ds=trainer.train_ds,
37+
ds_sharding=trainer.sharding.batch,
38+
model_config=trainer.raw_cfg.model if trainer.raw_cfg else None,
39+
rngs=trainer.rng_streams.init_rngs(),
40+
)
41+
42+
print("\n======== Model Overview ========")
43+
if hasattr(df, "data"): # Extract raw DataFrame from StyledDataFrame
44+
raw_df = df.data
45+
else:
46+
raw_df = df
47+
48+
if hasattr(raw_df, "to_markdown"):
49+
print(raw_df.to_markdown(index=False))
50+
else:
51+
print(raw_df.to_string(index=False))
52+
53+
54+
_SUBCOMMANDS = {
55+
"model_overview": ModelOverview,
56+
}
57+
58+
@dataclasses.dataclass(frozen=True, kw_only=True)
59+
class Inspect(cu.CommandGroup):
60+
"""Inspect commands."""
61+
62+
sub_command: Union[ModelOverview] = dataclasses.field(
63+
metadata={"subparsers": _SUBCOMMANDS}
64+
)

kauldron/cli/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from kauldron.cli import cmd_utils as cu
3232
from kauldron.cli import config
3333
from kauldron.cli import data
34+
from kauldron.cli import inspect_cli
35+
from kauldron.cli import multi
3436
from kauldron.cli import patch_config
3537
from kauldron.cli import run
3638
import simple_parsing
@@ -48,7 +50,8 @@
4850
@dataclasses.dataclass(frozen=True, kw_only=True)
4951
class Args:
5052

51-
command: config.Config | data.Data | run.Run
53+
command: config.Config | data.Data | run.Run | inspect_cli.Inspect | multi.Multi
54+
5255

5356
patch: patch_config.PatchConfig = dataclasses.field(
5457
default_factory=patch_config.PatchConfig,

kauldron/cli/multi.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright 2026 The kauldron Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Multi-command execution CLI."""
16+
17+
from __future__ import annotations
18+
19+
import dataclasses
20+
from typing import Union
21+
22+
from kauldron.cli import cmd_utils as cu
23+
from kauldron.cli import data
24+
from kauldron.cli import inspect_cli
25+
from kauldron.cli import run
26+
27+
_COMMANDS = {
28+
"data": {
29+
"element_spec": data.ElementSpec,
30+
"batch": data.Batch,
31+
},
32+
"run": {
33+
"eval_shape": run.EvalShape,
34+
"train": run.Train,
35+
"eval": run.Eval,
36+
},
37+
"inspect": {
38+
"model_overview": inspect_cli.ModelOverview,
39+
}
40+
}
41+
42+
@dataclasses.dataclass(frozen=True, kw_only=True)
43+
class Execute(cu.SubCommand):
44+
"""Execute multiple commands sequentially."""
45+
46+
cmds: str = ""
47+
48+
def __call__(self):
49+
self.print_config_origin()
50+
trainer = None
51+
52+
cmds_list = self.cmds.split(",")
53+
if not cmds_list or (len(cmds_list) == 1 and not cmds_list[0].strip()):
54+
print(
55+
'No subcommands provided to multi. Use --multi.execute.cmds="data'
56+
' batch, run train"'
57+
)
58+
return
59+
60+
for cmd_str in cmds_list:
61+
cmd_str = cmd_str.strip()
62+
if not cmd_str:
63+
continue
64+
parts = cmd_str.split()
65+
if len(parts) != 2:
66+
print(f"Invalid command format: {cmd_str}. Expected 'group subcommand'")
67+
continue
68+
group_name, sub_name = parts
69+
70+
if group_name not in _COMMANDS or sub_name not in _COMMANDS[group_name]:
71+
print(
72+
f"Unknown command: '{cmd_str}'. Available commands: "
73+
+ ", ".join(
74+
[f"{g} {s}" for g, subs in _COMMANDS.items() for s in subs]
75+
)
76+
)
77+
continue
78+
79+
cmd_cls = _COMMANDS[group_name][sub_name]
80+
81+
print(f"\\n{'='*40}\\nExecuting: {cmd_str}\\n{'='*40}")
82+
83+
cmd = cmd_cls(cfg=self.cfg, origin=None) # pytype: disable=wrong-keyword-args
84+
85+
# Share resolved trainer to avoid redundant recompilations/resolutions
86+
if trainer is None:
87+
trainer = cmd.trainer
88+
else:
89+
cmd.__dict__["trainer"] = trainer
90+
91+
cmd()
92+
93+
@dataclasses.dataclass(frozen=True, kw_only=True)
94+
class Multi(cu.CommandGroup):
95+
"""Multi commands."""
96+
97+
sub_command: Union[Execute]

kauldron/cli/run.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import functools
2121
from typing import Union
2222

23+
from etils import epy
2324
import jax
25+
import kauldron as kd
2426
from kauldron.cli import cmd_utils as cu
2527
from kauldron.data import utils as data_utils
2628
import tensorflow_datasets as tfds
@@ -71,17 +73,66 @@ def __call__(self) -> None:
7173
# TODO(klausg): could try to run the metrics computation too.
7274

7375

76+
@dataclasses.dataclass(frozen=True, kw_only=True)
77+
class Train(cu.SubCommand):
78+
"""Run trainer.train()."""
79+
80+
def __call__(self) -> None:
81+
self.print_config_origin()
82+
83+
# Ensure exactly one training step is performed to match kd_test.ipynb
84+
self.cfg.stop_after_steps = 1
85+
86+
if hasattr(self.cfg, 'evals'):
87+
kd.kontext.set_by_path(self.cfg, 'evals.**.num_batches', 1)
88+
89+
trainer = self.trainer # trigger config resolution
90+
91+
with cu.timed('trainer.train()'):
92+
train_state, aux = trainer.train()
93+
del train_state, aux
94+
# We don't print the output of train() as it might be very verbose
95+
# but we can print that it succeeded.
96+
print('Successfully completed trainer.train()')
97+
98+
@dataclasses.dataclass(frozen=True, kw_only=True)
99+
class Eval(cu.SubCommand):
100+
"""Run trainer.eval()."""
101+
102+
def __call__(self) -> None:
103+
self.print_config_origin()
104+
105+
if hasattr(self.cfg, 'evals'):
106+
kd.kontext.set_by_path(self.cfg, 'evals.**.num_batches', 1)
107+
108+
trainer = self.trainer # trigger config resolution
109+
110+
with cu.timed('trainer.init_state()'):
111+
state = trainer.init_state()
112+
113+
eval_metrics = {}
114+
for name, evaluator in trainer.evals.items():
115+
with cu.timed(f'evaluator.evaluate({name})'):
116+
eval_metrics[name] = evaluator.evaluate(state=state, step=0)
117+
118+
if eval_metrics:
119+
print('Evaluator metrics:')
120+
epy.pprint(eval_metrics)
121+
122+
74123
_SUBCOMMANDS = {
75124
# Manually name the subcommand 'eval_shape' beacause simple-parsing
76125
# would turn this into 'evalshape' instead.
77126
'eval_shape': EvalShape,
127+
'train': Train,
128+
'eval': Eval,
78129
}
79130

80131

81132
@dataclasses.dataclass(frozen=True, kw_only=True)
82133
class Run(cu.CommandGroup):
83134
"""Run commands for local training validation."""
84135

85-
sub_command: Union[EvalShape] = dataclasses.field(
136+
sub_command: Union[EvalShape, Train, Eval] = dataclasses.field(
86137
metadata={'subparsers': _SUBCOMMANDS}
87138
)

0 commit comments

Comments
 (0)