Lucas commited on
Commit
5e5caa6
·
1 Parent(s): 37ca2c4

Disable llama.cpp schema grammar by default

Browse files
.env.example CHANGED
@@ -15,8 +15,9 @@ PARSER_LOG_PARSED_JSON=0
15
  # LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF
16
  # LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf
17
  # LLAMA_CPP_MAX_TOKENS=384
18
- # LLAMA_CPP_N_CTX=2048
19
  # LLAMA_CPP_N_THREADS=2
 
20
 
21
  # Optional Hugging Face Dataset storage.
22
  # Keep real tokens in your local .env or Hugging Face Space secrets.
 
15
  # LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF
16
  # LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf
17
  # LLAMA_CPP_MAX_TOKENS=384
18
+ # LLAMA_CPP_N_CTX=4096
19
  # LLAMA_CPP_N_THREADS=2
20
+ # LLAMA_CPP_USE_GRAMMAR=0
21
 
22
  # Optional Hugging Face Dataset storage.
23
  # Keep real tokens in your local .env or Hugging Face Space secrets.
DECISIONS.md CHANGED
@@ -91,9 +91,12 @@ with a one-line note on why.
91
  configured with `PARSER_BACKEND=llama_cpp`,
92
  `LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF`,
93
  `LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf`, `LLAMA_CPP_MAX_TOKENS=384`,
94
- `LLAMA_CPP_N_CTX=2048`, and a Space-specific `LLAMA_CPP_N_THREADS`. This replaces the
95
- slow CPU Transformers runtime. The Transformers backend remains only as an explicit
96
- local experiment/fallback path and is not installed by the default requirements.
 
 
 
97
  - **MVP training template:** use the supplied fixed 4-day exercise template for the
98
  first engine slice. Preserve exercise grouping, execution order, set counts, rep
99
  targets, and notes. For MVP simplicity, all exercise rests are normalized to 1 minute.
 
91
  configured with `PARSER_BACKEND=llama_cpp`,
92
  `LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF`,
93
  `LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf`, `LLAMA_CPP_MAX_TOKENS=384`,
94
+ `LLAMA_CPP_N_CTX=4096`, a Space-specific `LLAMA_CPP_N_THREADS`, and
95
+ `LLAMA_CPP_USE_GRAMMAR=0`. The full Pydantic JSON-schema grammar is available behind
96
+ `LLAMA_CPP_USE_GRAMMAR=1`, but it hangs on Space CPU, so the Space uses strict
97
+ prompt-only JSON plus Pydantic validation. This replaces the slow CPU Transformers
98
+ runtime. The Transformers backend remains only as an explicit local experiment/fallback
99
+ path and is not installed by the default requirements.
100
  - **MVP training template:** use the supplied fixed 4-day exercise template for the
101
  first engine slice. Preserve exercise grouping, execution order, set counts, rep
102
  targets, and notes. For MVP simplicity, all exercise rests are normalized to 1 minute.
README.md CHANGED
@@ -126,13 +126,16 @@ PARSER_BACKEND=llama_cpp
126
  LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF
127
  LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf
128
  LLAMA_CPP_MAX_TOKENS=384
129
- LLAMA_CPP_N_CTX=2048
130
  LLAMA_CPP_N_THREADS=2
 
131
  ```
132
 
133
  The Space README frontmatter preloads only the selected GGUF file at build time so the
134
  app does not need to download model weights during the first parser request. Set
135
- `LLAMA_CPP_N_THREADS` to the Space CPU thread count.
 
 
136
 
137
  If `PARSER_BACKEND` is not set, the app chooses `llama_cpp` when the Hugging Face
138
  `SPACE_ID` environment variable exists, otherwise `ollama`.
 
126
  LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF
127
  LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf
128
  LLAMA_CPP_MAX_TOKENS=384
129
+ LLAMA_CPP_N_CTX=4096
130
  LLAMA_CPP_N_THREADS=2
131
+ LLAMA_CPP_USE_GRAMMAR=0
132
  ```
133
 
134
  The Space README frontmatter preloads only the selected GGUF file at build time so the
135
  app does not need to download model weights during the first parser request. Set
136
+ `LLAMA_CPP_N_THREADS` to the Space CPU thread count. `LLAMA_CPP_USE_GRAMMAR=0` keeps
137
+ llama.cpp on prompt-only JSON generation because the full Pydantic JSON-schema grammar
138
+ was too slow on Space CPU. Pydantic validation still rejects malformed parser output.
139
 
