raylings 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raylings/__init__.py +3 -0
- raylings/cli.py +331 -0
- raylings/daemon.py +112 -0
- raylings/exercises/01_basics/basics01.py +48 -0
- raylings/exercises/01_basics/basics02.py +49 -0
- raylings/exercises/01_basics/basics03.py +86 -0
- raylings/exercises/01_basics/basics04.py +84 -0
- raylings/exercises/01_basics/basics05.py +80 -0
- raylings/exercises/01_basics/basics06.py +63 -0
- raylings/exercises/02_actors/actors01.py +75 -0
- raylings/exercises/02_actors/actors02.py +74 -0
- raylings/exercises/02_actors/actors03.py +96 -0
- raylings/exercises/02_actors/actors04.py +74 -0
- raylings/exercises/02_actors/actors05.py +75 -0
- raylings/exercises/02_actors/actors06.py +67 -0
- raylings/exercises/02_actors/actors07.py +63 -0
- raylings/exercises/03_object_store/object_store01.py +68 -0
- raylings/exercises/03_object_store/object_store02.py +66 -0
- raylings/exercises/03_object_store/object_store03.py +73 -0
- raylings/exercises/03_object_store/object_store04.py +66 -0
- raylings/exercises/03_object_store/object_store05.py +77 -0
- raylings/exercises/03_object_store/object_store06.py +80 -0
- raylings/exercises/04_scheduling_resources/scheduling01.py +67 -0
- raylings/exercises/04_scheduling_resources/scheduling02.py +59 -0
- raylings/exercises/04_scheduling_resources/scheduling03.py +74 -0
- raylings/exercises/04_scheduling_resources/scheduling04.py +67 -0
- raylings/exercises/04_scheduling_resources/scheduling05.py +54 -0
- raylings/exercises/04_scheduling_resources/scheduling06.py +58 -0
- raylings/exercises/05_fault_tolerance/fault01.py +70 -0
- raylings/exercises/05_fault_tolerance/fault02.py +82 -0
- raylings/exercises/05_fault_tolerance/fault03.py +61 -0
- raylings/exercises/05_fault_tolerance/fault04.py +114 -0
- raylings/exercises/06_cluster_architecture/cluster01.py +63 -0
- raylings/exercises/06_cluster_architecture/cluster02.py +73 -0
- raylings/exercises/06_cluster_architecture/cluster03.py +64 -0
- raylings/exercises/06_cluster_architecture/cluster04.py +67 -0
- raylings/exercises/07_patterns_and_antipatterns/antipattern01.py +70 -0
- raylings/exercises/07_patterns_and_antipatterns/antipattern02.py +64 -0
- raylings/exercises/07_patterns_and_antipatterns/antipattern03.py +65 -0
- raylings/exercises/07_patterns_and_antipatterns/antipattern04.py +87 -0
- raylings/exercises/08_ray_data/data01.py +47 -0
- raylings/exercises/08_ray_data/data02.py +63 -0
- raylings/exercises/08_ray_data/data03.py +79 -0
- raylings/exercises/08_ray_data/data04.py +51 -0
- raylings/exercises/08_ray_data/data05.py +54 -0
- raylings/exercises/09_ml_from_scratch/ml_scratch01.py +88 -0
- raylings/exercises/09_ml_from_scratch/ml_scratch02.py +79 -0
- raylings/exercises/09_ml_from_scratch/ml_scratch03.py +87 -0
- raylings/exercises/09_ml_from_scratch/ml_scratch04.py +85 -0
- raylings/exercises/10_ray_train_and_tune/train01.py +51 -0
- raylings/exercises/10_ray_train_and_tune/train02.py +49 -0
- raylings/exercises/10_ray_train_and_tune/train03.py +47 -0
- raylings/exercises/10_ray_train_and_tune/train04.py +55 -0
- raylings/exercises/11_ray_tune/tune01.py +59 -0
- raylings/exercises/11_ray_tune/tune02.py +51 -0
- raylings/exercises/11_ray_tune/tune03.py +52 -0
- raylings/exercises/12_ray_serve/serve01.py +51 -0
- raylings/exercises/12_ray_serve/serve02.py +50 -0
- raylings/exercises/12_ray_serve/serve03.py +69 -0
- raylings/exercises/12_ray_serve/serve04.py +53 -0
- raylings/exercises/12_ray_serve/serve05.py +56 -0
- raylings/exercises/13_observability_and_debugging/perf01.py +63 -0
- raylings/exercises/13_observability_and_debugging/perf02.py +47 -0
- raylings/exercises/13_observability_and_debugging/perf03.py +55 -0
- raylings/exercises/14_kuberay/kuberay01.py +59 -0
- raylings/exercises/14_kuberay/kuberay02.py +45 -0
- raylings/exercises/14_kuberay/kuberay03.py +51 -0
- raylings/exercises/14_kuberay/kuberay04.py +69 -0
- raylings/exercises/14_kuberay/kuberay05.py +55 -0
- raylings/manifest.py +849 -0
- raylings/models.py +62 -0
- raylings/runner.py +180 -0
- raylings/solutions/01_basics/basics01.py +26 -0
- raylings/solutions/01_basics/basics02.py +28 -0
- raylings/solutions/01_basics/basics03.py +56 -0
- raylings/solutions/01_basics/basics04.py +51 -0
- raylings/solutions/01_basics/basics05.py +55 -0
- raylings/solutions/01_basics/basics06.py +30 -0
- raylings/solutions/02_actors/actors01.py +41 -0
- raylings/solutions/02_actors/actors02.py +56 -0
- raylings/solutions/02_actors/actors03.py +57 -0
- raylings/solutions/02_actors/actors04.py +45 -0
- raylings/solutions/02_actors/actors05.py +50 -0
- raylings/solutions/02_actors/actors06.py +43 -0
- raylings/solutions/02_actors/actors07.py +32 -0
- raylings/solutions/03_object_store/object_store01.py +40 -0
- raylings/solutions/03_object_store/object_store02.py +35 -0
- raylings/solutions/03_object_store/object_store03.py +46 -0
- raylings/solutions/03_object_store/object_store04.py +40 -0
- raylings/solutions/03_object_store/object_store05.py +40 -0
- raylings/solutions/03_object_store/object_store06.py +54 -0
- raylings/solutions/04_scheduling_resources/scheduling01.py +36 -0
- raylings/solutions/04_scheduling_resources/scheduling02.py +33 -0
- raylings/solutions/04_scheduling_resources/scheduling03.py +38 -0
- raylings/solutions/04_scheduling_resources/scheduling04.py +45 -0
- raylings/solutions/04_scheduling_resources/scheduling05.py +34 -0
- raylings/solutions/04_scheduling_resources/scheduling06.py +32 -0
- raylings/solutions/05_fault_tolerance/fault01.py +45 -0
- raylings/solutions/05_fault_tolerance/fault02.py +50 -0
- raylings/solutions/05_fault_tolerance/fault03.py +34 -0
- raylings/solutions/05_fault_tolerance/fault04.py +73 -0
- raylings/solutions/06_cluster_architecture/cluster01.py +37 -0
- raylings/solutions/06_cluster_architecture/cluster02.py +37 -0
- raylings/solutions/06_cluster_architecture/cluster03.py +38 -0
- raylings/solutions/06_cluster_architecture/cluster04.py +40 -0
- raylings/solutions/07_patterns_and_antipatterns/antipattern01.py +33 -0
- raylings/solutions/07_patterns_and_antipatterns/antipattern02.py +33 -0
- raylings/solutions/07_patterns_and_antipatterns/antipattern03.py +34 -0
- raylings/solutions/07_patterns_and_antipatterns/antipattern04.py +47 -0
- raylings/solutions/08_ray_data/data01.py +28 -0
- raylings/solutions/08_ray_data/data02.py +32 -0
- raylings/solutions/08_ray_data/data03.py +42 -0
- raylings/solutions/08_ray_data/data04.py +27 -0
- raylings/solutions/08_ray_data/data05.py +29 -0
- raylings/solutions/09_ml_from_scratch/ml_scratch01.py +49 -0
- raylings/solutions/09_ml_from_scratch/ml_scratch02.py +42 -0
- raylings/solutions/09_ml_from_scratch/ml_scratch03.py +90 -0
- raylings/solutions/09_ml_from_scratch/ml_scratch04.py +60 -0
- raylings/solutions/10_ray_train_and_tune/train01.py +56 -0
- raylings/solutions/10_ray_train_and_tune/train02.py +49 -0
- raylings/solutions/10_ray_train_and_tune/train03.py +59 -0
- raylings/solutions/10_ray_train_and_tune/train04.py +63 -0
- raylings/solutions/11_ray_tune/tune01.py +40 -0
- raylings/solutions/11_ray_tune/tune02.py +42 -0
- raylings/solutions/11_ray_tune/tune03.py +50 -0
- raylings/solutions/12_ray_serve/serve01.py +34 -0
- raylings/solutions/12_ray_serve/serve02.py +38 -0
- raylings/solutions/12_ray_serve/serve03.py +53 -0
- raylings/solutions/12_ray_serve/serve04.py +40 -0
- raylings/solutions/12_ray_serve/serve05.py +43 -0
- raylings/solutions/13_observability_and_debugging/perf01.py +50 -0
- raylings/solutions/13_observability_and_debugging/perf02.py +39 -0
- raylings/solutions/13_observability_and_debugging/perf03.py +44 -0
- raylings/solutions/14_kuberay/kuberay01.py +89 -0
- raylings/solutions/14_kuberay/kuberay02.py +64 -0
- raylings/solutions/14_kuberay/kuberay03.py +77 -0
- raylings/solutions/14_kuberay/kuberay04.py +58 -0
- raylings/solutions/14_kuberay/kuberay05.py +48 -0
- raylings/ui.py +265 -0
- raylings/watcher.py +166 -0
- raylings-0.1.0.dist-info/METADATA +159 -0
- raylings-0.1.0.dist-info/RECORD +145 -0
- raylings-0.1.0.dist-info/WHEEL +4 -0
- raylings-0.1.0.dist-info/entry_points.txt +2 -0
- raylings-0.1.0.dist-info/licenses/LICENSE +190 -0
raylings/__init__.py
ADDED
raylings/cli.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Command-line interface entrypoint for the Raylings learning framework."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from raylings import __version__
|
|
9
|
+
from raylings.daemon import RayDaemon
|
|
10
|
+
from raylings.manifest import get_exercise_by_name, get_manifest
|
|
11
|
+
from raylings.models import Exercise
|
|
12
|
+
from raylings.runner import ExerciseRunner
|
|
13
|
+
from raylings.ui import (
|
|
14
|
+
console,
|
|
15
|
+
render_banner,
|
|
16
|
+
render_cluster_status,
|
|
17
|
+
render_hint,
|
|
18
|
+
render_progress,
|
|
19
|
+
render_result,
|
|
20
|
+
)
|
|
21
|
+
from raylings.watcher import ExerciseWatcher
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="raylings",
|
|
25
|
+
help="Interactive Ray learning CLI inspired by Rustlings.",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
add_completion=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _version_callback(value: bool) -> None:
|
|
32
|
+
"""Callback for --version CLI option."""
|
|
33
|
+
if value:
|
|
34
|
+
render_banner()
|
|
35
|
+
console.print(
|
|
36
|
+
f"[bold cyan]raylings[/bold cyan] version [bold green]{__version__}[/bold green]\n"
|
|
37
|
+
)
|
|
38
|
+
raise typer.Exit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.callback()
|
|
42
|
+
def main(
|
|
43
|
+
version: bool | None = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--version",
|
|
46
|
+
"-v",
|
|
47
|
+
help="Show raylings version and exit.",
|
|
48
|
+
callback=_version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Raylings CLI main entrypoint callback."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command(name="version")
|
|
56
|
+
def version_command() -> None:
|
|
57
|
+
"""Display raylings branding banner and current version."""
|
|
58
|
+
render_banner()
|
|
59
|
+
console.print(
|
|
60
|
+
f"[bold cyan]raylings[/bold cyan] version [bold green]{__version__}[/bold green]\n"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command(name="init")
|
|
65
|
+
def init_command(
|
|
66
|
+
target_dir: Path = typer.Option(
|
|
67
|
+
Path("."),
|
|
68
|
+
"--directory",
|
|
69
|
+
"-d",
|
|
70
|
+
help="Target directory to initialize raylings exercises in (defaults to current working directory)",
|
|
71
|
+
),
|
|
72
|
+
force: bool = typer.Option(
|
|
73
|
+
False,
|
|
74
|
+
"--force",
|
|
75
|
+
"-f",
|
|
76
|
+
help="Overwrite existing exercise files if they already exist",
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Initialize a fresh Raylings workspace by extracting bundled exercises."""
|
|
80
|
+
import importlib.resources
|
|
81
|
+
import shutil
|
|
82
|
+
|
|
83
|
+
render_banner()
|
|
84
|
+
exercises_dest = target_dir / "exercises"
|
|
85
|
+
|
|
86
|
+
if exercises_dest.exists() and not force:
|
|
87
|
+
console.print(
|
|
88
|
+
f"[bold yellow]Directory '{exercises_dest}' already exists.[/bold yellow]\n"
|
|
89
|
+
"Use [bold cyan]raylings init --force[/bold cyan] to overwrite, or run [bold green]raylings watch[/bold green] to continue learning."
|
|
90
|
+
)
|
|
91
|
+
raise typer.Exit(0)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
bundled_pkg = importlib.resources.files("raylings")
|
|
95
|
+
bundled_exercises = bundled_pkg / "exercises"
|
|
96
|
+
|
|
97
|
+
if hasattr(bundled_exercises, "is_dir") and bundled_exercises.is_dir():
|
|
98
|
+
if exercises_dest.exists() and force:
|
|
99
|
+
shutil.rmtree(exercises_dest)
|
|
100
|
+
shutil.copytree(str(bundled_exercises), str(exercises_dest))
|
|
101
|
+
else:
|
|
102
|
+
# Fallback to local source tree if running in editable dev mode
|
|
103
|
+
repo_exercises = Path(__file__).parent.parent.parent / "exercises"
|
|
104
|
+
if repo_exercises.exists():
|
|
105
|
+
if exercises_dest.exists() and force:
|
|
106
|
+
shutil.rmtree(exercises_dest)
|
|
107
|
+
shutil.copytree(str(repo_exercises), str(exercises_dest))
|
|
108
|
+
else:
|
|
109
|
+
console.print(
|
|
110
|
+
"[bold red]Failed to locate bundled exercises in raylings package.[/bold red]"
|
|
111
|
+
)
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
console.print(
|
|
115
|
+
f"[bold green]✨ Raylings workspace initialized successfully in [white]{target_dir.resolve()}[/white]![/bold green]\n\n"
|
|
116
|
+
"To begin your Ray learning journey, run:\n"
|
|
117
|
+
" [bold cyan]raylings watch[/bold cyan]\n"
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
console.print(f"[bold red]Error initializing workspace:[/bold red] {e}")
|
|
121
|
+
raise typer.Exit(1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command(name="list")
|
|
125
|
+
def list_command() -> None:
|
|
126
|
+
"""List all 14 curriculum chapters and exercises with status breakdown."""
|
|
127
|
+
render_banner()
|
|
128
|
+
manifest = get_manifest()
|
|
129
|
+
runner = ExerciseRunner()
|
|
130
|
+
|
|
131
|
+
completed_count = 0
|
|
132
|
+
for ex in manifest.all_exercises:
|
|
133
|
+
if ex.file_path.exists() and not runner.check_marker(ex.file_path):
|
|
134
|
+
res = runner.run_exercise(ex, timeout=10.0)
|
|
135
|
+
if res.passed:
|
|
136
|
+
completed_count += 1
|
|
137
|
+
|
|
138
|
+
render_progress(manifest, completed_count=completed_count)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command(name="run")
|
|
142
|
+
def run_command(
|
|
143
|
+
exercise_name: str = typer.Argument(
|
|
144
|
+
...,
|
|
145
|
+
help="Exercise name (e.g. basics01) or direct file path to execute",
|
|
146
|
+
),
|
|
147
|
+
timeout: float = typer.Option(
|
|
148
|
+
30.0,
|
|
149
|
+
"--timeout",
|
|
150
|
+
"-t",
|
|
151
|
+
help="Subprocess timeout in seconds",
|
|
152
|
+
),
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Run an exercise or solution once and display evaluation diagnostics."""
|
|
155
|
+
ex = get_exercise_by_name(exercise_name)
|
|
156
|
+
if ex is None:
|
|
157
|
+
target_path = Path(exercise_name)
|
|
158
|
+
if target_path.exists():
|
|
159
|
+
ex = Exercise(
|
|
160
|
+
name=target_path.stem,
|
|
161
|
+
title=target_path.stem,
|
|
162
|
+
path=str(target_path),
|
|
163
|
+
chapter_name=target_path.parent.name,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
console.print(f"[bold red]Exercise '{exercise_name}' not found.[/bold red]")
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
runner = ExerciseRunner()
|
|
170
|
+
res = runner.run_exercise(ex, timeout=timeout)
|
|
171
|
+
render_result(res)
|
|
172
|
+
if not res.passed:
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command(name="hint")
|
|
177
|
+
def hint_command(
|
|
178
|
+
exercise_name: str = typer.Argument(
|
|
179
|
+
"",
|
|
180
|
+
help="Name of exercise (e.g. basics01). Defaults to current incomplete exercise.",
|
|
181
|
+
),
|
|
182
|
+
level: int = typer.Option(
|
|
183
|
+
0,
|
|
184
|
+
"--level",
|
|
185
|
+
"-l",
|
|
186
|
+
help="Progressive hint level index (0-indexed)",
|
|
187
|
+
),
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Display progressive hints for an exercise."""
|
|
190
|
+
target_ex: Exercise | None = None
|
|
191
|
+
if not exercise_name:
|
|
192
|
+
watcher = ExerciseWatcher()
|
|
193
|
+
target_ex = watcher.find_current_exercise()
|
|
194
|
+
if target_ex is None:
|
|
195
|
+
console.print("[yellow]No incomplete exercises found in curriculum.[/yellow]")
|
|
196
|
+
raise typer.Exit(0)
|
|
197
|
+
else:
|
|
198
|
+
target_ex = get_exercise_by_name(exercise_name)
|
|
199
|
+
|
|
200
|
+
if target_ex is None:
|
|
201
|
+
console.print(f"[bold red]Exercise '{exercise_name}' not found.[/bold red]")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
render_hint(target_ex, hint_level=level)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@app.command(name="test")
|
|
208
|
+
def test_command(
|
|
209
|
+
exercise_name: str = typer.Argument(
|
|
210
|
+
"",
|
|
211
|
+
help="Optional specific exercise name to test solution for",
|
|
212
|
+
),
|
|
213
|
+
all_solutions: bool = typer.Option(
|
|
214
|
+
True,
|
|
215
|
+
"--all",
|
|
216
|
+
help="Execute all canonical reference solutions",
|
|
217
|
+
),
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Execute canonical reference solutions and verify correctness."""
|
|
220
|
+
render_banner()
|
|
221
|
+
runner = ExerciseRunner()
|
|
222
|
+
manifest = get_manifest()
|
|
223
|
+
|
|
224
|
+
if exercise_name:
|
|
225
|
+
ex = get_exercise_by_name(exercise_name)
|
|
226
|
+
if ex is None:
|
|
227
|
+
console.print(f"[bold red]Exercise '{exercise_name}' not found.[/bold red]")
|
|
228
|
+
raise typer.Exit(1)
|
|
229
|
+
res = runner.run_solution(ex)
|
|
230
|
+
render_result(res)
|
|
231
|
+
if not res.passed:
|
|
232
|
+
raise typer.Exit(1)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
exercises = manifest.all_exercises
|
|
236
|
+
if not exercises:
|
|
237
|
+
console.print("[yellow]No exercises found in curriculum manifest.[/yellow]")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
passed_count = 0
|
|
241
|
+
failed_count = 0
|
|
242
|
+
console.print(f"[bold cyan]Testing {len(exercises)} reference solutions...[/bold cyan]\n")
|
|
243
|
+
|
|
244
|
+
table = Table(
|
|
245
|
+
title="Solution Verification Summary",
|
|
246
|
+
border_style="dim",
|
|
247
|
+
header_style="bold magenta",
|
|
248
|
+
)
|
|
249
|
+
table.add_column("Exercise", style="bold cyan", width=16)
|
|
250
|
+
table.add_column("Solution Path", style="dim white")
|
|
251
|
+
table.add_column("Result", justify="center", width=10)
|
|
252
|
+
|
|
253
|
+
for ex in exercises:
|
|
254
|
+
res = runner.run_solution(ex)
|
|
255
|
+
if res.passed:
|
|
256
|
+
passed_count += 1
|
|
257
|
+
table.add_row(ex.name, str(ex.solution_path), "[bold green]PASS[/bold green]")
|
|
258
|
+
else:
|
|
259
|
+
failed_count += 1
|
|
260
|
+
table.add_row(ex.name, str(ex.solution_path), "[bold red]FAIL[/bold red]")
|
|
261
|
+
|
|
262
|
+
console.print(table)
|
|
263
|
+
console.print(
|
|
264
|
+
f"\n[bold]Summary:[/bold] [bold green]{passed_count} passed[/bold green], "
|
|
265
|
+
f"[bold red]{failed_count} failed[/bold red]\n"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if failed_count > 0:
|
|
269
|
+
raise typer.Exit(1)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command(name="daemon")
|
|
273
|
+
def daemon_command(
|
|
274
|
+
action: str = typer.Argument(
|
|
275
|
+
"status",
|
|
276
|
+
help="Action to perform: start | stop | status | restart",
|
|
277
|
+
),
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Manage the background Python Ray session daemon."""
|
|
280
|
+
daemon = RayDaemon()
|
|
281
|
+
action_lower = action.lower()
|
|
282
|
+
|
|
283
|
+
if action_lower == "status":
|
|
284
|
+
info = daemon.get_cluster_info()
|
|
285
|
+
render_cluster_status(info)
|
|
286
|
+
elif action_lower == "start":
|
|
287
|
+
console.print("[cyan]Starting Ray daemon session...[/cyan]")
|
|
288
|
+
daemon.start()
|
|
289
|
+
info = daemon.get_cluster_info()
|
|
290
|
+
render_cluster_status(info)
|
|
291
|
+
elif action_lower == "stop":
|
|
292
|
+
console.print("[yellow]Stopping Ray daemon session...[/yellow]")
|
|
293
|
+
daemon.stop()
|
|
294
|
+
console.print("[green]Ray daemon stopped successfully.[/green]")
|
|
295
|
+
elif action_lower == "restart":
|
|
296
|
+
console.print("[cyan]Restarting Ray daemon session...[/cyan]")
|
|
297
|
+
daemon.stop()
|
|
298
|
+
daemon.start()
|
|
299
|
+
info = daemon.get_cluster_info()
|
|
300
|
+
render_cluster_status(info)
|
|
301
|
+
else:
|
|
302
|
+
console.print(
|
|
303
|
+
f"[bold red]Unknown daemon action: '{action}'. "
|
|
304
|
+
"Supported actions: start, stop, status, restart.[/bold red]"
|
|
305
|
+
)
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.command(name="watch")
|
|
310
|
+
def watch_command(
|
|
311
|
+
ctx: typer.Context,
|
|
312
|
+
warm_daemon: bool = typer.Option(
|
|
313
|
+
True,
|
|
314
|
+
"--warm-daemon/--no-warm-daemon",
|
|
315
|
+
help="Pre-warm Ray daemon session before watching",
|
|
316
|
+
),
|
|
317
|
+
exercise_dir: Path = typer.Option(
|
|
318
|
+
Path("exercises"),
|
|
319
|
+
"--dir",
|
|
320
|
+
"-d",
|
|
321
|
+
help="Root directory containing curriculum exercises",
|
|
322
|
+
),
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Interactive watcher mode: continuously monitors files and advances upon completion."""
|
|
325
|
+
daemon = RayDaemon() if warm_daemon else None
|
|
326
|
+
watcher = ExerciseWatcher(daemon=daemon)
|
|
327
|
+
watcher.watch_loop(exercise_dir=exercise_dir)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == "__main__":
|
|
331
|
+
app()
|
raylings/daemon.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Ray session daemon and lifecycle manager for interactive exercise execution."""
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import ray
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("raylings.daemon")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RayDaemon:
|
|
13
|
+
"""Lifecycle manager for the background Python Ray session."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
num_cpus: int = 2,
|
|
18
|
+
object_store_memory: int = 100 * 1024 * 1024,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Initialize RayDaemon with default cluster configuration parameters.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
num_cpus: Default number of logical CPUs to allocate if starting a local cluster.
|
|
24
|
+
object_store_memory: Default size in bytes for the Plasma object store (100MB default).
|
|
25
|
+
"""
|
|
26
|
+
self.num_cpus = num_cpus
|
|
27
|
+
self.object_store_memory = object_store_memory
|
|
28
|
+
|
|
29
|
+
def is_running(self) -> bool:
|
|
30
|
+
"""Return True if Ray runtime is initialized and active."""
|
|
31
|
+
return bool(ray.is_initialized())
|
|
32
|
+
|
|
33
|
+
def start(
|
|
34
|
+
self,
|
|
35
|
+
num_cpus: int | None = None,
|
|
36
|
+
object_store_memory: int | None = None,
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Start or connect to the local Ray cluster session.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
num_cpus: Number of CPUs to allocate (overrides instance default if provided).
|
|
42
|
+
object_store_memory: Object store memory in bytes (overrides instance default).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool: True if Ray is running and initialized.
|
|
46
|
+
"""
|
|
47
|
+
cpus = num_cpus if num_cpus is not None else self.num_cpus
|
|
48
|
+
mem = object_store_memory if object_store_memory is not None else self.object_store_memory
|
|
49
|
+
|
|
50
|
+
if not ray.is_initialized():
|
|
51
|
+
logger.info("Initializing Ray daemon with %d CPUs and %d bytes object store", cpus, mem)
|
|
52
|
+
ray.init(
|
|
53
|
+
ignore_reinit_error=True,
|
|
54
|
+
num_cpus=cpus,
|
|
55
|
+
object_store_memory=mem,
|
|
56
|
+
include_dashboard=False,
|
|
57
|
+
log_to_driver=False,
|
|
58
|
+
)
|
|
59
|
+
return self.is_running()
|
|
60
|
+
|
|
61
|
+
def stop(self) -> bool:
|
|
62
|
+
"""Shut down the active Ray cluster session if running.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
bool: True if Ray is no longer running.
|
|
66
|
+
"""
|
|
67
|
+
if ray.is_initialized():
|
|
68
|
+
logger.info("Shutting down Ray daemon session")
|
|
69
|
+
ray.shutdown()
|
|
70
|
+
return not self.is_running()
|
|
71
|
+
|
|
72
|
+
def reset_state(self) -> None:
|
|
73
|
+
"""Safely clean up runtime state and garbage collect between exercise runs."""
|
|
74
|
+
if ray.is_initialized():
|
|
75
|
+
gc.collect()
|
|
76
|
+
|
|
77
|
+
def get_cluster_info(self) -> dict[str, Any]:
|
|
78
|
+
"""Return cluster metadata, address, node count, and resource statistics.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
dict containing cluster status, gcs address, node count, and resource maps.
|
|
82
|
+
"""
|
|
83
|
+
if not self.is_running():
|
|
84
|
+
return {
|
|
85
|
+
"is_running": False,
|
|
86
|
+
"address": None,
|
|
87
|
+
"node_count": 0,
|
|
88
|
+
"cluster_resources": {},
|
|
89
|
+
"available_resources": {},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx = ray.get_runtime_context()
|
|
93
|
+
return {
|
|
94
|
+
"is_running": True,
|
|
95
|
+
"address": getattr(ctx, "gcs_address", None),
|
|
96
|
+
"node_count": len(ray.nodes()),
|
|
97
|
+
"cluster_resources": ray.cluster_resources(),
|
|
98
|
+
"available_resources": ray.available_resources(),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Compatibility aliases
|
|
102
|
+
def ensure_started(self) -> bool:
|
|
103
|
+
"""Ensure the daemon is running, starting it if necessary."""
|
|
104
|
+
return self.start()
|
|
105
|
+
|
|
106
|
+
def shutdown(self) -> bool:
|
|
107
|
+
"""Alias for stop()."""
|
|
108
|
+
return self.stop()
|
|
109
|
+
|
|
110
|
+
def cleanup_session(self) -> None:
|
|
111
|
+
"""Alias for reset_state()."""
|
|
112
|
+
self.reset_state()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# I AM NOT DONE
|
|
2
|
+
"""Chapter 1: Ray Core Foundations - Exercise 1: Ray Init & First Remote Task.
|
|
3
|
+
|
|
4
|
+
Welcome to Raylings!
|
|
5
|
+
|
|
6
|
+
Ray is an open-source unified framework for scaling AI and Python applications.
|
|
7
|
+
At its core, Ray turns standard Python functions into asynchronous, distributed
|
|
8
|
+
tasks executed across worker processes.
|
|
9
|
+
|
|
10
|
+
Key Concepts:
|
|
11
|
+
1. `ray.init(ignore_reinit_error=True)`: Connects your Python script to a local
|
|
12
|
+
Ray cluster or starts one automatically if not already running.
|
|
13
|
+
2. `@ray.remote`: A decorator that designates a Python function as a distributed
|
|
14
|
+
remote task.
|
|
15
|
+
3. `func.remote(*args)`: Invoking a remote function returns an `ObjectRef` (a future),
|
|
16
|
+
submitting the task to the Ray scheduler without blocking.
|
|
17
|
+
4. `ray.get(object_ref)`: Blocks until the task completes and retrieves the computed result.
|
|
18
|
+
|
|
19
|
+
Your Task:
|
|
20
|
+
- Initialize Ray using `ray.init(ignore_reinit_error=True)`.
|
|
21
|
+
- Decorate the `square` function with `@ray.remote`.
|
|
22
|
+
- Call `square.remote(7)` and retrieve the result using `ray.get()`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import ray
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# TODO: Decorate this function with @ray.remote
|
|
29
|
+
def square(x: int) -> int:
|
|
30
|
+
return x * x
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def verify() -> None:
|
|
34
|
+
# TODO: Initialize Ray
|
|
35
|
+
# ray.init(ignore_reinit_error=True)
|
|
36
|
+
|
|
37
|
+
# TODO: Invoke the remote task and get the ObjectRef
|
|
38
|
+
# result_ref = square.remote(7)
|
|
39
|
+
# result = ray.get(result_ref)
|
|
40
|
+
result = None
|
|
41
|
+
|
|
42
|
+
assert ray.is_initialized(), "Ray should be initialized"
|
|
43
|
+
assert result == 49, f"Expected 49, but got {result}"
|
|
44
|
+
print("✓ basics01 verified: Ray initialized and remote task executed successfully!")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
verify()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# I AM NOT DONE
|
|
2
|
+
"""Chapter 1: Ray Core Foundations - Exercise 2: ObjectRefs and ray.get().
|
|
3
|
+
|
|
4
|
+
In Ray, calling a remote function does NOT return the computed value directly.
|
|
5
|
+
Instead, it immediately returns an `ObjectRef`—a lightweight identifier representing
|
|
6
|
+
a future value that will be computed asynchronously on a cluster worker.
|
|
7
|
+
|
|
8
|
+
Key Concepts:
|
|
9
|
+
1. `ObjectRef`: A 28-byte unique identifier representing an object residing in Ray's
|
|
10
|
+
distributed object store.
|
|
11
|
+
2. `ray.get(object_ref)`: Blocks until the object is available and deserializes it.
|
|
12
|
+
3. Batch `ray.get([ref1, ref2, ...])`: Ray can wait on and retrieve an entire list of
|
|
13
|
+
`ObjectRef`s in parallel with a single call, which is much more efficient than calling
|
|
14
|
+
`ray.get()` one by one.
|
|
15
|
+
|
|
16
|
+
Your Task:
|
|
17
|
+
- Define a remote function `multiply(a: int, b: int) -> int`.
|
|
18
|
+
- Launch 5 parallel tasks calculating `i * 10` for `i` in `range(5)`.
|
|
19
|
+
- Store the list of `ObjectRef`s.
|
|
20
|
+
- Retrieve all 5 results in a single `ray.get(refs)` call.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ray
|
|
24
|
+
from ray import ObjectRef
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# TODO: Define a remote function named `multiply` that takes two integers and returns their product.
|
|
28
|
+
def multiply(a: int, b: int) -> int:
|
|
29
|
+
return a * b
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def verify() -> None:
|
|
33
|
+
ray.init(ignore_reinit_error=True)
|
|
34
|
+
|
|
35
|
+
# TODO: Launch 5 remote tasks to compute [0*10, 1*10, 2*10, 3*10, 4*10]
|
|
36
|
+
# and collect their ObjectRefs in `refs`.
|
|
37
|
+
refs: list[ObjectRef] = []
|
|
38
|
+
|
|
39
|
+
# TODO: Use a single ray.get() call to retrieve all results into `results`.
|
|
40
|
+
results: list[int] = []
|
|
41
|
+
|
|
42
|
+
expected = [0, 10, 20, 30, 40]
|
|
43
|
+
assert len(refs) == 5, f"Expected 5 ObjectRefs, got {len(refs)}"
|
|
44
|
+
assert results == expected, f"Expected {expected}, but got {results}"
|
|
45
|
+
print("✓ basics02 verified: ObjectRefs collected and fetched in batch successfully!")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
verify()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# I AM NOT DONE
|
|
2
|
+
"""Chapter 1: Ray Core Foundations - Exercise 3: Parallel Pipeline Execution.
|
|
3
|
+
|
|
4
|
+
A very common mistake when learning Ray is calling `ray.get()` immediately after
|
|
5
|
+
launching each remote task inside a loop:
|
|
6
|
+
|
|
7
|
+
# ❌ ANTI-PATTERN (Sequential Execution):
|
|
8
|
+
results = []
|
|
9
|
+
for x in data:
|
|
10
|
+
ref = slow_task.remote(x)
|
|
11
|
+
results.append(ray.get(ref)) # Blocks immediately! No parallelism!
|
|
12
|
+
|
|
13
|
+
Calling `ray.get()` blocks the driver until that single task finishes, forcing tasks
|
|
14
|
+
to run sequentially one after another.
|
|
15
|
+
|
|
16
|
+
# ✓ PROPER PATTERN (Parallel Execution):
|
|
17
|
+
refs = [slow_task.remote(x) for x in data] # Launch all tasks concurrently
|
|
18
|
+
results = ray.get(refs) # Wait for all tasks in parallel
|
|
19
|
+
|
|
20
|
+
Key Concepts:
|
|
21
|
+
1. Ray tasks run asynchronously in background workers as soon as `.remote()` is called.
|
|
22
|
+
2. Collecting `ObjectRef`s allows the Ray scheduler to distribute work concurrently
|
|
23
|
+
across available CPU cores.
|
|
24
|
+
|
|
25
|
+
Your Task:
|
|
26
|
+
- Refactor the sequential loop in `run_parallel()` to launch all 4 tasks concurrently.
|
|
27
|
+
- Fetch all results with a single `ray.get(refs)` call.
|
|
28
|
+
- The total parallel execution time must be significantly less than sequential time!
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import time
|
|
32
|
+
import ray
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@ray.remote
|
|
36
|
+
def simulate_work(duration: float, task_id: int) -> int:
|
|
37
|
+
time.sleep(duration)
|
|
38
|
+
return task_id * 2
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_sequential(durations: list[float]) -> tuple[list[int], float]:
|
|
42
|
+
"""Runs tasks sequentially by calling ray.get() inside the loop."""
|
|
43
|
+
start = time.perf_counter()
|
|
44
|
+
results = []
|
|
45
|
+
for i, d in enumerate(durations):
|
|
46
|
+
ref = simulate_work.remote(d, i)
|
|
47
|
+
results.append(ray.get(ref)) # Serialized!
|
|
48
|
+
elapsed = time.perf_counter() - start
|
|
49
|
+
return results, elapsed
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_parallel(durations: list[float]) -> tuple[list[int], float]:
|
|
53
|
+
"""TODO: Fix this function to run all tasks concurrently in parallel."""
|
|
54
|
+
start = time.perf_counter()
|
|
55
|
+
|
|
56
|
+
# TODO: 1. Launch all simulate_work.remote(d, i) tasks into a list of ObjectRefs
|
|
57
|
+
# TODO: 2. Call ray.get() on the list of ObjectRefs once
|
|
58
|
+
results: list[int] = []
|
|
59
|
+
|
|
60
|
+
elapsed = time.perf_counter() - start
|
|
61
|
+
return results, elapsed
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def verify() -> None:
|
|
65
|
+
ray.init(ignore_reinit_error=True)
|
|
66
|
+
|
|
67
|
+
durations = [0.08, 0.08, 0.08, 0.08]
|
|
68
|
+
|
|
69
|
+
seq_results, seq_time = run_sequential(durations)
|
|
70
|
+
par_results, par_time = run_parallel(durations)
|
|
71
|
+
|
|
72
|
+
expected = [0, 2, 4, 6]
|
|
73
|
+
assert seq_results == expected
|
|
74
|
+
assert par_results == expected, f"Expected {expected}, got {par_results}"
|
|
75
|
+
|
|
76
|
+
# Parallel time should be significantly faster than sequential time
|
|
77
|
+
# Sequential ~ 0.32s; Parallel ~ 0.08s - 0.18s
|
|
78
|
+
print(f"Sequential time: {seq_time:.3f}s | Parallel time: {par_time:.3f}s")
|
|
79
|
+
assert par_time < seq_time * 0.75, (
|
|
80
|
+
f"Parallel execution ({par_time:.3f}s) was not significantly faster than sequential ({seq_time:.3f}s)"
|
|
81
|
+
)
|
|
82
|
+
print("✓ basics03 verified: Parallel task execution pipeline confirmed!")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
verify()
|