Spaces:
Runtime error
Runtime error
Alvaro Romo
commited on
Commit
·
c2f297a
1
Parent(s):
db0086f
Fixed logging messages and refactor code. Added log in private dataset
Browse files- .gitignore +17 -0
- app.py +115 -134
- src/__pycache__/__init__.cpython-310.pyc +0 -0
- src/__pycache__/__init__.cpython-38.pyc +0 -0
- src/__pycache__/check_validity.cpython-310.pyc +0 -0
- src/__pycache__/check_validity.cpython-38.pyc +0 -0
- src/__pycache__/submit.cpython-310.pyc +0 -0
- src/check_validity.py +200 -34
- src/submit.py +3 -1
.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
.venv/
|
| 3 |
+
.mypy_cache/
|
| 4 |
+
.idea/
|
| 5 |
+
*/__pycache__/*
|
| 6 |
+
.env
|
| 7 |
+
.ipynb_checkpoints
|
| 8 |
+
*ipynb
|
| 9 |
+
.vscode/
|
| 10 |
+
.DS_Store
|
| 11 |
+
.ruff_cache/
|
| 12 |
+
.python-version
|
| 13 |
+
.profile_app.python
|
| 14 |
+
*pstats
|
| 15 |
+
*.lock
|
| 16 |
+
|
| 17 |
+
user_request/
|
app.py
CHANGED
|
@@ -1,122 +1,74 @@
|
|
| 1 |
-
import
|
| 2 |
-
import pandas as pd
|
| 3 |
-
import re
|
| 4 |
-
from datasets import load_dataset
|
| 5 |
-
import src.check_validity as cv
|
| 6 |
-
from src.submit import ModelSizeChecker
|
| 7 |
import os
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
"""
|
| 15 |
-
Validate model with some checkers to assure tha can be evaluated
|
| 16 |
-
:param model: hf model name
|
| 17 |
-
:param precision: model parameters data type
|
| 18 |
-
:param base_model: base model (if it is need it)
|
| 19 |
-
:param weight_type:
|
| 20 |
-
:param use_chat_template:
|
| 21 |
-
:return:
|
| 22 |
-
"""
|
| 23 |
-
API = HfApi()
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
except Exception as e:
|
| 28 |
-
return "Could not get your model information. Please fill it up properly."
|
| 29 |
-
|
| 30 |
-
# TODO: think if it makes sense. Maybe we need to allow upload sumissions more than once
|
| 31 |
-
# # Has it been submitted already?
|
| 32 |
-
# model_key = f"{model}_{model_info.sha}_{precision}"
|
| 33 |
-
# if model_key in requested_models:
|
| 34 |
-
# return st.error(
|
| 35 |
-
# f"The model '{model}' with revision '{model_info.sha}' and precision '{precision}' has already been submitted.")
|
| 36 |
-
|
| 37 |
-
# Check model size early
|
| 38 |
-
model_size, error_text = cv.get_model_size(model_info=model_info, precision=precision, base_model=base_model)
|
| 39 |
-
if model_size is None:
|
| 40 |
-
return error_text
|
| 41 |
-
|
| 42 |
-
# Absolute size limit for float16 and bfloat16
|
| 43 |
-
if precision in ["float16", "bfloat16"] and model_size > 100:
|
| 44 |
-
error_message = f"Sadly, models larger than 100B parameters cannot be submitted in {precision} precision at this time. " \
|
| 45 |
-
f"Your model size: {model_size:.2f}B parameters."
|
| 46 |
-
return error_message
|
| 47 |
-
|
| 48 |
-
# Precision-adjusted size limit for 8bit, 4bit, and GPTQ
|
| 49 |
-
if precision in ["8bit", "4bit", "GPTQ"]:
|
| 50 |
-
size_checker = ModelSizeChecker(model=model, precision=precision, model_size_in_b=model_size)
|
| 51 |
-
|
| 52 |
-
if not size_checker.can_evaluate():
|
| 53 |
-
precision_factor = size_checker.get_precision_factor()
|
| 54 |
-
max_size = 140 * precision_factor
|
| 55 |
-
error_message = f"Sadly, models this big ({model_size:.2f}B parameters) cannot be evaluated automatically " \
|
| 56 |
-
f"at the moment on our cluster. The maximum size for {precision} precision is {max_size:.2f}B parameters."
|
| 57 |
-
return error_message
|
| 58 |
-
|
| 59 |
-
architecture = "?"
|
| 60 |
-
# Is the model on the hub?
|
| 61 |
-
if weight_type in ["Delta", "Adapter"]:
|
| 62 |
-
base_model_on_hub, error, _ = cv.is_model_on_hub(
|
| 63 |
-
model_name=base_model, revision="main", token=None, test_tokenizer=True
|
| 64 |
-
)
|
| 65 |
-
if not base_model_on_hub:
|
| 66 |
-
return f'Base model "{base_model}" {error}'
|
| 67 |
-
if not weight_type == "Adapter":
|
| 68 |
-
model_on_hub, error, model_config = cv.is_model_on_hub(model_name=model, revision=model_info.sha,
|
| 69 |
-
test_tokenizer=True)
|
| 70 |
-
if not model_on_hub or model_config is None:
|
| 71 |
-
return f'Model "{model}" {error}'
|
| 72 |
-
if model_config is not None:
|
| 73 |
-
architectures = getattr(model_config, "architectures", None)
|
| 74 |
-
if architectures:
|
| 75 |
-
architecture = ";".join(architectures)
|
| 76 |
-
|
| 77 |
-
# Were the model card and license filled?
|
| 78 |
-
try:
|
| 79 |
-
model_info.cardData["license"]
|
| 80 |
-
except Exception:
|
| 81 |
-
return "Please select a license for your model"
|
| 82 |
-
|
| 83 |
-
modelcard_OK, error_msg, model_card = cv.check_model_card(model)
|
| 84 |
-
if not modelcard_OK:
|
| 85 |
-
return error_msg
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
return chat_template_error
|
| 92 |
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
# Function to send email
|
| 96 |
-
def log_submission(model_name, description, user_contact):
|
| 97 |
-
# todo: create email or log in dataset
|
| 98 |
-
...
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
|
| 103 |
-
def get_url(html_content):
|
| 104 |
match = re.search(r'href=["\'](https?://[^\s"\']+)', html_content)
|
| 105 |
if match:
|
| 106 |
url = match.group(1)
|
| 107 |
return url
|
| 108 |
-
|
| 109 |
-
|
| 110 |
|
| 111 |
|
| 112 |
@st.cache_data
|
| 113 |
-
def load_data():
|
| 114 |
try:
|
| 115 |
-
columns = [
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
data = data[columns]
|
| 119 |
-
# TODO: check if from submit this is neede it
|
| 120 |
data["Model"] = data["Model"].apply(get_url)
|
| 121 |
data.sort_values(by="Average ⬆️", ascending=False, inplace=True)
|
| 122 |
data.reset_index(drop=True, inplace=True)
|
|
@@ -126,11 +78,12 @@ def load_data():
|
|
| 126 |
return pd.DataFrame()
|
| 127 |
|
| 128 |
|
|
|
|
| 129 |
leaderboard_data = load_data()
|
| 130 |
-
tabs = st.tabs(["Leaderboard", "Submit model"])
|
| 131 |
|
| 132 |
with tabs[0]:
|
| 133 |
-
# logo
|
| 134 |
cols_logo = st.columns(5, vertical_alignment="center")
|
| 135 |
with cols_logo[2]:
|
| 136 |
st.image("assets/images/hf-logo.png", use_container_width=True)
|
|
@@ -148,18 +101,16 @@ with tabs[0]:
|
|
| 148 |
""",
|
| 149 |
unsafe_allow_html=True,
|
| 150 |
)
|
| 151 |
-
leaderboard_cols = st.columns([0.1, 0.8, 0.1], vertical_alignment="center")
|
| 152 |
-
with leaderboard_cols[1]:
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
else:
|
| 162 |
-
st.write("No data found to display on leaderboard.")
|
| 163 |
|
| 164 |
with tabs[1]:
|
| 165 |
st.header("Submit model")
|
|
@@ -189,54 +140,84 @@ with tabs[1]:
|
|
| 189 |
html_path = "assets/html"
|
| 190 |
for filename in os.listdir(html_path):
|
| 191 |
file_path = os.path.join(html_path, filename)
|
| 192 |
-
with open(file_path,
|
| 193 |
guide_info_list.append(file.read())
|
| 194 |
|
| 195 |
# display adding number id
|
| 196 |
for i, info_div in enumerate(guide_info_list):
|
| 197 |
-
st.markdown(get_id_number(i+1) + info_div, unsafe_allow_html=True)
|
| 198 |
|
| 199 |
with st.form("submit_model_form"):
|
| 200 |
-
model_name = st.text_input(
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
precision_option = st.selectbox(
|
| 205 |
"Choose precision format:",
|
| 206 |
help="Size limits vary by precision: • FP16/BF16: up to 100B parameters • 8-bit: up to 280B parameters (2x) • 4-bit: up to 560B parameters (4x) Choose carefully as incorrect precision can cause evaluation errors.",
|
| 207 |
options=["float16", "bfloat16", "8bit", "4bit", "GPTQ"],
|
| 208 |
-
index=0
|
| 209 |
)
|
| 210 |
weight_type_option = st.selectbox(
|
| 211 |
"Select what type of weights are being loaded from the checkpoint provided:",
|
| 212 |
help="Original: Complete model weights in safetensors format Delta: Weight differences from base model (requires base model for size calculation) Adapter: Lightweight fine-tuning layers (requires base model for size calculation)",
|
| 213 |
options=["Original", "Adapter", "Delta"],
|
| 214 |
-
index=0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
)
|
| 216 |
-
base_model_name = st.text_input("Base model",
|
| 217 |
-
help="Required for delta weights or adapters. This information is used to identify the original model and calculate the total parameter count by combining base model and adapter/delta parameters.",
|
| 218 |
-
value="")
|
| 219 |
model_type = st.selectbox(
|
| 220 |
"Choose model type:",
|
| 221 |
help="🟢 Pretrained: Base models trained on text using masked modeling 🟩 Continuously Pretrained: Extended training on additional corpus 🔶 Fine-tuned: Domain-specific optimization 💬 Chat: Models using RLHF, DPO, or IFT for conversation 🤝 Merge: Combined weights without additional training",
|
| 222 |
-
options=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
)
|
| 224 |
submit_button = st.form_submit_button("Submit Request")
|
| 225 |
|
| 226 |
if submit_button:
|
| 227 |
# validate model size, license, chat_templates
|
| 228 |
use_chat_template = True if model_type == "💬 Chat" else False
|
| 229 |
-
validation_error = validate_model(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
if validation_error is not None:
|
| 231 |
st.error(validation_error)
|
| 232 |
elif not re.match(r"[^@]+@[^@]+\.[^@]+", user_contact):
|
| 233 |
st.error("Invalid email address.")
|
| 234 |
else:
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
st.success("Your request has been sent successfully.")
|
| 237 |
-
|
| 238 |
-
st.error(
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
# st.header("Vote for next model")
|
| 242 |
-
# st.write("Esta sección estará disponible próximamente.")
|
|
|
|
| 1 |
+
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
+
import re
|
| 4 |
+
import uuid
|
| 5 |
+
from pathlib import Path
|
| 6 |
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import streamlit as st
|
| 9 |
+
from datasets import load_dataset
|
| 10 |
+
from huggingface_hub import CommitScheduler
|
| 11 |
|
| 12 |
+
from src.check_validity import validate_model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
# define page config
|
| 15 |
+
st.set_page_config(page_title="IVACE Leaderboard", layout="wide")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# setup scheduler to upload user requests
|
| 18 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", None)
|
| 19 |
+
request_file = Path("user_request/") / f"data_{uuid.uuid4()}.json"
|
| 20 |
+
request_folder = request_file.parent
|
|
|
|
| 21 |
|
| 22 |
+
scheduler = CommitScheduler(
|
| 23 |
+
repo_id="iberbench/ivace-user-request",
|
| 24 |
+
repo_type="dataset",
|
| 25 |
+
private=True,
|
| 26 |
+
folder_path=request_folder,
|
| 27 |
+
token=HF_TOKEN,
|
| 28 |
+
path_in_repo="data",
|
| 29 |
+
every=10,
|
| 30 |
+
)
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
def log_submission(input_dict: dict) -> None:
|
| 34 |
+
"""
|
| 35 |
+
Append input/outputs and user feedback to a JSON Lines file using a thread lock to avoid concurrent writes from different users.
|
| 36 |
+
"""
|
| 37 |
+
with scheduler.lock:
|
| 38 |
+
with request_file.open("a") as f:
|
| 39 |
+
f.write(json.dumps(input_dict))
|
| 40 |
+
f.write("\n")
|
| 41 |
|
| 42 |
|
| 43 |
+
def get_url(html_content: str) -> str:
|
| 44 |
match = re.search(r'href=["\'](https?://[^\s"\']+)', html_content)
|
| 45 |
if match:
|
| 46 |
url = match.group(1)
|
| 47 |
return url
|
| 48 |
+
else:
|
| 49 |
+
raise ValueError("Url not found in the link")
|
| 50 |
|
| 51 |
|
| 52 |
@st.cache_data
|
| 53 |
+
def load_data() -> pd.DataFrame:
|
| 54 |
try:
|
| 55 |
+
columns = [
|
| 56 |
+
"eval_name",
|
| 57 |
+
"Model",
|
| 58 |
+
"Type",
|
| 59 |
+
"Average ⬆️",
|
| 60 |
+
"IFEval",
|
| 61 |
+
"MMLU-PRO",
|
| 62 |
+
"GPQA",
|
| 63 |
+
"MUSR",
|
| 64 |
+
"CO₂ cost (kg)",
|
| 65 |
+
]
|
| 66 |
+
data = (
|
| 67 |
+
load_dataset("open-llm-leaderboard/contents")["train"]
|
| 68 |
+
.to_pandas()
|
| 69 |
+
.head(10)
|
| 70 |
+
)
|
| 71 |
data = data[columns]
|
|
|
|
| 72 |
data["Model"] = data["Model"].apply(get_url)
|
| 73 |
data.sort_values(by="Average ⬆️", ascending=False, inplace=True)
|
| 74 |
data.reset_index(drop=True, inplace=True)
|
|
|
|
| 78 |
return pd.DataFrame()
|
| 79 |
|
| 80 |
|
| 81 |
+
# streamlit UI
|
| 82 |
leaderboard_data = load_data()
|
| 83 |
+
tabs = st.tabs(["Leaderboard", "Submit model"])
|
| 84 |
|
| 85 |
with tabs[0]:
|
| 86 |
+
# logo image
|
| 87 |
cols_logo = st.columns(5, vertical_alignment="center")
|
| 88 |
with cols_logo[2]:
|
| 89 |
st.image("assets/images/hf-logo.png", use_container_width=True)
|
|
|
|
| 101 |
""",
|
| 102 |
unsafe_allow_html=True,
|
| 103 |
)
|
| 104 |
+
# leaderboard_cols = st.columns([0.1, 0.8, 0.1], vertical_alignment="center")
|
| 105 |
+
# with leaderboard_cols[1]:
|
| 106 |
+
if not leaderboard_data.empty:
|
| 107 |
+
st.data_editor(
|
| 108 |
+
leaderboard_data,
|
| 109 |
+
column_config={"Model": st.column_config.LinkColumn("Model")},
|
| 110 |
+
hide_index=False,
|
| 111 |
+
)
|
| 112 |
+
else:
|
| 113 |
+
st.write("No data found to display on leaderboard.")
|
|
|
|
|
|
|
| 114 |
|
| 115 |
with tabs[1]:
|
| 116 |
st.header("Submit model")
|
|
|
|
| 140 |
html_path = "assets/html"
|
| 141 |
for filename in os.listdir(html_path):
|
| 142 |
file_path = os.path.join(html_path, filename)
|
| 143 |
+
with open(file_path, "r", encoding="utf-8") as file:
|
| 144 |
guide_info_list.append(file.read())
|
| 145 |
|
| 146 |
# display adding number id
|
| 147 |
for i, info_div in enumerate(guide_info_list):
|
| 148 |
+
st.markdown(get_id_number(i + 1) + info_div, unsafe_allow_html=True)
|
| 149 |
|
| 150 |
with st.form("submit_model_form"):
|
| 151 |
+
model_name = st.text_input(
|
| 152 |
+
"Model Name (format: user_name/model_name)",
|
| 153 |
+
help="Your model should be public on the Hub and follow the username/model-id format (e.g. mistralai/Mistral-7B-v0.1).",
|
| 154 |
+
)
|
| 155 |
+
description = st.text_area(
|
| 156 |
+
"Description",
|
| 157 |
+
help="Add a description of the proposed model for the evaluation to help prioritize its evaluation",
|
| 158 |
+
)
|
| 159 |
+
user_contact = st.text_input(
|
| 160 |
+
"Your Contact Email",
|
| 161 |
+
help="User e-mail to contact when there are updates",
|
| 162 |
+
)
|
| 163 |
precision_option = st.selectbox(
|
| 164 |
"Choose precision format:",
|
| 165 |
help="Size limits vary by precision: • FP16/BF16: up to 100B parameters • 8-bit: up to 280B parameters (2x) • 4-bit: up to 560B parameters (4x) Choose carefully as incorrect precision can cause evaluation errors.",
|
| 166 |
options=["float16", "bfloat16", "8bit", "4bit", "GPTQ"],
|
| 167 |
+
index=0,
|
| 168 |
)
|
| 169 |
weight_type_option = st.selectbox(
|
| 170 |
"Select what type of weights are being loaded from the checkpoint provided:",
|
| 171 |
help="Original: Complete model weights in safetensors format Delta: Weight differences from base model (requires base model for size calculation) Adapter: Lightweight fine-tuning layers (requires base model for size calculation)",
|
| 172 |
options=["Original", "Adapter", "Delta"],
|
| 173 |
+
index=0,
|
| 174 |
+
)
|
| 175 |
+
base_model_name = st.text_input(
|
| 176 |
+
"Base model",
|
| 177 |
+
help="Required for delta weights or adapters. This information is used to identify the original model and calculate the total parameter count by combining base model and adapter/delta parameters.",
|
| 178 |
+
value="",
|
| 179 |
)
|
|
|
|
|
|
|
|
|
|
| 180 |
model_type = st.selectbox(
|
| 181 |
"Choose model type:",
|
| 182 |
help="🟢 Pretrained: Base models trained on text using masked modeling 🟩 Continuously Pretrained: Extended training on additional corpus 🔶 Fine-tuned: Domain-specific optimization 💬 Chat: Models using RLHF, DPO, or IFT for conversation 🤝 Merge: Combined weights without additional training",
|
| 183 |
+
options=[
|
| 184 |
+
"🟢 Pretrained",
|
| 185 |
+
"🟩 Continuously Pretrained",
|
| 186 |
+
"🔶 Fine-tuned",
|
| 187 |
+
"💬 Chat",
|
| 188 |
+
"🤝 Merge",
|
| 189 |
+
],
|
| 190 |
)
|
| 191 |
submit_button = st.form_submit_button("Submit Request")
|
| 192 |
|
| 193 |
if submit_button:
|
| 194 |
# validate model size, license, chat_templates
|
| 195 |
use_chat_template = True if model_type == "💬 Chat" else False
|
| 196 |
+
validation_error = validate_model(
|
| 197 |
+
model_name,
|
| 198 |
+
precision_option,
|
| 199 |
+
base_model_name,
|
| 200 |
+
weight_type_option,
|
| 201 |
+
use_chat_template,
|
| 202 |
+
)
|
| 203 |
if validation_error is not None:
|
| 204 |
st.error(validation_error)
|
| 205 |
elif not re.match(r"[^@]+@[^@]+\.[^@]+", user_contact):
|
| 206 |
st.error("Invalid email address.")
|
| 207 |
else:
|
| 208 |
+
input_dict = {
|
| 209 |
+
"model_name": model_name,
|
| 210 |
+
"description": description,
|
| 211 |
+
"user_contact": user_contact,
|
| 212 |
+
"precision_option": precision_option,
|
| 213 |
+
"weight_type_option": weight_type_option,
|
| 214 |
+
"base_model_name": base_model_name,
|
| 215 |
+
"model_type": model_type,
|
| 216 |
+
}
|
| 217 |
+
try:
|
| 218 |
+
log_submission(input_dict)
|
| 219 |
st.success("Your request has been sent successfully.")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
st.error(
|
| 222 |
+
f"Failed to send your request: {e}. Please try again later."
|
| 223 |
+
)
|
|
|
|
|
|
src/__pycache__/__init__.cpython-310.pyc
DELETED
|
Binary file (156 Bytes)
|
|
|
src/__pycache__/__init__.cpython-38.pyc
DELETED
|
Binary file (154 Bytes)
|
|
|
src/__pycache__/check_validity.cpython-310.pyc
DELETED
|
Binary file (5.85 kB)
|
|
|
src/__pycache__/check_validity.cpython-38.pyc
DELETED
|
Binary file (5.9 kB)
|
|
|
src/__pycache__/submit.cpython-310.pyc
DELETED
|
Binary file (1.22 kB)
|
|
|
src/check_validity.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
| 1 |
import json
|
| 2 |
-
import os
|
| 3 |
-
import re
|
| 4 |
import logging
|
| 5 |
-
|
| 6 |
-
from datetime import datetime, timedelta, timezone
|
| 7 |
|
| 8 |
import huggingface_hub
|
| 9 |
-
from huggingface_hub import ModelCard, hf_hub_download
|
| 10 |
-
from huggingface_hub.hf_api import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from transformers import AutoConfig, AutoTokenizer
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
# ht to @Wauplin, thank you for the snippet!
|
| 14 |
# See https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard/discussions/317
|
| 15 |
def check_model_card(repo_id: str) -> tuple[bool, str]:
|
|
@@ -17,10 +21,16 @@ def check_model_card(repo_id: str) -> tuple[bool, str]:
|
|
| 17 |
try:
|
| 18 |
card = ModelCard.load(repo_id)
|
| 19 |
except huggingface_hub.utils.EntryNotFoundError:
|
| 20 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Enforce license metadata
|
| 23 |
-
if card.data.license is None and not (
|
|
|
|
|
|
|
| 24 |
return (
|
| 25 |
False,
|
| 26 |
(
|
|
@@ -32,24 +42,44 @@ def check_model_card(repo_id: str) -> tuple[bool, str]:
|
|
| 32 |
|
| 33 |
# Enforce card content
|
| 34 |
if len(card.text) < 200:
|
| 35 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
return True, "", card
|
| 38 |
|
| 39 |
|
| 40 |
def is_model_on_hub(
|
| 41 |
-
model_name: str,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
) -> tuple[bool, str, AutoConfig]:
|
| 43 |
try:
|
| 44 |
config = AutoConfig.from_pretrained(
|
| 45 |
-
model_name,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
if test_tokenizer:
|
| 47 |
try:
|
| 48 |
-
AutoTokenizer.from_pretrained(
|
| 49 |
-
model_name,
|
|
|
|
|
|
|
|
|
|
| 50 |
)
|
| 51 |
except ValueError as e:
|
| 52 |
-
return (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
except Exception:
|
| 54 |
return (
|
| 55 |
False,
|
|
@@ -74,27 +104,42 @@ def is_model_on_hub(
|
|
| 74 |
except Exception as e:
|
| 75 |
if "You are trying to access a gated repo." in str(e):
|
| 76 |
return True, "uses a gated model.", None
|
| 77 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
-
def get_model_size(
|
|
|
|
|
|
|
| 81 |
size_pattern = re.compile(r"(\d+\.)?\d+(b|m)")
|
| 82 |
safetensors = None
|
| 83 |
adapter_safetensors = None
|
| 84 |
# hack way to check that model is adapter
|
| 85 |
-
is_adapter = "adapter_config.json" in (
|
|
|
|
|
|
|
| 86 |
|
| 87 |
try:
|
| 88 |
if is_adapter:
|
| 89 |
if not base_model:
|
| 90 |
-
return
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
adapter_safetensors = parse_safetensors_file_metadata(
|
|
|
|
|
|
|
| 93 |
safetensors = get_safetensors_metadata(base_model)
|
| 94 |
else:
|
| 95 |
safetensors = get_safetensors_metadata(model_info.id)
|
| 96 |
except Exception as e:
|
| 97 |
-
logging.warning(
|
|
|
|
|
|
|
| 98 |
|
| 99 |
if safetensors is not None:
|
| 100 |
model_size = sum(safetensors.parameter_count.values())
|
|
@@ -106,21 +151,32 @@ def get_model_size(model_info: ModelInfo, precision: str, base_model: str| None)
|
|
| 106 |
size_match = re.search(size_pattern, model_info.id.lower())
|
| 107 |
if size_match:
|
| 108 |
model_size = size_match.group(0)
|
| 109 |
-
model_size = round(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
else:
|
| 111 |
return None, "Unknown model size"
|
| 112 |
except AttributeError:
|
| 113 |
-
logging.warning(
|
|
|
|
|
|
|
| 114 |
return None, "Unknown model size"
|
| 115 |
|
| 116 |
-
size_factor =
|
|
|
|
|
|
|
| 117 |
model_size = size_factor * model_size
|
| 118 |
|
| 119 |
return model_size, ""
|
| 120 |
|
|
|
|
| 121 |
def get_model_arch(model_info: ModelInfo):
|
| 122 |
return model_info.config.get("architectures", "Unknown")
|
| 123 |
|
|
|
|
| 124 |
def check_chat_template(model: str, revision: str) -> tuple[bool, str]:
|
| 125 |
try:
|
| 126 |
# Attempt to download only the tokenizer_config.json file
|
|
@@ -128,21 +184,28 @@ def check_chat_template(model: str, revision: str) -> tuple[bool, str]:
|
|
| 128 |
repo_id=model,
|
| 129 |
filename="tokenizer_config.json",
|
| 130 |
revision=revision,
|
| 131 |
-
repo_type="model"
|
| 132 |
)
|
| 133 |
|
| 134 |
# Read and parse the tokenizer_config.json file
|
| 135 |
-
with open(config_file,
|
| 136 |
tokenizer_config = json.load(f)
|
| 137 |
|
| 138 |
# Check if chat_template exists in the tokenizer configuration
|
| 139 |
-
if
|
| 140 |
-
return
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
return True, ""
|
| 143 |
except Exception as e:
|
| 144 |
-
return
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
def get_model_tags(model_card, model: str):
|
| 147 |
is_merge_from_metadata = False
|
| 148 |
is_moe_from_metadata = False
|
|
@@ -152,22 +215,125 @@ def get_model_tags(model_card, model: str):
|
|
| 152 |
return tags
|
| 153 |
if model_card.data.tags:
|
| 154 |
is_merge_from_metadata = any(
|
| 155 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
)
|
| 157 |
-
is_moe_from_metadata = any([tag in model_card.data.tags for tag in ["moe", "moerge"]])
|
| 158 |
|
| 159 |
is_merge_from_model_card = any(
|
| 160 |
-
keyword in model_card.text.lower()
|
|
|
|
| 161 |
)
|
| 162 |
if is_merge_from_model_card or is_merge_from_metadata:
|
| 163 |
tags.append("merge")
|
| 164 |
-
is_moe_from_model_card = any(
|
|
|
|
|
|
|
| 165 |
# Hardcoding because of gating problem
|
| 166 |
if "Qwen/Qwen1.5-32B" in model:
|
| 167 |
is_moe_from_model_card = False
|
| 168 |
-
is_moe_from_name = "moe" in model.lower().replace("/", "-").replace(
|
|
|
|
|
|
|
| 169 |
if is_moe_from_model_card or is_moe_from_name or is_moe_from_metadata:
|
| 170 |
tags.append("moe")
|
| 171 |
|
| 172 |
return tags
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import json
|
|
|
|
|
|
|
| 2 |
import logging
|
| 3 |
+
import re
|
|
|
|
| 4 |
|
| 5 |
import huggingface_hub
|
| 6 |
+
from huggingface_hub import HfApi, ModelCard, hf_hub_download
|
| 7 |
+
from huggingface_hub.hf_api import (
|
| 8 |
+
ModelInfo,
|
| 9 |
+
get_safetensors_metadata,
|
| 10 |
+
parse_safetensors_file_metadata,
|
| 11 |
+
)
|
| 12 |
from transformers import AutoConfig, AutoTokenizer
|
| 13 |
|
| 14 |
+
from src.submit import ModelSizeChecker
|
| 15 |
+
|
| 16 |
+
|
| 17 |
# ht to @Wauplin, thank you for the snippet!
|
| 18 |
# See https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard/discussions/317
|
| 19 |
def check_model_card(repo_id: str) -> tuple[bool, str]:
|
|
|
|
| 21 |
try:
|
| 22 |
card = ModelCard.load(repo_id)
|
| 23 |
except huggingface_hub.utils.EntryNotFoundError:
|
| 24 |
+
return (
|
| 25 |
+
False,
|
| 26 |
+
"Please add a model card to your model to explain how you trained/fine-tuned it.",
|
| 27 |
+
None,
|
| 28 |
+
)
|
| 29 |
|
| 30 |
# Enforce license metadata
|
| 31 |
+
if card.data.license is None and not (
|
| 32 |
+
"license_name" in card.data and "license_link" in card.data
|
| 33 |
+
):
|
| 34 |
return (
|
| 35 |
False,
|
| 36 |
(
|
|
|
|
| 42 |
|
| 43 |
# Enforce card content
|
| 44 |
if len(card.text) < 200:
|
| 45 |
+
return (
|
| 46 |
+
False,
|
| 47 |
+
"Please add a description to your model card, it is too short.",
|
| 48 |
+
None,
|
| 49 |
+
)
|
| 50 |
|
| 51 |
return True, "", card
|
| 52 |
|
| 53 |
|
| 54 |
def is_model_on_hub(
|
| 55 |
+
model_name: str,
|
| 56 |
+
revision: str,
|
| 57 |
+
token: str | None = None,
|
| 58 |
+
trust_remote_code: bool = False,
|
| 59 |
+
test_tokenizer: bool = False,
|
| 60 |
) -> tuple[bool, str, AutoConfig]:
|
| 61 |
try:
|
| 62 |
config = AutoConfig.from_pretrained(
|
| 63 |
+
model_name,
|
| 64 |
+
revision=revision,
|
| 65 |
+
trust_remote_code=trust_remote_code,
|
| 66 |
+
token=token,
|
| 67 |
+
force_download=True,
|
| 68 |
+
)
|
| 69 |
if test_tokenizer:
|
| 70 |
try:
|
| 71 |
+
_ = AutoTokenizer.from_pretrained(
|
| 72 |
+
model_name,
|
| 73 |
+
revision=revision,
|
| 74 |
+
trust_remote_code=trust_remote_code,
|
| 75 |
+
token=token,
|
| 76 |
)
|
| 77 |
except ValueError as e:
|
| 78 |
+
return (
|
| 79 |
+
False,
|
| 80 |
+
f"uses a tokenizer which is not in a transformers release: {e}",
|
| 81 |
+
None,
|
| 82 |
+
)
|
| 83 |
except Exception:
|
| 84 |
return (
|
| 85 |
False,
|
|
|
|
| 104 |
except Exception as e:
|
| 105 |
if "You are trying to access a gated repo." in str(e):
|
| 106 |
return True, "uses a gated model.", None
|
| 107 |
+
return (
|
| 108 |
+
False,
|
| 109 |
+
f"was not found or misconfigured on the hub! Error raised was {e.args[0]}",
|
| 110 |
+
None,
|
| 111 |
+
)
|
| 112 |
|
| 113 |
|
| 114 |
+
def get_model_size(
|
| 115 |
+
model_info: ModelInfo, precision: str, base_model: str | None
|
| 116 |
+
) -> tuple[float | None, str]:
|
| 117 |
size_pattern = re.compile(r"(\d+\.)?\d+(b|m)")
|
| 118 |
safetensors = None
|
| 119 |
adapter_safetensors = None
|
| 120 |
# hack way to check that model is adapter
|
| 121 |
+
is_adapter = "adapter_config.json" in (
|
| 122 |
+
s.rfilename for s in model_info.siblings
|
| 123 |
+
)
|
| 124 |
|
| 125 |
try:
|
| 126 |
if is_adapter:
|
| 127 |
if not base_model:
|
| 128 |
+
return (
|
| 129 |
+
None,
|
| 130 |
+
"Adapter model submission detected. Please ensure the base model information is provided.",
|
| 131 |
+
)
|
| 132 |
|
| 133 |
+
adapter_safetensors = parse_safetensors_file_metadata(
|
| 134 |
+
model_info.id, "adapter_model.safetensors"
|
| 135 |
+
)
|
| 136 |
safetensors = get_safetensors_metadata(base_model)
|
| 137 |
else:
|
| 138 |
safetensors = get_safetensors_metadata(model_info.id)
|
| 139 |
except Exception as e:
|
| 140 |
+
logging.warning(
|
| 141 |
+
f"Failed to get safetensors metadata for model {model_info.id}: {e!s}"
|
| 142 |
+
)
|
| 143 |
|
| 144 |
if safetensors is not None:
|
| 145 |
model_size = sum(safetensors.parameter_count.values())
|
|
|
|
| 151 |
size_match = re.search(size_pattern, model_info.id.lower())
|
| 152 |
if size_match:
|
| 153 |
model_size = size_match.group(0)
|
| 154 |
+
model_size = round(
|
| 155 |
+
float(model_size[:-1])
|
| 156 |
+
if model_size[-1] == "b"
|
| 157 |
+
else float(model_size[:-1]) / 1e3,
|
| 158 |
+
3,
|
| 159 |
+
)
|
| 160 |
else:
|
| 161 |
return None, "Unknown model size"
|
| 162 |
except AttributeError:
|
| 163 |
+
logging.warning(
|
| 164 |
+
f"Unable to parse model size from ID: {model_info.id}"
|
| 165 |
+
)
|
| 166 |
return None, "Unknown model size"
|
| 167 |
|
| 168 |
+
size_factor = (
|
| 169 |
+
8 if (precision == "GPTQ" or "gptq" in model_info.id.lower()) else 1
|
| 170 |
+
)
|
| 171 |
model_size = size_factor * model_size
|
| 172 |
|
| 173 |
return model_size, ""
|
| 174 |
|
| 175 |
+
|
| 176 |
def get_model_arch(model_info: ModelInfo):
|
| 177 |
return model_info.config.get("architectures", "Unknown")
|
| 178 |
|
| 179 |
+
|
| 180 |
def check_chat_template(model: str, revision: str) -> tuple[bool, str]:
|
| 181 |
try:
|
| 182 |
# Attempt to download only the tokenizer_config.json file
|
|
|
|
| 184 |
repo_id=model,
|
| 185 |
filename="tokenizer_config.json",
|
| 186 |
revision=revision,
|
| 187 |
+
repo_type="model",
|
| 188 |
)
|
| 189 |
|
| 190 |
# Read and parse the tokenizer_config.json file
|
| 191 |
+
with open(config_file, "r") as f:
|
| 192 |
tokenizer_config = json.load(f)
|
| 193 |
|
| 194 |
# Check if chat_template exists in the tokenizer configuration
|
| 195 |
+
if "chat_template" not in tokenizer_config:
|
| 196 |
+
return (
|
| 197 |
+
False,
|
| 198 |
+
f"The model {model} doesn't have a chat_template in its tokenizer_config.json. Please add a chat_template before submitting or submit without it.",
|
| 199 |
+
)
|
| 200 |
|
| 201 |
return True, ""
|
| 202 |
except Exception as e:
|
| 203 |
+
return (
|
| 204 |
+
False,
|
| 205 |
+
f"Error checking chat_template for model {model}: {str(e)}",
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
def get_model_tags(model_card, model: str):
|
| 210 |
is_merge_from_metadata = False
|
| 211 |
is_moe_from_metadata = False
|
|
|
|
| 215 |
return tags
|
| 216 |
if model_card.data.tags:
|
| 217 |
is_merge_from_metadata = any(
|
| 218 |
+
[
|
| 219 |
+
tag in model_card.data.tags
|
| 220 |
+
for tag in ["merge", "moerge", "mergekit", "lazymergekit"]
|
| 221 |
+
]
|
| 222 |
+
)
|
| 223 |
+
is_moe_from_metadata = any(
|
| 224 |
+
[tag in model_card.data.tags for tag in ["moe", "moerge"]]
|
| 225 |
)
|
|
|
|
| 226 |
|
| 227 |
is_merge_from_model_card = any(
|
| 228 |
+
keyword in model_card.text.lower()
|
| 229 |
+
for keyword in ["merged model", "merge model", "moerge"]
|
| 230 |
)
|
| 231 |
if is_merge_from_model_card or is_merge_from_metadata:
|
| 232 |
tags.append("merge")
|
| 233 |
+
is_moe_from_model_card = any(
|
| 234 |
+
keyword in model_card.text.lower() for keyword in ["moe", "mixtral"]
|
| 235 |
+
)
|
| 236 |
# Hardcoding because of gating problem
|
| 237 |
if "Qwen/Qwen1.5-32B" in model:
|
| 238 |
is_moe_from_model_card = False
|
| 239 |
+
is_moe_from_name = "moe" in model.lower().replace("/", "-").replace(
|
| 240 |
+
"_", "-"
|
| 241 |
+
).split("-")
|
| 242 |
if is_moe_from_model_card or is_moe_from_name or is_moe_from_metadata:
|
| 243 |
tags.append("moe")
|
| 244 |
|
| 245 |
return tags
|
| 246 |
|
| 247 |
+
|
| 248 |
+
def validate_model(
|
| 249 |
+
model, precision, base_model, weight_type, use_chat_template
|
| 250 |
+
):
|
| 251 |
+
"""
|
| 252 |
+
Validate model with some checkers to assure tha can be evaluated
|
| 253 |
+
:param model: hf model name
|
| 254 |
+
:param precision: model parameters data type
|
| 255 |
+
:param base_model: base model (if it is need it)
|
| 256 |
+
:param weight_type:
|
| 257 |
+
:param use_chat_template:
|
| 258 |
+
:return:
|
| 259 |
+
"""
|
| 260 |
+
API = HfApi()
|
| 261 |
+
|
| 262 |
+
try:
|
| 263 |
+
model_info = API.model_info(repo_id=model, revision="main")
|
| 264 |
+
except:
|
| 265 |
+
return (
|
| 266 |
+
"Could not get your model information. Please fill it up properly."
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Check model size early
|
| 270 |
+
model_size, error_text = get_model_size(
|
| 271 |
+
model_info=model_info, precision=precision, base_model=base_model
|
| 272 |
+
)
|
| 273 |
+
if model_size is None:
|
| 274 |
+
return error_text
|
| 275 |
+
|
| 276 |
+
# Absolute size limit for float16 and bfloat16
|
| 277 |
+
if precision in ["float16", "bfloat16"] and model_size > 100:
|
| 278 |
+
error_message = (
|
| 279 |
+
f"Sadly, models larger than 100B parameters cannot be submitted in {precision} precision at this time. "
|
| 280 |
+
f"Your model size: {model_size:.2f}B parameters."
|
| 281 |
+
)
|
| 282 |
+
return error_message
|
| 283 |
+
|
| 284 |
+
# Precision-adjusted size limit for 8bit, 4bit, and GPTQ
|
| 285 |
+
if precision in ["8bit", "4bit", "GPTQ"]:
|
| 286 |
+
size_checker = ModelSizeChecker(
|
| 287 |
+
model=model, precision=precision, model_size_in_b=model_size
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
if not size_checker.can_evaluate():
|
| 291 |
+
precision_factor = size_checker.get_precision_factor()
|
| 292 |
+
max_size = 140 * precision_factor
|
| 293 |
+
error_message = (
|
| 294 |
+
f"Sadly, models this big ({model_size:.2f}B parameters) cannot be evaluated automatically "
|
| 295 |
+
f"at the moment on our cluster. The maximum size for {precision} precision is {max_size:.2f}B parameters."
|
| 296 |
+
)
|
| 297 |
+
return error_message
|
| 298 |
+
|
| 299 |
+
architecture = "?"
|
| 300 |
+
# Is the model on the hub?
|
| 301 |
+
if weight_type in ["Delta", "Adapter"]:
|
| 302 |
+
base_model_on_hub, error, _ = is_model_on_hub(
|
| 303 |
+
model_name=base_model,
|
| 304 |
+
revision="main",
|
| 305 |
+
token=None,
|
| 306 |
+
test_tokenizer=True,
|
| 307 |
+
)
|
| 308 |
+
if not base_model_on_hub:
|
| 309 |
+
return f'Base model "{base_model}" {error}'
|
| 310 |
+
if not weight_type == "Adapter":
|
| 311 |
+
model_on_hub, error, model_config = is_model_on_hub(
|
| 312 |
+
model_name=model, revision=model_info.sha, test_tokenizer=True
|
| 313 |
+
)
|
| 314 |
+
if not model_on_hub or model_config is None:
|
| 315 |
+
return f'Model "{model}" {error}'
|
| 316 |
+
if model_config is not None:
|
| 317 |
+
architectures = getattr(model_config, "architectures", None)
|
| 318 |
+
if architectures:
|
| 319 |
+
architecture = ";".join(architectures)
|
| 320 |
+
|
| 321 |
+
# Were the model card and license filled?
|
| 322 |
+
try:
|
| 323 |
+
_ = model_info.cardData["license"]
|
| 324 |
+
except Exception:
|
| 325 |
+
return "Please select a license for your model"
|
| 326 |
+
|
| 327 |
+
modelcard_OK, error_msg, model_card = check_model_card(model)
|
| 328 |
+
if not modelcard_OK:
|
| 329 |
+
return error_msg
|
| 330 |
+
|
| 331 |
+
# Check the chat template submission
|
| 332 |
+
if use_chat_template:
|
| 333 |
+
chat_template_valid, chat_template_error = check_chat_template(
|
| 334 |
+
model, "main"
|
| 335 |
+
)
|
| 336 |
+
if not chat_template_valid:
|
| 337 |
+
return chat_template_error
|
| 338 |
+
|
| 339 |
+
return None
|
src/submit.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
-
from transformers import AutoConfig
|
| 2 |
from dataclasses import dataclass
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
@dataclass
|
| 5 |
class ModelSizeChecker:
|
| 6 |
model: str
|
|
|
|
|
|
|
| 1 |
from dataclasses import dataclass
|
| 2 |
|
| 3 |
+
from transformers import AutoConfig
|
| 4 |
+
|
| 5 |
+
|
| 6 |
@dataclass
|
| 7 |
class ModelSizeChecker:
|
| 8 |
model: str
|