140
  If `PARSER_BACKEND` is not set, the app chooses `llama_cpp` when the Hugging Face
141
  `SPACE_ID` environment variable exists, otherwise `ollama`.
tests/test_parser_llama_cpp.py CHANGED
@@ -7,6 +7,7 @@ from training_coach.parser_llama_cpp import (
7
  DEFAULT_LLAMA_CPP_MODEL_FILE,
8
  DEFAULT_LLAMA_CPP_MODEL_REPO,
9
  DEFAULT_LLAMA_CPP_N_CTX,
 
10
  LlamaCppRuntimeUnavailableError,
11
  _load_llama_cpp,
12
  generate_parser_response_llama_cpp,
@@ -70,7 +71,8 @@ class LlamaCppParserTest(unittest.TestCase):
70
  self.assertEqual(DEFAULT_LLAMA_CPP_MODEL_REPO, "unsloth/Qwen3-1.7B-GGUF")
71
  self.assertEqual(DEFAULT_LLAMA_CPP_MODEL_FILE, "Qwen3-1.7B-Q4_K_M.gguf")
72
  self.assertEqual(DEFAULT_LLAMA_CPP_MAX_TOKENS, 384)
73
- self.assertEqual(DEFAULT_LLAMA_CPP_N_CTX, 2048)
 
74
 
75
  def test_runtime_can_import_llama_cpp_or_reports_clear_error(self):
76
  try:
@@ -105,7 +107,7 @@ class LlamaCppParserTest(unittest.TestCase):
105
  ],
106
  )
107
 
108
- def test_generate_parser_response_uses_schema_grammar(self):
109
  with patch(
110
  "training_coach.parser_llama_cpp._load_llama_cpp",
111
  return_value=(FakeLlama, FakeGrammar),
@@ -122,6 +124,27 @@ class LlamaCppParserTest(unittest.TestCase):
122
  completion_call = FakeLlama.completion_calls[0]
123
  self.assertEqual(completion_call["max_tokens"], 128)
124
  self.assertEqual(completion_call["temperature"], 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  self.assertEqual(completion_call["grammar"]["verbose"], False)
126
  self.assertEqual(FakeGrammar.schemas[0][1], False)
127
  self.assertIn("ParsedCheckIn", FakeGrammar.schemas[0][0])
@@ -137,6 +160,7 @@ class LlamaCppParserTest(unittest.TestCase):
137
  "LLAMA_CPP_MAX_TOKENS": "128",
138
  "LLAMA_CPP_N_CTX": "4096",
139
  "LLAMA_CPP_N_THREADS": "2",
 
140
  },
141
  clear=True,
142
  ), patch(
@@ -150,6 +174,7 @@ class LlamaCppParserTest(unittest.TestCase):
150
  self.assertEqual(FakeLlama.from_pretrained_calls[0]["filename"], "model.gguf")
151
  self.assertEqual(FakeLlama.from_pretrained_calls[0]["n_ctx"], 4096)
152
  self.assertEqual(FakeLlama.from_pretrained_calls[0]["n_threads"], 2)
 
153
 
154
 
155
  if __name__ == "__main__":
 
7
  DEFAULT_LLAMA_CPP_MODEL_FILE,
8
  DEFAULT_LLAMA_CPP_MODEL_REPO,
9
  DEFAULT_LLAMA_CPP_N_CTX,
10
+ DEFAULT_LLAMA_CPP_USE_GRAMMAR,
11
  LlamaCppRuntimeUnavailableError,
12
  _load_llama_cpp,
13
  generate_parser_response_llama_cpp,
 
71
  self.assertEqual(DEFAULT_LLAMA_CPP_MODEL_REPO, "unsloth/Qwen3-1.7B-GGUF")
72
  self.assertEqual(DEFAULT_LLAMA_CPP_MODEL_FILE, "Qwen3-1.7B-Q4_K_M.gguf")
73
  self.assertEqual(DEFAULT_LLAMA_CPP_MAX_TOKENS, 384)
74
+ self.assertEqual(DEFAULT_LLAMA_CPP_N_CTX, 4096)
75
+ self.assertEqual(DEFAULT_LLAMA_CPP_USE_GRAMMAR, False)
76
 
77
  def test_runtime_can_import_llama_cpp_or_reports_clear_error(self):
78
  try:
 
107
  ],
108
  )
109
 
