nexusbert commited on
Commit
f2651f6
·
1 Parent(s): dfd32f3

Update model references to Qwen2.5-1.5B-Instruct; adjust download and preload functions accordingly

Browse files
Files changed (3) hide show
  1. download_models.py +4 -2
  2. main.py +65 -91
  3. preload_models.py +12 -18
download_models.py CHANGED
@@ -1,11 +1,13 @@
1
  from huggingface_hub import snapshot_download
2
  import os
3
 
 
 
 
4
  MODELS = [
5
  "yasserrmd/GemmaECG-Vision",
6
  "google/tabfm-1.0.0-pytorch",
7
- "Qwen/Qwen3-0.6B",
8
- "BioMistral/BioMistral-7B"
9
  ]
10
 
11
  def download():
 
1
  from huggingface_hub import snapshot_download
2
  import os
3
 
4
+ # Keep the download set aligned with the active inference pipeline.
5
+ # The app now uses the lighter Qwen2.5-1.5B-Instruct path for reasoning/report synthesis
6
+ # to minimize latency on small GPU instances.
7
  MODELS = [
8
  "yasserrmd/GemmaECG-Vision",
9
  "google/tabfm-1.0.0-pytorch",
10
+ "Qwen/Qwen2.5-1.5B-Instruct",
 
11
  ]
12
 
13
  def download():
main.py CHANGED
@@ -2,7 +2,7 @@ import torch
2
  from fastapi import FastAPI, UploadFile, File, Form
3
  from pydantic import BaseModel
4
  from typing import Optional
5
- from transformers import AutoProcessor, AutoModelForMultimodalLM, AutoTokenizer, AutoModelForCausalLM, AutoModel, BitsAndBytesConfig
6
  from PIL import Image
7
  import io
8
  import uvicorn
@@ -80,6 +80,21 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
80
 
81
  models = {}
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def load_models():
84
  if models:
85
  return
@@ -123,23 +138,14 @@ def load_models():
123
  print(f"Warning: Could not load TabFM model: {e}. Using mock for pipeline demonstration.")
124
  models['tabfm_clf'] = None
125
 
126
- print("Loading Qwen3-0.6B...")
127
- models['qwen_tokenizer'] = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
128
- models['qwen_model'] = AutoModelForCausalLM.from_pretrained(
129
- "Qwen/Qwen3-0.6B",
 
130
  device_map="auto",
131
- quantization_config=quant_config,
132
- low_cpu_mem_usage=True
133
  )
134
 
135
- print("Loading BioMistral-7B...")
136
- models['mistral_tokenizer'] = AutoTokenizer.from_pretrained("BioMistral/BioMistral-7B")
137
- models['mistral_model'] = AutoModelForCausalLM.from_pretrained(
138
- "BioMistral/BioMistral-7B",
139
- device_map="auto",
140
- quantization_config=quant_config,
141
- low_cpu_mem_usage=True
142
- )
143
  print("All models loaded successfully.")
144
 
145
  class PatientMetrics(BaseModel):
