LEM/pkg/lem/config.go
Snider b9da23a0be feat(distill): add Metal memory limit config fields
CacheLimit (8GB) and MemoryLimit (16GB) in DistillConfig control
mlx.SetCacheLimit/SetMemoryLimit before model load. Conservative
defaults for 1B model on 96GB machine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:59:11 +00:00

154 lines
4.5 KiB
Go

package lem
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// AIConfig is the top-level .core/ai/ai.yaml configuration.
type AIConfig struct {
Version int `yaml:"version"`
Backend string `yaml:"backend"`
Scorer ScorerConfig `yaml:"scorer"`
Generate GenerateConfig `yaml:"generate"`
Distill DistillConfig `yaml:"distill"`
}
// ScorerConfig controls quality gating.
type ScorerConfig struct {
Engine string `yaml:"engine"`
MinScore float64 `yaml:"min_score"`
Delta bool `yaml:"delta"`
SycophancyEcho float64 `yaml:"sycophancy_echo"`
SycophancyUplift float64 `yaml:"sycophancy_uplift"`
}
// GenerateConfig holds default inference parameters.
type GenerateConfig struct {
MaxTokens int `yaml:"max_tokens"`
Temperature float64 `yaml:"temperature"`
TopP float64 `yaml:"top_p"`
TopK int `yaml:"top_k"`
RepeatPenalty float64 `yaml:"repeat_penalty"`
}
// DistillConfig holds distillation defaults.
type DistillConfig struct {
Runs int `yaml:"runs"`
MinChars int `yaml:"min_chars"`
CacheLimit int `yaml:"cache_limit"` // Metal cache limit in GB (0 = no limit)
MemoryLimit int `yaml:"memory_limit"` // Metal memory limit in GB (0 = no limit)
}
// ModelConfig is a .core/ai/models/{family}/{size}.yaml file.
type ModelConfig struct {
Version int `yaml:"version"`
Name string `yaml:"name"`
Family string `yaml:"family"`
Parameters string `yaml:"parameters"`
Format string `yaml:"format"`
Paths ModelPaths `yaml:"paths"`
Kernel string `yaml:"kernel"`
Signature string `yaml:"signature"`
Training string `yaml:"training"`
Lessons map[int]string `yaml:"lessons"`
Valid string `yaml:"valid"`
Test string `yaml:"test"`
Generate GenerateConfig `yaml:"generate"`
Baselines Baselines `yaml:"baselines"`
}
// ModelPaths holds filesystem locations for model files.
type ModelPaths struct {
Base string `yaml:"base"`
Safetensors string `yaml:"safetensors"`
}
// Baselines holds scoring reference points.
type Baselines struct {
NoKernel float64 `yaml:"no_kernel"`
WithKernel float64 `yaml:"with_kernel"`
Target float64 `yaml:"target"`
}
// ProbesConfig is a .core/ai/probes.yaml file.
type ProbesConfig struct {
Version int `yaml:"version"`
Sets map[string]ProbeSet `yaml:"sets"`
}
// ProbeSet groups related probe files.
type ProbeSet struct {
Description string `yaml:"description"`
Phase *int `yaml:"phase"`
Files []string `yaml:"files"`
}
// LoadAIConfig reads .core/ai/ai.yaml from the given root directory.
func LoadAIConfig(root string) (*AIConfig, error) {
path := filepath.Join(root, ".core", "ai", "ai.yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read ai config: %w", err)
}
var cfg AIConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse ai config: %w", err)
}
return &cfg, nil
}
// LoadModelConfig reads .core/ai/models/{model}.yaml.
// The model arg is a slash path like "gemma3/27b".
func LoadModelConfig(root, model string) (*ModelConfig, error) {
path := filepath.Join(root, ".core", "ai", "models", model+".yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read model config: %w", err)
}
var cfg ModelConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse model config: %w", err)
}
return &cfg, nil
}
// LoadProbesConfig reads .core/ai/probes.yaml.
func LoadProbesConfig(root string) (*ProbesConfig, error) {
path := filepath.Join(root, ".core", "ai", "probes.yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read probes config: %w", err)
}
var cfg ProbesConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse probes config: %w", err)
}
return &cfg, nil
}
// MergeGenerate returns a GenerateConfig with model-level overrides
// applied on top of the global defaults. Zero values in the model
// config are ignored (global default kept).
func MergeGenerate(global, model GenerateConfig) GenerateConfig {
merged := global
if model.MaxTokens > 0 {
merged.MaxTokens = model.MaxTokens
}
if model.Temperature > 0 {
merged.Temperature = model.Temperature
}
if model.TopP > 0 {
merged.TopP = model.TopP
}
if model.TopK > 0 {
merged.TopK = model.TopK
}
if model.RepeatPenalty > 0 {
merged.RepeatPenalty = model.RepeatPenalty
}
return merged
}