Spaces:
Running
Running
File size: 8,811 Bytes
9c84f9d | 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | """νμ΄νλΌμΈ 쑰립 β `extends` μμ + dot-path override.
μ: ν μΆλ§ λ°κΎΌ μ€νμ΄ νμ΄νλΌμΈ νμΌ **μ 체 μ¬λ³Έ**μΌλ‘ μμ¬ μλ€.
`pipeline_dev_v24a/b/c` λ 109μ€μ§λ¦¬ v24 μ¬λ³ΈμΈλ° μ€μ μ°¨μ΄λ
repeat_temperatures: [0.0, 0.2] β [0.0, 0.0] / [0.0, 0.3] / [0.0, 0.5]
ν μ€λΏμ΄λ€. μ¬λ³ΈμΌλ‘ λλ©΄ (1) μ€νμ μ€μ λ³μκ° diff λ₯Ό λ μΌλ§ 보μ΄κ³ ,
(2) base λ₯Ό κ³ μΉ λ μ¬λ³Έλ€μ΄ μ‘°μ©ν κ°λΌμ§λ€. μ€νμ μ μλ μ¬λ³Έμ΄ μλλΌ
**diff** μ¬μΌ νλ€.
λ κ°μ§ ννμ μ§μνλ€. λ λ€ κ°μ dot-path λ¬Έλ²μ μ΄λ€:
# νμΌλ‘ λ¨κΈ°λ μ€ν (μ¬μ¬μ©Β·μΈμ© λμ)
name: pipeline_dev_v24a
extends: pipeline_dev_v24
overrides:
steps.basic_correction.repeat_temperatures: [0.0, 0.0]
# νμΌ μμ΄ ν λ² λλ €λ³΄λ μ€ν
solar-eval runs start ... -P pipeline_dev_v24 \\
--set steps.basic_correction.repeat_temperatures='[0.0, 0.5]'
쑰립μ **λ‘λ μμ μ λλλ€** β μ€ν μμ§(`STEP_REGISTRY`)μ `extends` λ₯Ό
μμ λͺ¨λ₯΄κ³ , μμ±λ steps λͺ©λ‘λ§ λ°λλ€. κ·Έλμ μ΄ κΈ°λ₯μ μμ§μ μΌλ°μ±μ
μν₯μ μ£Όμ§ μλλ€(λ²μΒ·μμ½ λ± λ€λ₯Έ νλ‘μ νΈλ κ·Έλλ‘ μΈ μ μλ€).
μμμ **1λ¨κ³λ‘ μ ν**νλ€. base κ° λ base λ₯Ό κ°λ 체μΈμ hydra κ³μ΄μμ
λ°λ³΅μ μΌλ‘ λ¬Έμ κ° λ μ§μ μ΄κ³ ("μ΄ κ°μ΄ μ΄λμ μλκ°"λ₯Ό μΆμ λΆκ°), PoC κ·λͺ¨μμ
μ»λ κ² μλ€.
"""
import copy
import logging
from collections.abc import Callable, Mapping
from typing import Any
import yaml
logger = logging.getLogger(__name__)
#: dot-path μμ μ€ν
λͺ©λ‘μ κ°λ¦¬ν€λ μ΅μμ ν€.
STEPS_KEY = "steps"
class PipelineCompositionError(ValueError):
"""`extends` ν΄μμ΄λ override μ μ©μ΄ μ€ν¨νμ λ.
μ‘°μ©ν λμ΄κ°μ§ μλλ€ β μ€ν λ override λ "μ무 ν¨κ³Ό μλ μ€ν"μ λ§λ€κ³ ,
그건 μλͺ»λ κ²°λ‘ μΌλ‘ λ°λ‘ μ΄μ΄μ§λ€ (μ΄ μμ§μ΄ κ³Όκ±° μ‘°μ©ν no-op μ€ν
μΌλ‘
κ°μ λ°©μμΌλ‘ λ€μ³€λ€: `pipelines/errors.py` μ°Έκ³ ).
"""
def compose_pipeline(
raw: Mapping[str, Any],
*,
load_base: Callable[[str], Mapping[str, Any]] | None = None,
overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""νμ΄νλΌμΈ YAML ν μ₯μ μ€ν κ°λ₯ν μ€μ μΌλ‘ 쑰립νλ€.
Args:
raw: νμ΄νλΌμΈ YAML μ νμ±ν dict.
load_base: `extends` λμ μ΄λ¦μ λ°μ κ·Έ YAML dict λ₯Ό λλ €μ£Όλ ν¨μ.
`extends` λ₯Ό μ΄ νμ΄νλΌμΈμ 쑰립νλ €λ©΄ λ°λμ νμνλ€.
overrides: νμΌ λ°κΉ₯μμ λ€μ΄μ¨ override (CLI `--set`). YAML μ
`overrides` λ³΄λ€ **λμ€μ** μ μ©λλ€.
Returns:
`extends`/`overrides` ν€κ° κ±·ν, ν΄μμ΄ λλ μ€μ dict.
Raises:
PipelineCompositionError: base λ₯Ό λͺ» μ°Ύκ±°λ, μμμ΄ 2λ¨κ³ μ΄μμ΄κ±°λ,
override κ²½λ‘κ° μ€μ¬νμ§ μλ μ€ν
μ κ°λ¦¬ν¬ λ.
"""
base_name = raw.get("extends")
if base_name is not None and not isinstance(base_name, str):
raise PipelineCompositionError(f"`extends` must be a pipeline name, got {base_name!r}")
if base_name:
if load_base is None:
raise PipelineCompositionError(
f"Pipeline extends {base_name!r} but no loader was provided"
)
base = dict(load_base(base_name))
if base.get("extends"):
raise PipelineCompositionError(
f"Multi-level inheritance is not supported: {base_name!r} itself extends "
f"{base['extends']!r}. Flatten it or extend the root pipeline directly."
)
composed = _merge_base(base, raw)
else:
composed = copy.deepcopy({k: v for k, v in raw.items() if k != "extends"})
file_overrides = composed.pop("overrides", None) or raw.get("overrides") or {}
_apply_overrides(composed, file_overrides, origin="pipeline `overrides`")
_apply_overrides(composed, overrides or {}, origin="--set")
return composed
def _merge_base(base: dict[str, Any], child: Mapping[str, Any]) -> dict[str, Any]:
"""base μμ child μ μ΅μμ ν€λ₯Ό μΉλλ€.
`steps` λ child κ° ν΅μ§Έλ‘ μ μΈνμ λλ§ κ΅μ²΄νλ€ β λΆλΆ λ³ν©μ override μ
μΌμ΄κ³ , μ¬κΈ°μκΉμ§ νλ©΄ "steps κ° μ΄λμ μλκ°"κ° νλ €μ§λ€.
**κΉμ 볡μ¬κ° νμλ€**: μκ² λ³΅μ¬νλ©΄ μμμ override κ° base μ step dict λ₯Ό
μ μ리μμ κ³ μ³, κ°μ base λ₯Ό μ°λ λ€λ₯Έ μμκΉμ§ μ€μΌλλ€.
"""
merged = copy.deepcopy(base)
for key, value in child.items():
if key in ("extends", "overrides"):
continue
merged[key] = copy.deepcopy(value)
return merged
def _apply_overrides(config: dict[str, Any], overrides: Mapping[str, Any], *, origin: str) -> None:
"""dot-path override λ₯Ό μ μ리μ μ μ©νλ€."""
if not isinstance(overrides, Mapping):
raise PipelineCompositionError(
f"{origin} must be a mapping of dot-path β value, got {type(overrides).__name__}"
)
for path, value in overrides.items():
try:
_set_path(config, str(path), value)
except PipelineCompositionError as e:
raise PipelineCompositionError(f"{origin}: {e}") from None
def _set_path(config: dict[str, Any], path: str, value: Any) -> None:
"""`steps.<step>.<field>...` λλ `<field>...` κ²½λ‘μ κ°μ μ΄λ€."""
parts = [p for p in path.split(".") if p]
if not parts:
raise PipelineCompositionError("empty override path")
if parts[0] == STEPS_KEY:
if len(parts) < 3:
raise PipelineCompositionError(
f"{path!r} β step override needs at least "
f"'{STEPS_KEY}.<step_name>.<field>' (e.g. "
f"'{STEPS_KEY}.basic_correction.reasoning_effort')"
)
target = _find_step(config, parts[1], path)
_assign(target, parts[2:], value, path)
return
_assign(config, parts, value, path)
def _find_step(config: Mapping[str, Any], step_name: str, path: str) -> dict[str, Any]:
"""μ΄λ¦μΌλ‘ μ€ν
μ μ°Ύλλ€. μμΌλ©΄ ν보λ₯Ό μ€μ΄ raise (μ€νλ₯Ό μ‘°μ©ν λκΈ°μ§ μλλ€)."""
steps = config.get(STEPS_KEY)
if not isinstance(steps, list):
raise PipelineCompositionError(f"{path!r} β pipeline has no `{STEPS_KEY}` list")
matches = [s for s in steps if isinstance(s, Mapping) and s.get("name") == step_name]
if not matches:
names = [str(s.get("name")) for s in steps if isinstance(s, Mapping)]
raise PipelineCompositionError(
f"{path!r} β no step named {step_name!r} (steps: {', '.join(names)})"
)
if len(matches) > 1:
raise PipelineCompositionError(
f"{path!r} β {step_name!r} matches {len(matches)} steps; step names must be unique "
f"to be addressable by override"
)
return matches[0] # type: ignore[return-value]
def _assign(target: dict[str, Any], parts: list[str], value: Any, path: str) -> None:
"""μ€μ²© dict λ₯Ό λ°λΌ λ΄λ €κ°λ©° λ§μ§λ§ ν€μ κ°μ μ΄λ€. μ€κ° dict λ λ§λ€μ΄ μ€λ€."""
cursor: Any = target
for key in parts[:-1]:
nxt = cursor.get(key)
if nxt is None:
nxt = {}
cursor[key] = nxt
elif not isinstance(nxt, dict):
raise PipelineCompositionError(
f"{path!r} β {key!r} holds {type(nxt).__name__}, cannot descend into it"
)
cursor = nxt
cursor[parts[-1]] = value
def parse_set_option(raw_values: tuple[str, ...] | list[str]) -> dict[str, Any]:
"""CLI `--set key=value` λͺ©λ‘μ override λ§΅μΌλ‘ λ°κΎΌλ€.
κ°μ YAML λ‘ νμ±ν΄ νμ
μ μ΄λ¦°λ€ β `high` λ λ¬Έμμ΄, `2` λ μ μ,
`[0.0, 0.5]` λ 리μ€νΈκ° λλ€. νμ±μ μ€ν¨νλ©΄ λ¬Έμμ΄ κ·Έλλ‘ λλ€.
Raises:
PipelineCompositionError: `=` κ° μλ νλͺ©μ΄ μμ λ.
"""
overrides: dict[str, Any] = {}
for item in raw_values:
if "=" not in item:
raise PipelineCompositionError(
f"--set expects 'path=value', got {item!r} "
f"(e.g. --set steps.basic_correction.reasoning_effort=high)"
)
key, _, raw_value = item.partition("=")
key = key.strip()
if not key:
raise PipelineCompositionError(f"--set has an empty path: {item!r}")
try:
overrides[key] = yaml.safe_load(raw_value)
except yaml.YAMLError:
overrides[key] = raw_value
return overrides
|