@@ -215,8 +221,8 @@ async def predict(
215
  tabfm_risk_probability = 68.2
216
  tabfm_classification_label = "Positive for Heart Disease"
217
 
218
- # 3. Reasoning Layer via Qwen3-0.6B
219
- qwen_system_prompt = "You are a clinical reasoning engine. Analyze the provided patient metrics and ECG findings to provide a detailed, step-by-step diagnostic reasoning chain. Think through the correlations between demographics, vitals, and electrical cardiac activity."
220
  qwen_user_content = f"""
221
  PATIENT DATA:
222
  - Age: {patient_metrics.age}, Sex: {patient_metrics.sex}, Ethnicity: {patient_metrics.ethnicity}
@@ -234,35 +240,26 @@ async def predict(
234
  """
235
 
236
  qwen_messages = [{"role": "user", "content": f"{qwen_system_prompt}\n\n{qwen_user_content}"}]
237
- qwen_text = models['qwen_tokenizer'].apply_chat_template(
238
  qwen_messages,
239
- tokenize=False,
240
- add_generation_prompt=True,
241
- enable_thinking=True
242
- )
243
- qwen_inputs = models['qwen_tokenizer'](qwen_text, return_tensors="pt").to(models['qwen_model'].device)
244
- qwen_outputs = models['qwen_model'].generate(
245
- **qwen_inputs,
246
- max_new_tokens=1024,
247
- do_sample=True,
248
- temperature=0.6,
249
- top_p=0.95,
250
- top_k=20,
251
- min_p=0,
252
- repetition_penalty=1.2
253
  )
254
- qwen_reasoning = models['qwen_tokenizer'].decode(qwen_outputs[0][qwen_inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
255
 
256
  bmi_bp_interaction = 1.24 # Derived output
257
  health_risk_score = 0.78 # Derived output
258
 
259
- system_prompt = """You are BioMistral-7B, a specialized clinical reasoning and medical decision-support model. You are part of an end-to-end multimodal diagnostic pipeline that integrates three upstream specialized models:
260
 
261
  1. Google TabFM (google/tabfm-1.0.0-pytorch): Processes tabular clinical, demographic, metabolic, and engineered metrics.
262
  2. GemmaECG-Vision (yasserrmd/GemmaECG-Vision via image-text-to-text pipeline): Processes 12-lead ECG image waveforms.
263
- 3. Qwen3-0.6B (Qwen/Qwen3-0.6B): A reasoning engine that generates a diagnostic chain-of-thought.
264
 
265
- Your job is to ingest all patient metrics along with the outputs of GemmaECG-Vision, Google TabFM, and the reasoning generated by Qwen3-0.6B to generate a unified, explainable medical evaluation report.
266
 
267
  OPERATIONAL MANDATES:
268
  1. Complete Parameter Coverage: You MUST account for every single parameter in the input data block (Demographics, Vitals, Labs, Cardiac Features, Lifestyle, and Engineered Metrics). Do not omit any parameter.
@@ -316,31 +313,24 @@ OUTPUT HEADINGS:
316
  - TabFM Risk Probability: {tabfm_risk_probability}%
317
  - TabFM Classification Label: {tabfm_classification_label}
318
 
319
- ### 4. UPSTREAM MODEL 3 OUTPUT: Qwen3-0.6B (Qwen/Qwen3-0.6B)
320
  - Clinical Reasoning Chain: {qwen_reasoning}
321
  """
322
 
323
- messages = [
324
- {"role": "user", "content": f"{system_prompt}\n\n{user_content}"},
 
325
  ]
326
 
327
- inputs = models['mistral_tokenizer'].apply_chat_template(
328
- messages,
329
- add_generation_prompt=True,
330
- tokenize=True,
331
- return_dict=True,
332
- return_tensors="pt",
333
- ).to(models['mistral_model'].device)
334
-
335
- outputs = models['mistral_model'].generate(
336
- **inputs,
337
- max_new_tokens=1024,
338
- do_sample=True,
339
- temperature=0.7,
340
  top_p=0.9,
341
- repetition_penalty=1.2
342
  )
343
- final_report = models['mistral_tokenizer'].decode(outputs[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
344
 
345
  return {
346
  "clinical_report": final_report,
@@ -359,7 +349,7 @@ OUTPUT HEADINGS:
359
  async def analyze_ecg(file: UploadFile = File(...)):
360
  """
361
  Analyzes a 12-lead ECG image and generates a detailed clinical report using
362
- GemmaECG-Vision for visual analysis and BioMistral-7B for clinical synthesis.
363
  """
364
  image_data = await file.read()
365
  image = Image.open(io.BytesIO(image_data)).convert("RGB")
@@ -386,31 +376,22 @@ async def analyze_ecg(file: UploadFile = File(...)):
386
  ecg_outputs = models['ecg_model'].generate(**ecg_inputs, max_new_tokens=100)
387
  raw_findings = models['ecg_processor'].tokenizer.decode(ecg_outputs[0][ecg_inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
388
 
389
- # 2. Reasoning Layer via Qwen3-0.6B
390
- qwen_system_prompt = "You are a clinical reasoning engine. Analyze the raw AI-extracted ECG findings and provide a detailed, step-by-step clinical interpretation."
391
  qwen_user_content = f"RAW AI ECG FINDINGS: {raw_findings}"
392
 
393
  qwen_messages = [{"role": "user", "content": f"{qwen_system_prompt}\n\n{qwen_user_content}"}]
394
- qwen_text = models['qwen_tokenizer'].apply_chat_template(
395
  qwen_messages,
396
- tokenize=False,
397
- add_generation_prompt=True,
398
- enable_thinking=True
399
- )
400
- qwen_inputs = models['qwen_tokenizer'](qwen_text, return_tensors="pt").to(models['qwen_model'].device)
401
- qwen_outputs = models['qwen_model'].generate(
402
- **qwen_inputs,
403
- max_new_tokens=1024,
404
- do_sample=True,
405
- temperature=0.6,
406
- top_p=0.95,
407
- top_k=20,
408
- min_p=0,
409
- repetition_penalty=1.2
410
  )
411
- qwen_reasoning = models['qwen_tokenizer'].decode(qwen_outputs[0][qwen_inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
412
 
413
- # 3. Clinical Synthesis via BioMistral-7B
414
  ecg_report_system_prompt = """You are a Board-Certified Cardiologist. Your task is to take raw visual findings from an AI-based ECG analysis tool and a reasoning chain from a clinical reasoning engine, and synthesize them into a professional, structured clinical ECG report.
415
 
416
  Your report MUST include:
@@ -428,27 +409,20 @@ async def analyze_ecg(file: UploadFile = File(...)):
428
  {qwen_reasoning}
429
  """
430
 
431
- messages = [
432
- {"role": "user", "content": f"{ecg_report_system_prompt}\n\n{user_content}"},
 
433
  ]
434
 
435
- inputs = models['mistral_tokenizer'].apply_chat_template(
436
- messages,
437
- add_generation_prompt=True,
438
- tokenize=True,
439
- return_dict=True,
440
- return_tensors="pt",
441
- ).to(models['mistral_model'].device)
442
-
443
- outputs = models['mistral_model'].generate(
444
- **inputs,
445
- max_new_tokens=1024,
446
- do_sample=True,
447
- temperature=0.7,
448
  top_p=0.9,
449
- repetition_penalty=1.2
450
  )
451
- final_report = models['mistral_tokenizer'].decode(outputs[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
452
 
453
  return {
454
  "ecg_report": final_report,
 
2
  from fastapi import FastAPI, UploadFile, File, Form
3
  from pydantic import BaseModel
4
  from typing import Optional
5
+ from transformers import AutoProcessor, AutoModelForMultimodalLM, AutoTokenizer, AutoModel, BitsAndBytesConfig, pipeline
6
  from PIL import Image
7
  import io
8
  import uvicorn
 
80
 
81
  models = {}
82
 
83
+ def extract_generated_text(response):
84
+ """Normalize chat-pipeline output to a plain string."""
85
+ if not response:
86
+ return ""
87
+
88
+ generated = response[0].get("generated_text", "")
89
+ if isinstance(generated, list):
90
+ if generated and isinstance(generated[-1], dict):
91
+ return generated[-1].get("content", "")
92
+ return " ".join(part.get("content", "") for part in generated if isinstance(part, dict))
93
+ if isinstance(generated, str):
94
+ return generated
95
+ return str(generated)
96
+
97
+
98
  def load_models():
99
  if models:
100
  return
 
138
  print(f"Warning: Could not load TabFM model: {e}. Using mock for pipeline demonstration.")
139
  models['tabfm_clf'] = None
140
 
141
+ print("Loading Qwen2.5-1.5B-Instruct via pipeline...")
142
+ models['qwen_pipeline'] = pipeline(
143
+ "text-generation",
144
+ model="Qwen/Qwen2.5-1.5B-Instruct",
145
+ torch_dtype=DTYPE,
146
  device_map="auto",
 
 
147
  )
148
 
 
 
 
 
 
 
 
 
149
  print("All models loaded successfully.")
150
 
151
  class PatientMetrics(BaseModel):
 
221
  tabfm_risk_probability = 68.2
222
  tabfm_classification_label = "Positive for Heart Disease"
223
 
224
+ # 3. Reasoning Layer via Qwen2.5-1.5B-Instruct
225
+ qwen_system_prompt = "You are a clinical reasoning engine powered by Qwen2.5-1.5B-Instruct. Analyze the provided patient metrics and ECG findings to provide a detailed, step-by-step diagnostic reasoning chain. Think through the correlations between demographics, vitals, and electrical cardiac activity."
226
  qwen_user_content = f"""
227
  PATIENT DATA:
228
  - Age: {patient_metrics.age}, Sex: {patient_metrics.sex}, Ethnicity: {patient_metrics.ethnicity}
 
240
  """
241
 
242
  qwen_messages = [{"role": "user", "content": f"{qwen_system_prompt}\n\n{qwen_user_content}"}]
243
+ qwen_outputs = models['qwen_pipeline'](
244
  qwen_messages,
245
+ max_new_tokens=300,
246
+ do_sample=False,
247
+ temperature=0.2,
248
+ top_p=0.9,
249
+ repetition_penalty=1.1,
 
 
 
 
 
 
 
 
 
250
  )
251
+ qwen_reasoning = extract_generated_text(qwen_outputs)
252
 
253
  bmi_bp_interaction = 1.24 # Derived output
254
  health_risk_score = 0.78 # Derived output
255
 
256
+ system_prompt = """You are a specialized clinical decision-support model powered by Qwen2.5-1.5B-Instruct. You are part of an end-to-end multimodal diagnostic pipeline that integrates three upstream specialized models:
257
 
258
  1. Google TabFM (google/tabfm-1.0.0-pytorch): Processes tabular clinical, demographic, metabolic, and engineered metrics.
259
  2. GemmaECG-Vision (yasserrmd/GemmaECG-Vision via image-text-to-text pipeline): Processes 12-lead ECG image waveforms.
260
+ 3. Qwen2.5-1.5B-Instruct: A reasoning engine that generates a diagnostic chain-of-thought.
261
 
262
+ Your job is to ingest all patient metrics along with the outputs of GemmaECG-Vision, Google TabFM, and the reasoning generated by Qwen2.5-1.5B-Instruct to generate a unified, explainable medical evaluation report.
263
 
264
  OPERATIONAL MANDATES:
265
  1. Complete Parameter Coverage: You MUST account for every single parameter in the input data block (Demographics, Vitals, Labs, Cardiac Features, Lifestyle, and Engineered Metrics). Do not omit any parameter.
 
313
  - TabFM Risk Probability: {tabfm_risk_probability}%
314
  - TabFM Classification Label: {tabfm_classification_label}
315
 
316
+ ### 4. UPSTREAM MODEL 3 OUTPUT: Qwen2.5-1.5B-Instruct
317
  - Clinical Reasoning Chain: {qwen_reasoning}
318
  """
319
 
320
+ final_messages = [
321
+ {"role": "system", "content": system_prompt},
322
+ {"role": "user", "content": user_content},
323
  ]
324
 
325
+ final_output = models['qwen_pipeline'](
326
+ final_messages,
327
+ max_new_tokens=350,
328
+ do_sample=False,
329
+ temperature=0.2,
 
 
 
 
 
 
 
 
330
  top_p=0.9,
331
+ repetition_penalty=1.1,
332
  )
333
+ final_report = extract_generated_text(final_output)
334
 
335
  return {
336
  "clinical_report": final_report,
 
349
  async def analyze_ecg(file: UploadFile = File(...)):
350
  """
351
  Analyzes a 12-lead ECG image and generates a detailed clinical report using
352
+ GemmaECG-Vision for visual analysis and Qwen2.5-1.5B-Instruct for clinical synthesis.
353
  """
354
  image_data = await file.read()
355
  image = Image.open(io.BytesIO(image_data)).convert("RGB")
 
376
  ecg_outputs = models['ecg_model'].generate(**ecg_inputs, max_new_tokens=100)
377
  raw_findings = models['ecg_processor'].tokenizer.decode(ecg_outputs[0][ecg_inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
378
 
379
+ # 2. Reasoning Layer via Qwen2.5-1.5B-Instruct
380
+ qwen_system_prompt = "You are a clinical reasoning engine powered by Qwen2.5-1.5B-Instruct. Analyze the raw AI-extracted ECG findings and provide a detailed, step-by-step clinical interpretation."
381
  qwen_user_content = f"RAW AI ECG FINDINGS: {raw_findings}"
382
 
383
  qwen_messages = [{"role": "user", "content": f"{qwen_system_prompt}\n\n{qwen_user_content}"}]
384
+ qwen_outputs = models['qwen_pipeline'](
385
  qwen_messages,
386
+ max_new_tokens=200,
387
+ do_sample=False,
388
+ temperature=0.2,
389
+ top_p=0.9,
390
+ repetition_penalty=1.1,
 
 
 
 
 
 
 
 
 
391
  )
392
+ qwen_reasoning = extract_generated_text(qwen_outputs)
393
 
394
+ # 3. Clinical Synthesis via Qwen2.5-1.5B-Instruct
395
  ecg_report_system_prompt = """You are a Board-Certified Cardiologist. Your task is to take raw visual findings from an AI-based ECG analysis tool and a reasoning chain from a clinical reasoning engine, and synthesize them into a professional, structured clinical ECG report.
396
 
397
  Your report MUST include:
 
409
  {qwen_reasoning}
410
  """
411
 
412
+ final_messages = [
413
+ {"role": "system", "content": ecg_report_system_prompt},
414
+ {"role": "user", "content": user_content},
415
  ]
416
 
417
+ final_output = models['qwen_pipeline'](
418
+ final_messages,
419
+ max_new_tokens=250,
420
+ do_sample=False,
421
+ temperature=0.2,
 
 
 
 
 
 
 
 
422
  top_p=0.9,
423
+ repetition_penalty=1.1,
424
  )
425
+ final_report = extract_generated_text(final_output)
426
 
427
  return {
428
  "ecg_report": final_report,
preload_models.py CHANGED
@@ -1,8 +1,9 @@
1
  import torch
2
- from transformers import AutoProcessor, AutoModelForMultimodalLM, AutoTokenizer, AutoModelForCausalLM, AutoModel, BitsAndBytesConfig
3
 
4
  DTYPE = torch.float16 if torch.cuda.is_available() else torch.float32
5
 
 
6
  def preload():
7
  print("Preloading models to disk...")
8
 
@@ -17,9 +18,9 @@ def preload():
17
  AutoProcessor.from_pretrained("yasserrmd/GemmaECG-Vision")
18
  AutoModelForMultimodalLM.from_pretrained(
19
  "yasserrmd/GemmaECG-Vision",
20
- device_map="cpu", # Use CPU for build time to avoid needing GPU in Docker build
21
  quantization_config=quant_config,
22
- low_cpu_mem_usage=True
23
  )
24
 
25
  print("Preloading Google TabFM...")
@@ -29,24 +30,17 @@ def preload():
29
  except Exception as e:
30
  print(f"Warning: Could not preload TabFM model: {e}")
31
 
32
- print("Preloading Qwen3-0.6B...")
33
- AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
34
- AutoModelForCausalLM.from_pretrained(
35
- "Qwen/Qwen3-0.6B",
36
- device_map="cpu", # Use CPU for build time to avoid needing GPU in Docker build
37
- quantization_config=quant_config,
38
- low_cpu_mem_usage=True
39
- )
40
-
41
- print("Preloading BioMistral-7B...")
42
- AutoTokenizer.from_pretrained("BioMistral/BioMistral-7B")
43
- AutoModelForCausalLM.from_pretrained(
44
- "BioMistral/BioMistral-7B",
45
  device_map="cpu",
46
- quantization_config=quant_config,
47
- low_cpu_mem_usage=True
48
  )
 
49
  print("All models preloaded successfully.")
50
 
 
51
  if __name__ == "__main__":
52
  preload()
 
1
  import torch
2
+ from transformers import AutoProcessor, AutoModelForMultimodalLM, AutoTokenizer, BitsAndBytesConfig, pipeline
3
 
4
  DTYPE = torch.float16 if torch.cuda.is_available() else torch.float32
5
 
6
+
7
  def preload():
8
  print("Preloading models to disk...")
9
 
 
18
  AutoProcessor.from_pretrained("yasserrmd/GemmaECG-Vision")
19
  AutoModelForMultimodalLM.from_pretrained(
20
  "yasserrmd/GemmaECG-Vision",
21
+ device_map="cpu", # Use CPU for build time to avoid needing GPU in Docker build
22
  quantization_config=quant_config,
23
+ low_cpu_mem_usage=True,
24
  )
25
 
26
  print("Preloading Google TabFM...")
 
30
  except Exception as e:
31
  print(f"Warning: Could not preload TabFM model: {e}")
32
 
33
+ print("Preloading Qwen2.5-1.5B-Instruct...")
34
+ AutoTokenizer.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")
35
+ pipeline(
36
+ "text-generation",
37
+ model="Qwen/Qwen2.5-1.5B-Instruct",
38
+ torch_dtype=DTYPE,
 
 
 
 
 
 
 
39
  device_map="cpu",
 
 
40
  )
41
+
42
  print("All models preloaded successfully.")
43
 
44
+
45
  if __name__ == "__main__":
46
  preload()