File size: 5,469 Bytes
e994c16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2fb233c
e994c16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63c4f20
 
 
 
 
 
 
e994c16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2fb233c
63c4f20
 
 
 
 
 
5f76dd3
 
e994c16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a17b94
e994c16
 
 
 
 
2fb233c
63c4f20
 
 
 
 
 
5f76dd3
5f0f16b
e994c16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0245824
e994c16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63c4f20
e994c16
 
 
 
 
 
 
 
 
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
"""Runtime configuration for Project Halide.

This module intentionally contains no model imports. It is safe to import in
local CPU-only tooling, tests, and dataset preparation scripts.
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent
DATA_DIR = REPO_ROOT / "data"
STORAGE_DIR = REPO_ROOT / "storage"
CHECKPOINT_DIR = REPO_ROOT / "checkpoints"

CANONICAL_VISION_MODEL_ID = "openbmb/MiniCPM-V-4.6"
VISION_MODEL_ALIASES = {
    "openbmb/MiniCPM-V-4_6": CANONICAL_VISION_MODEL_ID,
}
DEFAULT_FINETUNED_MODEL_ID = "Lonelyguyse1/halide-vision"
DEFAULT_REASONING_MODEL_ID = "nvidia/Nemotron-Mini-4B-Instruct"


def env_bool(name: str, default: bool = False) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def env_int(name: str, default: int) -> int:
    value = os.getenv(name)
    if value is None or value.strip() == "":
        return default
    return int(value)


def env_float(name: str, default: float) -> float:
    value = os.getenv(name)
    if value is None or value.strip() == "":
        return default
    return float(value)


def env_path(name: str, default: Path) -> Path:
    value = os.getenv(name)
    return Path(value) if value else default


def normalize_model_id(model_id: str) -> str:
    return VISION_MODEL_ALIASES.get(model_id, model_id)


@dataclass(frozen=True)
class VisionConfig:
    base_model_id: str
    finetuned_model_id: str
    local_model_path: Path
    use_finetuned: bool
    downsample_mode: str
    max_slice_nums: int
    max_new_tokens: int
    max_input_pixels: int
    tile_fallback_enabled: bool
    tile_fallback_min_defects: int
    tile_min_side: int
    tile_max_side: int
    tile_overlap: float
    tile_max_tiles: int
    classical_assist_enabled: bool
    classical_assist_max_defects: int


@dataclass(frozen=True)
class ReasoningConfig:
    model_id: str
    max_new_tokens: int


@dataclass(frozen=True)
class AppConfig:
    db_path: Path
    cache_size: int
    cache_ttl_seconds: int
    gpu_duration_seconds: int
    max_history_items: int


def get_vision_config() -> VisionConfig:
    return VisionConfig(
        base_model_id=normalize_model_id(
            os.getenv("HALIDE_VISION_BASE_MODEL_ID", CANONICAL_VISION_MODEL_ID)
        ),
        finetuned_model_id=os.getenv(
            "HALIDE_VISION_FINETUNED_MODEL_ID", DEFAULT_FINETUNED_MODEL_ID
        ),
        local_model_path=env_path(
            "HALIDE_VISION_LOCAL_MODEL_PATH",
            CHECKPOINT_DIR / "minicpm-v-4.6-merged-v4-stage1",
        ),
        use_finetuned=env_bool("HALIDE_USE_FINETUNED_VISION", False),
        downsample_mode=os.getenv("HALIDE_DOWNSAMPLE_MODE", "4x"),
        max_slice_nums=env_int("HALIDE_MAX_SLICE_NUMS", 36),
        max_new_tokens=env_int("HALIDE_MAX_NEW_TOKENS", 2048),
        max_input_pixels=env_int("HALIDE_MAX_INPUT_PIXELS", 4_000_000),
        tile_fallback_enabled=env_bool("HALIDE_ENABLE_TILE_FALLBACK", True),
        tile_fallback_min_defects=env_int("HALIDE_TILE_FALLBACK_MIN_DEFECTS", 1),
        tile_min_side=env_int("HALIDE_TILE_MIN_SIDE", 900),
        tile_max_side=env_int("HALIDE_TILE_MAX_SIDE", 960),
        tile_overlap=env_float("HALIDE_TILE_OVERLAP", 0.35),
        tile_max_tiles=env_int("HALIDE_TILE_MAX_TILES", 9),
        classical_assist_enabled=env_bool("HALIDE_ENABLE_CLASSICAL_ASSIST", True),
        classical_assist_max_defects=env_int("HALIDE_CLASSICAL_ASSIST_MAX_DEFECTS", 8),
    )


def get_reasoning_config() -> ReasoningConfig:
    return ReasoningConfig(
        model_id=os.getenv("HALIDE_REASONING_MODEL_ID", DEFAULT_REASONING_MODEL_ID),
        max_new_tokens=env_int("HALIDE_NEMOTRON_MAX_TOKENS", 768),
    )


def get_app_config() -> AppConfig:
    return AppConfig(
        db_path=env_path("HALIDE_DB_PATH", STORAGE_DIR / "halide.db"),
        cache_size=env_int("HALIDE_CACHE_SIZE", 64),
        cache_ttl_seconds=env_int("HALIDE_CACHE_TTL_SECONDS", 3600),
        gpu_duration_seconds=env_int("HALIDE_GPU_DURATION_SECONDS", 120),
        max_history_items=env_int("HALIDE_HISTORY_LIMIT", 10),
    )


def running_on_hugging_face_space() -> bool:
    return bool(os.getenv("SPACE_ID") or os.getenv("SPACE_HOST"))


def require_gpu_for_inference(stage: str) -> None:
    """Refuse model inference unless a CUDA device is visible.

    Local CPU use is allowed for file I/O, JSON parsing, image resizing, tests,
    and dataset preparation. It is not allowed for loading or running the
    vision or reasoning models.
    """
    import torch

    if torch.cuda.is_available():
        return

    raise RuntimeError(
        f"Halide refused to run {stage} model inference because no CUDA GPU "
        "is visible. Run inference on Modal, Hugging Face ZeroGPU, or another "
        "GPU runtime. Local CPU is reserved for editing, parsing, and tests."
    )


__all__ = [
    "AppConfig",
    "CHECKPOINT_DIR",
    "CANONICAL_VISION_MODEL_ID",
    "DATA_DIR",
    "DEFAULT_FINETUNED_MODEL_ID",
    "DEFAULT_REASONING_MODEL_ID",
    "REPO_ROOT",
    "ReasoningConfig",
    "STORAGE_DIR",
    "VisionConfig",
    "env_bool",
    "env_float",
    "env_int",
    "env_path",
    "get_app_config",
    "get_reasoning_config",
    "get_vision_config",
    "normalize_model_id",
    "require_gpu_for_inference",
    "running_on_hugging_face_space",
]