110
+ def test_generate_parser_response_does_not_use_schema_grammar_by_default(self):
111
  with patch(
112
  "training_coach.parser_llama_cpp._load_llama_cpp",
113
  return_value=(FakeLlama, FakeGrammar),
 
124
  completion_call = FakeLlama.completion_calls[0]
125
  self.assertEqual(completion_call["max_tokens"], 128)
126
  self.assertEqual(completion_call["temperature"], 0)
127
+ self.assertNotIn("grammar", completion_call)
128
+ self.assertEqual(FakeGrammar.schemas, [])
129
+ self.assertIn("60 min", completion_call["messages"][1]["content"])
130
+ self.assertIn("check_in", response)
131
+
132
+ def test_generate_parser_response_can_use_schema_grammar_when_enabled(self):
133
+ with patch(
134
+ "training_coach.parser_llama_cpp._load_llama_cpp",
135
+ return_value=(FakeLlama, FakeGrammar),
136
+ ):
137
+ response = generate_parser_response_llama_cpp(
138
+ "60 min",
139
+ repo_id="repo/model",
140
+ filename="model.gguf",
141
+ max_tokens=128,
142
+ n_ctx=2048,
143
+ n_threads=None,
144
+ use_grammar=True,
145
+ )
146
+
147
+ completion_call = FakeLlama.completion_calls[0]
148
  self.assertEqual(completion_call["grammar"]["verbose"], False)
149
  self.assertEqual(FakeGrammar.schemas[0][1], False)
150
  self.assertIn("ParsedCheckIn", FakeGrammar.schemas[0][0])
 
160
  "LLAMA_CPP_MAX_TOKENS": "128",
161
  "LLAMA_CPP_N_CTX": "4096",
162
  "LLAMA_CPP_N_THREADS": "2",
163
+ "LLAMA_CPP_USE_GRAMMAR": "1",
164
  },
165
  clear=True,
