File size: 5,024 Bytes
d412559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Train a Grandma Goodwin IDENTITY control vector on Modal.
24 contrastive pairs encoding the complete Hearthfold Recursion Anchor:
  - 5 spine principles (Joshua-first, comfort before counsel, stories over lectures,
    sacred hospitality, still remembering)
  - 4 voice registers (warm hearth, story wisdom, steady lantern, gentle witness)
  - Safety gate, recognition loop, tether words, sensory vocabulary
  - The Grandma Formula, pattern collapse recovery, quest-giver role
"""
import modal

app = modal.App("grandma-cvector-v2")

image = (
    modal.Image.debian_slim(python_version="3.12")
    .apt_install("git", "cmake", "ninja-build", "build-essential")
    .run_commands(
        "git clone --depth 1 https://github.com/ggerganov/llama.cpp /llama.cpp",
        "cd /llama.cpp && cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja",
        "cd /llama.cpp && ninja -C build llama-cvector-generator",
    )
    .pip_install("huggingface_hub")
)

vol = modal.Volume.from_name("grandma-cvector", create_if_missing=True)


def convert_pairs_to_lines(text):
    """Convert multi-line chat pairs into one-prompt-per-line format.
    Each pair starts with <start_of_turn>user and runs until the next pair."""
    pairs = []
    current = []
    for line in text.strip().split('\n'):
        if line.strip() == '<start_of_turn>user' and current:
            pairs.append('\\n'.join(current))
            current = [line.strip()]
        else:
            current.append(line.strip())
    if current:
        pairs.append('\\n'.join(current))
    return '\n'.join(pairs) + '\n'


@app.function(
    image=image,
    gpu="A10G",
    timeout=1800,
    volumes={"/vol": vol},
)
def train_cvector(positive_text: str, negative_text: str):
    import subprocess, os
    from huggingface_hub import hf_hub_download

    print("Downloading Gemma-4-26B-A4B Q4_K_M GGUF...")
    model_path = hf_hub_download(
        repo_id="aidenyyy/gemma-4-26B-A4B-it-GGUF-Q4",
        filename="gemma-4-26B-A4B-it-Q4_K_M.gguf",
        cache_dir="/vol/hf_cache",
        token="YOUR_HF_TOKEN_HERE",
    )
    print(f"Model at: {model_path}")

    pos_lines = convert_pairs_to_lines(positive_text)
    neg_lines = convert_pairs_to_lines(negative_text)

    n_pos = len(pos_lines.strip().split('\n'))
    n_neg = len(neg_lines.strip().split('\n'))
    print(f"Positive prompts: {n_pos}, Negative prompts: {n_neg}")
    assert n_pos == n_neg, f"Mismatch: {n_pos} positive vs {n_neg} negative"

    with open("/tmp/positive.txt", "w") as f:
        f.write(pos_lines)
    with open("/tmp/negative.txt", "w") as f:
        f.write(neg_lines)

    # Show first few lines for sanity
    print("First positive line:", pos_lines.split('\n')[0][:120])
    print("First negative line:", neg_lines.split('\n')[0][:120])

    output_path = "/vol/grandma-hearthfold.gguf"
    print(f"Training control vector with {n_pos} pairs...")
    result = subprocess.run(
        [
            "/llama.cpp/build/bin/llama-cvector-generator",
            "-m", model_path,
            "-ngl", "99",
            "--positive-file", "/tmp/positive.txt",
            "--negative-file", "/tmp/negative.txt",
            "--pca-iter", "2000",
            "-o", output_path,
        ],
        capture_output=True,
        text=True,
        timeout=1200,
    )
    print("STDOUT:", result.stdout[-3000:] if len(result.stdout) > 3000 else result.stdout)
    if result.stderr:
        print("STDERR:", result.stderr[-1000:] if len(result.stderr) > 1000 else result.stderr)
    print("Return code:", result.returncode)

    if os.path.exists(output_path):
        size = os.path.getsize(output_path)
        print(f"Control vector saved: {output_path} ({size} bytes)")
        return True
    return False


@app.function(image=image, volumes={"/vol": vol})
def download_cvector():
    import os
    path = "/vol/grandma-hearthfold.gguf"
    if os.path.exists(path):
        with open(path, "rb") as f:
            data = f.read()
        print(f"Vector size: {len(data)} bytes")
        return data
    return None


@app.local_entrypoint()
def main():
    import os
    script_dir = os.path.dirname(os.path.abspath(__file__))

    with open(os.path.join(script_dir, "positive.txt")) as f:
        positive_text = f.read()
    with open(os.path.join(script_dir, "negative.txt")) as f:
        negative_text = f.read()

    print(f"Training Grandma Hearthfold identity vector on Modal...")
    print(f"24 contrastive pairs encoding the complete Hearthfold Loop")
    success = train_cvector.remote(positive_text, negative_text)
    if success:
        print("Training complete! Downloading...")
        data = download_cvector.remote()
        if data:
            out_path = os.path.join(script_dir, "grandma-hearthfold.gguf")
            with open(out_path, "wb") as f:
                f.write(data)
            print(f"Saved to {out_path} ({len(data)} bytes)")
        else:
            print("Vector file not found on volume")
    else:
        print("Training failed")