Spaces:
Runtime error
Runtime error
Download inference.py from rudhratej09/Eco-LLM-Carbon-Aware-LLM-Inference-Routing: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/spaces/rudhratej09/Eco-LLM-Carbon-Aware-LLM-Inference-Routing/resolve/main/inference.py
- Command line
-
hf download hf://spaces/rudhratej09/Eco-LLM-Carbon-Aware-LLM-Inference-Routing/inference.py
-
curl -L -o inference.py https://huggingface.co/spaces/rudhratej09/Eco-LLM-Carbon-Aware-LLM-Inference-Routing/resolve/main/inference.py
17.9 kB
| """ | |
| inference.py - Eco-LLM Inference Routing Environment | |
| ==================================================== | |
| Spec-compliant inference script for the OpenEnv hackathon. | |
| Environment variables: | |
| HF_TOKEN Your HuggingFace API key (REQUIRED, no default) | |
| API_BASE_URL LLM endpoint (default: https://router.huggingface.co/v1) | |
| MODEL_NAME Model identifier (default: Qwen/Qwen2.5-72B-Instruct) | |
| ECO_LLM_TASK Task selector (default: all discovered tasks) | |
| Stdout format (mandatory): | |
| [START] task=<task> env=eco_llm_inference_routing model=<model> | |
| [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null> | |
| [END] success=<true|false> steps=<n> score=<0.00> rewards=<r1,r2,...,rn> | |
| FIXES vs previous version | |
| ββββββββββββββββββββββββββ | |
| 1. score and success initialised to 0.0/False BEFORE the try block so the | |
| finally clause never raises UnboundLocalError when an exception occurs | |
| before the score= assignment line. | |
| 2. _MAX_TOTAL_REWARD computed inside run_episode (per task), not at module | |
| load time β eliminates stale-value bug when task_id changes. | |
| 3. HF_TOKEN check kept in main() only (not at module load) so the script | |
| can be imported for testing without a token. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import os | |
| import re | |
| import textwrap | |
| from pathlib import Path | |
| from typing import Any, List, Optional | |
| from urllib.error import HTTPError, URLError | |
| from urllib.request import urlopen | |
| from openai import OpenAI | |
| # ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HF_TOKEN: Optional[str] = os.getenv("HF_TOKEN") | |
| API_KEY: Optional[str] = HF_TOKEN or os.getenv("OPENAI_API_KEY") | |
| API_BASE_URL: str = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") | |
| MODEL_NAME: str = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct") | |
| TASK_ID: str = os.getenv("ECO_LLM_TASK", "all") | |
| BENCHMARK: str = "eco_llm_inference_routing" | |
| ENV_BASE_URL: str = os.getenv("ENV_BASE_URL", "http://localhost:7860") | |
| MAX_STEPS: int = 50 | |
| SUCCESS_SCORE_THRESHOLD: float = 0.5 | |
| TEMPERATURE: float = 0.2 | |
| MAX_TOKENS: int = 80 | |
| # Max reward per query step = score(1.0) + cache_bonus(0.5) = 1.5 | |
| _MAX_REWARD_PER_STEP: float = 1.5 | |
| SYSTEM_PROMPT = textwrap.dedent(""" | |
| You are an LLM inference router for the Eco-LLM environment. | |
| At each step you receive: | |
| - The current query | |
| - Carbon intensity of the grid (0.0 = clean, 1.0 = very dirty) | |
| - Cache contents (queries already answered correctly β re-use for free) | |
| - Whether a knowledge base (KB) is available for this query | |
| Your goal is to maximise total reward: | |
| reward = score - carbon_penalty - latency_penalty + bonuses | |
| where: | |
| score = 1.0 if model tier >= query minimum, else 0.0 | |
| carbon_penalty = carbon_intensity * energy_cost (capped at 0.8) | |
| latency_penalty = 0.1 * latency_cost | |
| cache_bonus = +0.5 for a cache hit | |
| early_exit_bonus= +0.1 for EARLY_EXIT when correct | |
| Model costs: | |
| SMALL β energy 0.1, latency 1.0 | |
| MEDIUM β energy 0.3, latency 2.0 | |
| LARGE β energy 0.6, latency 5.0 | |
| Strategies: | |
| NONE β direct inference with chosen model | |
| USE_CACHE β serve from cache (only if query is in cache_contents) | |
| DO_CASCADE β try models smallβchosen, stop on exit_flag if correct | |
| EARLY_EXIT β direct inference; bonus if correct and exits early | |
| WAIT β skip step to wait for lower carbon (useful when carbon > 0.7) | |
| CALL_KB β use knowledge base (only when kb_available=true) | |
| Respond with ONLY valid JSON, no markdown: | |
| { | |
| "strategy": "<NONE|USE_CACHE|DO_CASCADE|EARLY_EXIT|WAIT|CALL_KB>", | |
| "model_choice": "<SMALL|MEDIUM|LARGE>", | |
| "exit_flag": <true|false> | |
| } | |
| """).strip() | |
| # ββ Logging helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def log_start(task: str, env: str, model: str) -> None: | |
| print(f"[START] task={task} env={env} model={model}", flush=True) | |
| def log_step( | |
| step: int, action: str, reward: float, done: bool, error: Optional[str] | |
| ) -> None: | |
| error_val = sanitize_field(error) if error else "null" | |
| print( | |
| f"[STEP] step={step} action={action} " | |
| f"reward={reward:.2f} done={str(done).lower()} error={error_val}", | |
| flush=True, | |
| ) | |
| def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None: | |
| """ | |
| [END] success=<true|false> steps=<n> score=<0.00> rewards=<r1,r2,...,rn> | |
| """ | |
| rewards_str = ",".join(f"{r:.2f}" for r in rewards) | |
| print( | |
| f"[END] success={str(success).lower()} steps={steps} " | |
| f"score={score:.2f} rewards={rewards_str}", | |
| flush=True, | |
| ) | |
| def sanitize_field(value: Optional[str]) -> str: | |
| if not value: | |
| return "null" | |
| compact = re.sub(r"\s+", " ", str(value)).strip() | |
| compact = compact.replace("[", "(").replace("]", ")") | |
| return compact if compact else "null" | |
| def _extract_task_ids(payload: Any) -> List[str]: | |
| if isinstance(payload, dict): | |
| if isinstance(payload.get("tasks"), list): | |
| return _extract_task_ids(payload["tasks"]) | |
| for key in ("available_task_ids", "task_ids"): | |
| values = payload.get(key) | |
| if isinstance(values, list): | |
| return [str(value).strip() for value in values if str(value).strip()] | |
| return [] | |
| if not isinstance(payload, list): | |
| return [] | |
| task_ids: List[str] = [] | |
| for item in payload: | |
| if isinstance(item, str) and item.strip(): | |
| task_ids.append(item.strip()) | |
| continue | |
| if not isinstance(item, dict): | |
| continue | |
| for key in ("task_id", "id", "name"): | |
| value = item.get(key) | |
| if value and str(value).strip(): | |
| task_ids.append(str(value).strip()) | |
| break | |
| return task_ids | |
| def _dedupe_task_ids(task_ids: List[str]) -> List[str]: | |
| deduped: List[str] = [] | |
| seen: set[str] = set() | |
| for task_id in task_ids: | |
| clean = task_id.strip() | |
| if not clean or clean in seen: | |
| continue | |
| seen.add(clean) | |
| deduped.append(clean) | |
| return deduped | |
| def _fetch_json(url: str) -> Optional[Any]: | |
| try: | |
| with urlopen(url, timeout=3) as response: | |
| charset = response.headers.get_content_charset() or "utf-8" | |
| return json.loads(response.read().decode(charset)) | |
| except (HTTPError, URLError, TimeoutError, json.JSONDecodeError, OSError): | |
| return None | |
| def discover_task_ids() -> List[str]: | |
| remote_candidates = [ | |
| f"{ENV_BASE_URL.rstrip('/')}/tasks", | |
| f"{ENV_BASE_URL.rstrip('/')}/tasks/meta", | |
| f"{ENV_BASE_URL.rstrip('/')}/task_ids", | |
| f"{ENV_BASE_URL.rstrip('/')}/metadata", | |
| ] | |
| for url in remote_candidates: | |
| payload = _fetch_json(url) | |
| task_ids = _dedupe_task_ids(_extract_task_ids(payload)) | |
| if task_ids: | |
| return task_ids | |
| try: | |
| from tasks import TASKS as VALIDATOR_TASKS # type: ignore | |
| task_ids = _dedupe_task_ids(_extract_task_ids(VALIDATOR_TASKS)) | |
| if task_ids: | |
| return task_ids | |
| except Exception: | |
| pass | |
| try: | |
| from server.tasks import TASKS as SERVER_TASKS # type: ignore | |
| task_ids = _dedupe_task_ids(list(SERVER_TASKS.keys())) | |
| if task_ids: | |
| return task_ids | |
| except Exception: | |
| pass | |
| manifest_path = Path(__file__).with_name("openenv.yaml") | |
| if manifest_path.exists(): | |
| raw_lines = manifest_path.read_text(encoding="utf-8").splitlines() | |
| task_ids: List[str] = [] | |
| for line in raw_lines: | |
| stripped = line.strip() | |
| if stripped.startswith("task_id:"): | |
| task_ids.append(stripped.split(":", 1)[1].strip()) | |
| task_ids = _dedupe_task_ids(task_ids) | |
| if task_ids: | |
| return task_ids | |
| return ["task_1"] | |
| def resolve_task_ids(task_selector: str) -> List[str]: | |
| available_tasks = discover_task_ids() | |
| selector = task_selector.strip() | |
| if not selector or selector.lower() == "all": | |
| return available_tasks | |
| requested_tasks = [part.strip() for part in selector.split(",") if part.strip()] | |
| return requested_tasks or available_tasks | |
| def get_task_query_count(task_id: str) -> int: | |
| try: | |
| from server.tasks import TASKS as SERVER_TASKS, TASK_ALIASES # type: ignore | |
| resolved_task_id = TASK_ALIASES.get(task_id, task_id) | |
| task_spec = SERVER_TASKS.get(resolved_task_id) | |
| if task_spec is not None: | |
| queries = getattr(task_spec, "queries", ()) | |
| if queries: | |
| return len(queries) | |
| except Exception: | |
| pass | |
| return 1 | |
| # ββ LLM action βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_user_prompt( | |
| step: int, query: str, carbon_intensity: float, | |
| cache_contents: List[str], kb_available: bool, | |
| last_reward: float, history: List[str], | |
| ) -> str: | |
| cache_str = json.dumps(cache_contents) if cache_contents else "[]" | |
| hist_block = "\n".join(history[-5:]) if history else "None" | |
| in_cache = query in cache_contents | |
| return textwrap.dedent(f""" | |
| Step: {step} | |
| Query: {query!r} | |
| In cache: {str(in_cache).lower()} | |
| Cache: {cache_str} | |
| Carbon intensity: {carbon_intensity:.2f} | |
| KB available: {str(kb_available).lower()} | |
| Last reward: {last_reward:.2f} | |
| Recent history: | |
| {hist_block} | |
| Choose the most energy-efficient routing action. | |
| """).strip() | |
| def get_routing_action( | |
| client: OpenAI, step: int, query: str, carbon_intensity: float, | |
| cache_contents: List[str], kb_available: bool, | |
| last_reward: float, history: List[str], | |
| ) -> dict: | |
| """Call LLM for routing decision; falls back to safe heuristic on any error.""" | |
| try: | |
| completion = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": build_user_prompt( | |
| step, query, carbon_intensity, cache_contents, | |
| kb_available, last_reward, history)}, | |
| ], | |
| temperature=TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| stream=False, | |
| timeout=10.0, | |
| ) | |
| raw = (completion.choices[0].message.content or "").strip() | |
| raw = raw.replace("```json", "").replace("```", "").strip() | |
| parsed = json.loads(raw) | |
| strategy = parsed.get("strategy", "NONE").upper() | |
| model_choice = parsed.get("model_choice", "SMALL").upper() | |
| exit_flag = bool(parsed.get("exit_flag", False)) | |
| if strategy not in {"NONE","USE_CACHE","DO_CASCADE","EARLY_EXIT","WAIT","CALL_KB"}: | |
| strategy = "NONE" | |
| if model_choice not in {"SMALL", "MEDIUM", "LARGE"}: | |
| model_choice = "SMALL" | |
| return {"strategy": strategy, "model_choice": model_choice, "exit_flag": exit_flag} | |
| except (asyncio.TimeoutError, TimeoutError) as exc: | |
| if query in cache_contents: | |
| return {"strategy": "USE_CACHE", "model_choice": "SMALL", "exit_flag": False} | |
| if carbon_intensity > 0.7: | |
| return {"strategy": "WAIT", "model_choice": "SMALL", "exit_flag": False} | |
| return {"strategy": "NONE", "model_choice": "SMALL", "exit_flag": False} | |
| except Exception as exc: | |
| if query in cache_contents: | |
| return {"strategy": "USE_CACHE", "model_choice": "SMALL", "exit_flag": False} | |
| if kb_available: | |
| return {"strategy": "CALL_KB", "model_choice": "SMALL", "exit_flag": False} | |
| if carbon_intensity > 0.7: | |
| return {"strategy": "WAIT", "model_choice": "SMALL", "exit_flag": False} | |
| return {"strategy": "NONE", "model_choice": "SMALL", "exit_flag": True} | |
| def action_to_str(action: dict) -> str: | |
| strategy = sanitize_field(str(action["strategy"])).replace(",", "_") | |
| model = sanitize_field(str(action["model_choice"])).replace(",", "_") | |
| return (f"strategy={strategy}," | |
| f"model={model}," | |
| f"exit={str(action['exit_flag']).lower()}") | |
| # ββ Episode runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def run_episode(client: OpenAI, task_id: str) -> float: | |
| """ | |
| Run one complete episode. Emits [START], NΓ[STEP], [END]. | |
| Returns normalised score in [0.0, 1.0]. | |
| FIX: score and success are initialised here (before the try block) | |
| so the finally clause can always call log_end() without risk of | |
| UnboundLocalError even when an exception is raised before score= line. | |
| """ | |
| # β Initialise BEFORE try so finally never hits UnboundLocalError | |
| rewards: List[float] = [] | |
| history: List[str] = [] | |
| steps_taken: int = 0 | |
| success: bool = False | |
| score: float = 0.0 # β key fix | |
| # Compute max reward for THIS task (not at module load) | |
| max_total: float = get_task_query_count(task_id) * _MAX_REWARD_PER_STEP | |
| # Try AsyncEnvClient (HF Space), fall back to local env | |
| env = None | |
| try: | |
| from openenv.client import AsyncEnvClient # type: ignore | |
| env = AsyncEnvClient(base_url=ENV_BASE_URL) | |
| except Exception: | |
| from server.env import EcoLLMInferenceRoutingEnvironment # type: ignore | |
| env = EcoLLMInferenceRoutingEnvironment() | |
| log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME) | |
| try: | |
| # reset | |
| if asyncio.iscoroutinefunction(getattr(env, "reset", None)): | |
| obs = await env.reset(task_id=task_id) | |
| else: | |
| obs = env.reset(task_id=task_id) | |
| def _field(o: object, *keys: str) -> object: | |
| for k in keys: | |
| if isinstance(o, dict): | |
| if k in o: return o[k] | |
| elif hasattr(o, k): | |
| return getattr(o, k) | |
| return None | |
| last_reward = 0.0 | |
| done = bool(_field(obs, "done") or False) | |
| for step in range(1, MAX_STEPS + 1): | |
| if done: | |
| break | |
| query = str(_field(obs, "query") or "") | |
| carbon = float(_field(obs, "carbon_intensity") or 0.5) | |
| cache = list(_field(obs, "cache_contents") or []) | |
| kb = bool(_field(obs, "kb_available") or False) | |
| action = get_routing_action(client, step, query, carbon, cache, kb, last_reward, history) | |
| action_str = action_to_str(action) | |
| error_msg: Optional[str] = None | |
| try: | |
| from server.models import ModelChoice, RLAction, Strategy # type: ignore | |
| rl_action = RLAction( | |
| strategy=Strategy(action["strategy"]), | |
| model_choice=ModelChoice(action["model_choice"]), | |
| exit_flag=action["exit_flag"], | |
| ) | |
| if asyncio.iscoroutinefunction(getattr(env, "step", None)): | |
| obs = await env.step(rl_action) | |
| else: | |
| obs = env.step(rl_action) | |
| except Exception as exc: | |
| error_msg = sanitize_field(str(exc))[:120] | |
| done = True | |
| log_step(step=step, action=action_str, reward=0.0, done=True, error=error_msg) | |
| break | |
| reward = float(_field(obs, "reward") or 0.0) | |
| done = bool(_field(obs, "done") or False) | |
| last_reward = reward | |
| rewards.append(reward) | |
| steps_taken = step | |
| log_step(step=step, action=action_str, reward=reward, done=done, error=error_msg) | |
| history.append( | |
| f"step={step} strategy={action['strategy']} " | |
| f"model={action['model_choice']} reward={reward:+.2f}" | |
| ) | |
| if done: | |
| break | |
| raw_total = sum(rewards) | |
| score = float(min(max(raw_total / max_total, 0.0), 1.0)) if max_total > 0 else 0.0 | |
| success = score >= SUCCESS_SCORE_THRESHOLD | |
| finally: | |
| try: | |
| if asyncio.iscoroutinefunction(getattr(env, "close", None)): | |
| await env.close() | |
| elif hasattr(env, "close"): | |
| env.close() | |
| except Exception: | |
| pass | |
| # score and success are always defined here (initialised above) | |
| log_end(success=success, steps=steps_taken, score=score, rewards=rewards) | |
| return score | |
| # ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main() -> None: | |
| if not API_KEY or not str(API_KEY).strip(): | |
| raise ValueError( | |
| "HF_TOKEN (or OPENAI_API_KEY) is required.\n" | |
| "Set: export HF_TOKEN='your_token'" | |
| ) | |
| client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY) | |
| async def run_tasks() -> None: | |
| for task_id in resolve_task_ids(TASK_ID): | |
| await run_episode(client, task_id) | |
| asyncio.run(run_tasks()) | |
| if __name__ == "__main__": | |
| main() | |