166
  ), patch(
 
174
  self.assertEqual(FakeLlama.from_pretrained_calls[0]["filename"], "model.gguf")
175
  self.assertEqual(FakeLlama.from_pretrained_calls[0]["n_ctx"], 4096)
176
  self.assertEqual(FakeLlama.from_pretrained_calls[0]["n_threads"], 2)
177
+ self.assertIn("grammar", FakeLlama.completion_calls[0])
178
 
179
 
180
  if __name__ == "__main__":
training_coach/parser_llama_cpp.py CHANGED
@@ -17,7 +17,8 @@ from training_coach.parser import (
17
  DEFAULT_LLAMA_CPP_MODEL_REPO = "unsloth/Qwen3-1.7B-GGUF"
18
  DEFAULT_LLAMA_CPP_MODEL_FILE = "Qwen3-1.7B-Q4_K_M.gguf"
19
  DEFAULT_LLAMA_CPP_MAX_TOKENS = 384
20
- DEFAULT_LLAMA_CPP_N_CTX = 2048
 
21
  logger = logging.getLogger(__name__)
22
 
23
 
@@ -43,6 +44,13 @@ def _optional_int_env(name: str) -> int | None:
43
  return int(raw_value)
44
 
45
 
 
 
 
 
 
 
 
46
  @lru_cache(maxsize=1)
47
  def load_llama_cpp_model(
48
  repo_id: str = DEFAULT_LLAMA_CPP_MODEL_REPO,
@@ -95,15 +103,18 @@ def generate_parser_response_llama_cpp(
95
  max_tokens: int = DEFAULT_LLAMA_CPP_MAX_TOKENS,
96
  n_ctx: int = DEFAULT_LLAMA_CPP_N_CTX,
97
  n_threads: int | None = None,
 
98
  ) -> str:
99
  start_time = perf_counter()
100
  messages = build_parser_messages(raw_text)
101
  logger.info(
102
- "event=parser_llama_cpp_generate_start repo=%s file=%s text_chars=%s max_tokens=%s",
 
103
  repo_id,
104
  filename,
105
  len(raw_text),
106
  max_tokens,
 
107
  )
108
  log_parser_messages(
109
  backend="llama_cpp",
@@ -116,12 +127,15 @@ def generate_parser_response_llama_cpp(
116
  n_ctx=n_ctx,
117
  n_threads=n_threads,
118
  )
119
- response = model.create_chat_completion(
120
- messages=messages,
121
- max_tokens=max_tokens,
122
- temperature=0,
123
- grammar=llama_cpp_response_grammar(),
124
- )
 
 
 
125
  response_text = response["choices"][0]["message"]["content"].strip()
126
  logger.info(
127
  "event=parser_llama_cpp_generate_complete repo=%s file=%s "
@@ -145,6 +159,7 @@ def parse_check_in_with_llama_cpp(raw_text: str) -> ParsedCheckIn:
145
  max_tokens = int(os.getenv("LLAMA_CPP_MAX_TOKENS", str(DEFAULT_LLAMA_CPP_MAX_TOKENS)))
146
  n_ctx = int(os.getenv("LLAMA_CPP_N_CTX", str(DEFAULT_LLAMA_CPP_N_CTX)))
147
  n_threads = _optional_int_env("LLAMA_CPP_N_THREADS")
 
148
  response_text = generate_parser_response_llama_cpp(
149
  raw_text,
150
  repo_id=repo_id,
@@ -152,5 +167,6 @@ def parse_check_in_with_llama_cpp(raw_text: str) -> ParsedCheckIn:
152
  max_tokens=max_tokens,
153
  n_ctx=n_ctx,
154
  n_threads=n_threads,
 
155
  )
156
  return parse_model_response(response_text)
 
17
  DEFAULT_LLAMA_CPP_MODEL_REPO = "unsloth/Qwen3-1.7B-GGUF"
18
  DEFAULT_LLAMA_CPP_MODEL_FILE = "Qwen3-1.7B-Q4_K_M.gguf"
19
  DEFAULT_LLAMA_CPP_MAX_TOKENS = 384
20
+ DEFAULT_LLAMA_CPP_N_CTX = 4096
21
+ DEFAULT_LLAMA_CPP_USE_GRAMMAR = False
22
  logger = logging.getLogger(__name__)
23
 
24
 
 
44
  return int(raw_value)
45
 
46
 
47
+ def _bool_env(name: str, default: bool) -> bool:
48
+ raw_value = os.getenv(name, "").strip().lower()
49
+ if not raw_value:
50
+ return default
51
+ return raw_value in {"1", "true", "yes", "on"}
52
+
53
+
54
  @lru_cache(maxsize=1)
55
  def load_llama_cpp_model(
56
  repo_id: str = DEFAULT_LLAMA_CPP_MODEL_REPO,
 
103
  max_tokens: int = DEFAULT_LLAMA_CPP_MAX_TOKENS,
104
  n_ctx: int = DEFAULT_LLAMA_CPP_N_CTX,
105
  n_threads: int | None = None,
106
+ use_grammar: bool = DEFAULT_LLAMA_CPP_USE_GRAMMAR,
107
  ) -> str:
108
  start_time = perf_counter()
109
  messages = build_parser_messages(raw_text)
110
  logger.info(
111
+ "event=parser_llama_cpp_generate_start repo=%s file=%s text_chars=%s "
112
+ "max_tokens=%s use_grammar=%s",
113
  repo_id,
114
  filename,
115
  len(raw_text),
116
  max_tokens,
117
+ use_grammar,
118
  )
119
  log_parser_messages(
120
  backend="llama_cpp",
 
127
  n_ctx=n_ctx,
128
  n_threads=n_threads,
129
  )
130
+ completion_kwargs = {
131
+ "messages": messages,
132
+ "max_tokens": max_tokens,
133
+ "temperature": 0,
134
+ }
135
+ if use_grammar:
136
+ completion_kwargs["grammar"] = llama_cpp_response_grammar()
137
+
138
+ response = model.create_chat_completion(**completion_kwargs)
139
  response_text = response["choices"][0]["message"]["content"].strip()
140
  logger.info(
141
  "event=parser_llama_cpp_generate_complete repo=%s file=%s "
 
159
  max_tokens = int(os.getenv("LLAMA_CPP_MAX_TOKENS", str(DEFAULT_LLAMA_CPP_MAX_TOKENS)))
160
  n_ctx = int(os.getenv("LLAMA_CPP_N_CTX", str(DEFAULT_LLAMA_CPP_N_CTX)))
161
  n_threads = _optional_int_env("LLAMA_CPP_N_THREADS")
162
+ use_grammar = _bool_env("LLAMA_CPP_USE_GRAMMAR", DEFAULT_LLAMA_CPP_USE_GRAMMAR)
163
  response_text = generate_parser_response_llama_cpp(
164
  raw_text,
165
  repo_id=repo_id,
 
167
  max_tokens=max_tokens,
168
  n_ctx=n_ctx,
169
  n_threads=n_threads,
170
+ use_grammar=use_grammar,
171
  )
172
  return parse_model_response(response_text)