File size: 6,880 Bytes
e313085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
Kernel Env Environment Implementation.

reset() picks a KernelBench task and returns the prompt; each step() evaluates one ModelNew
submission (static checks -> compile -> correctness on hidden seeds -> timing) and returns
feedback the agent can use to refine. Episode ends after `max_turns` submissions.
"""

import asyncio
import hashlib
import json
import logging
import os
import random
import threading
import time
from typing import Any, Dict, Optional
from uuid import uuid4

from openenv.core.env_server.interfaces import Environment

try:
    from ..models import KernelAction, KernelObservation, KernelState
    from .evaluator import Evaluator, default_evaluator
    from .rubrics import kernel_rubric
    from .tasks import Task, build_prompt, load_tasks
except ImportError:
    from models import KernelAction, KernelObservation, KernelState
    from server.evaluator import Evaluator, default_evaluator
    from server.rubrics import kernel_rubric
    from server.tasks import Task, build_prompt, load_tasks

log = logging.getLogger(__name__)

N_TRIALS = 5
SUSPICIOUS_SPEEDUP = 50.0  # flagged in state.history for manual review of possible reward hacks
RESULT_FIELDS = ("stage", "feedback", "correct", "max_abs_err", "speedup", "t_ref_ms", "t_new_ms",
                 "t_min_ms", "bound", "arithmetic_intensity", "roofline_frac", "ref_roofline_frac")
_log_lock = threading.Lock()


def emit(event: str, **fields: Any) -> None:
    """One JSON line per event on stdout (Modal / docker logs); also appended to $KERNEL_ENV_EVAL_LOG if set."""
    line = json.dumps({"event": event, "ts": round(time.time(), 3), **fields}, default=str)
    print(line, flush=True)
    path = os.environ.get("KERNEL_ENV_EVAL_LOG")
    if path:
        with _log_lock, open(path, "a") as f:
            f.write(line + "\n")


class KernelEnvironment(Environment[KernelAction, KernelObservation, KernelState]):
    """Multi-turn GPU kernel optimization over KernelBench tasks."""

    # Episode state is per instance; GPU access is serialized by the shared Evaluator.
    SUPPORTS_CONCURRENT_SESSIONS: bool = True

    def __init__(
        self,
        evaluator: Optional[Evaluator] = None,
        tasks: Optional[Dict[str, Task]] = None,
        max_turns: Optional[int] = None,
    ):
        super().__init__(rubric=kernel_rubric())
        self._evaluator = evaluator or default_evaluator()
        self._tasks = tasks if tasks is not None else load_tasks()
        self.max_turns = max_turns or int(os.environ.get("KERNEL_ENV_MAX_TURNS", "4"))
        self._state = KernelState(episode_id=str(uuid4()))
        self._task: Optional[Task] = None
        self._rng = random.Random()

    def reset(
        self,
        seed: Optional[int] = None,
        episode_id: Optional[str] = None,
        task_id: Optional[str] = None,
        split: str = "train",
        language: str = "triton",
        **kwargs: Any,
    ) -> KernelObservation:
        """Start an episode on `task_id`, or a seeded random task from `split` ("train"/"eval")."""
        self._reset_rubric()
        self._rng = random.Random(seed)
        if task_id is None:
            pool = sorted(t for t, task in self._tasks.items() if task.split == split)
            if not pool:
                raise ValueError(f"no tasks in split {split!r}")
            task_id = self._rng.choice(pool)
        if task_id not in self._tasks:
            raise ValueError(f"unknown task_id {task_id!r}")
        self._task = self._tasks[task_id]
        self._state = KernelState(
            episode_id=episode_id or str(uuid4()), task_id=task_id, level=self._task.level
        )
        emit("reset", episode_id=self._state.episode_id, task_id=task_id, split=split, seed=seed)
        return KernelObservation(
            task_id=task_id,
            problem=build_prompt(self._task, language, self.max_turns),
            max_turns=self.max_turns,
            done=False,
            reward=0.0,
        )

    def step(self, action: KernelAction, timeout_s: Optional[float] = None, **kwargs: Any) -> KernelObservation:  # type: ignore[override]
        if self._task is None:
            raise RuntimeError("call reset() before step()")
        if self._state.step_count >= self.max_turns:
            raise RuntimeError("episode is done; call reset()")
        self._state.step_count += 1

        # hidden per-step input seeds: outputs can't be memorized across turns
        seeds = [self._rng.randrange(2**31) for _ in range(N_TRIALS)]
        res = self._evaluator.evaluate(self._task.src, action.code, action.language, seeds, task_id=self._task.task_id)

        obs = KernelObservation(
            task_id=self._task.task_id,
            turn=self._state.step_count,
            max_turns=self.max_turns,
            done=self._state.step_count >= self.max_turns,
            **{k: res[k] for k in RESULT_FIELDS if res.get(k) is not None},
        )
        obs.reward = self._apply_rubric(action, obs)

        entry = {
            "turn": obs.turn, "stage": obs.stage, "speedup": obs.speedup, "reward": obs.reward,
            "roofline_frac": obs.roofline_frac, "roofline_violation": bool(res.get("roofline_violation")),
            "gpu_id": res.get("gpu_id"), "queue_wait_s": res.get("queue_wait_s"), "eval_s": res.get("eval_s"),
            "code_sha": hashlib.sha1(action.code.encode()).hexdigest()[:12],
            "phases": res.get("phases"),  # worker seconds per phase: inputs, JIT, correctness, bench, lock wait
        }
        if obs.correct and (obs.speedup or 0) > SUSPICIOUS_SPEEDUP:
            entry["suspicious"] = True
            log.warning("suspicious speedup %.1fx on %s (episode %s)", obs.speedup, obs.task_id, self._state.episode_id)
        self._state.history.append(entry)
        emit(
            "step", episode_id=self._state.episode_id, task_id=obs.task_id, **entry,
            t_ref_ms=obs.t_ref_ms, t_new_ms=obs.t_new_ms, t_min_ms=obs.t_min_ms, bound=obs.bound,
            ref_roofline_frac=obs.ref_roofline_frac, max_abs_err=obs.max_abs_err,
            gpu=res.get("gpu"), feedback_head=obs.feedback[:200],
        )
        if obs.correct:
            self._state.best_speedup = max(self._state.best_speedup, obs.speedup or 0.0)
        return obs

    async def step_async(self, action: KernelAction, timeout_s: Optional[float] = None, **kwargs: Any) -> KernelObservation:  # type: ignore[override]
        # evaluation blocks for seconds; keep the server event loop free for other sessions
        return await asyncio.to_thread(self.step, action, timeout_s, **kwargs)

    @property
    def state(self) -> KernelState:
        return self._state