Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add log to file of parameters #353

Merged
merged 11 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 83 additions & 5 deletions smartsim/_core/generation/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@
import shutil
import typing as t

from datetime import datetime
from distutils import dir_util # pylint: disable=deprecated-module
from logging import INFO, DEBUG
from os import mkdir, path, symlink
from os.path import join, relpath
from tabulate import tabulate

from ...entity import Model, TaggedFilesHierarchy
from ...log import get_logger
Expand All @@ -49,7 +53,9 @@ class Generator:
and writing into configuration files as well.
"""

def __init__(self, gen_path: str, overwrite: bool = False) -> None:
def __init__(
self, gen_path: str, overwrite: bool = False, verbose: bool = True
) -> None:
"""Initialize a generator object

if overwrite is true, replace any existing
Expand All @@ -59,12 +65,28 @@ def __init__(self, gen_path: str, overwrite: bool = False) -> None:
is false, raises EntityExistsError when there is a name
collision between entities.

:param gen_path: Path in which files need to be generated
:type gen_path: str
:param overwrite: toggle entity replacement, defaults to False
:type overwrite: bool, optional
:param verbose: Whether generation information should be logged to std out
:type verbose: bool, optional
"""
self._writer = ModelWriter()
self.gen_path = gen_path
self.overwrite = overwrite
self.log_level = DEBUG if not verbose else INFO

@property
def log_file(self) -> str:
"""Returns the location of the file
summarizing the parameters used for the last generation
of all generated entities.

:returns: path to file with parameter settings
:rtype: str
"""
return join(self.gen_path, "smartsim_params.txt")

def generate_experiment(self, *args: t.Any) -> None:
"""Run ensemble and experiment file structure generation
Expand Down Expand Up @@ -129,7 +151,17 @@ def _gen_exp_dir(self) -> None:
# keep exists ok for race conditions on NFS
pathlib.Path(self.gen_path).mkdir(exist_ok=True)
else:
logger.info("Working in previously created experiment")
logger.log(
level=self.log_level, msg="Working in previously created experiment"
)

# The log_file only keeps track of the last generation
# this is to avoid gigantic files in case the user repeats
# generation several times. The information is anyhow
# redundant, as it is also written in each entity's dir
with open(self.log_file, mode= 'w', encoding='utf-8') as log_file:
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
log_file.write(f"Generation start date and time: {dt_string}\n")

def _gen_orc_dir(self, orchestrator: t.Optional[Orchestrator]) -> None:
"""Create the directory that will hold the error, output and
Expand Down Expand Up @@ -249,10 +281,56 @@ def _build_tagged_files(tagged: TaggedFilesHierarchy) -> None:

# write in changes to configurations
if isinstance(entity, Model):
logger.debug(
f"Configuring model {entity.name} with params {entity.params}"
files_to_params = self._writer.configure_tagged_model_files(
to_write, entity.params
)
self._log_params(entity, files_to_params)

def _log_params(
self, entity: Model, files_to_params: t.Dict[str, t.Dict[str, str]]
) -> None:
"""Log which files were modified during generation

and what values were set to the parameters

:param entity: the model being generated
:type entity: Model
:param files_to_params: a dict connecting each file to its parameter settings
:type files_to_params: t.Dict[str, t.Dict[str, str]]
"""
used_params: t.Dict[str, str] = {}
file_to_tables: t.Dict[str, str] = {}
for file, params in files_to_params.items():
used_params.update(params)
table = tabulate(params.items(),
headers=["Name", "Value"])
file_to_tables[relpath(file, self.gen_path)] = table

if used_params:
used_params_str = ", ".join(
[f"{name}={value}" for name, value in used_params.items()]
)
logger.log(
level=self.log_level,
msg=f"Configured model {entity.name} with params {used_params_str}",
)
file_table = tabulate(
file_to_tables.items(),
headers=["File name", "Parameters"],
)
self._writer.configure_tagged_model_files(to_write, entity.params)
log_entry = f"Model name: {entity.name}\n{file_table}\n\n"
with open(self.log_file, mode="a", encoding="utf-8") as logfile:
logfile.write(log_entry)
with open(join(entity.path, "smartsim_params.txt"),
mode="w",
encoding="utf-8") as local_logfile:
local_logfile.write(log_entry)

else:
logger.log(
level=self.log_level,
msg=f"Configured model {entity.name} with no parameters",
)

@staticmethod
def _copy_entity_files(entity: Model) -> None:
Expand Down
27 changes: 19 additions & 8 deletions smartsim/_core/generation/modelwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,27 @@ def configure_tagged_model_files(
tagged_files: t.List[str],
params: t.Dict[str, str],
make_missing_tags_fatal: bool = False,
) -> None:
) -> t.Dict[str, t.Dict[str, str]]:
"""Read, write and configure tagged files attached to a Model
instance.

:param tagged_files: list of paths to tagged files
:type model: list[str]
:param params: model parameters
:type params: dict[str, str]
:param make_missing_tags_fatal: blow up if a tag is missing
:param make_missing_tags_fatal: raise an error if a tag is missing
:type make_missing_tags_fatal: bool
:returns: A dict connecting each file to its parameter settings
:rtype: dict[str,dict[str,str]]
"""
files_to_tags: t.Dict[str, t.Dict[str, str]] = {}
for tagged_file in tagged_files:
self._set_lines(tagged_file)
self._replace_tags(params, make_missing_tags_fatal)
used_tags = self._replace_tags(params, make_missing_tags_fatal)
self._write_changes(tagged_file)
files_to_tags[tagged_file] = used_tags

return files_to_tags

def _set_lines(self, file_path: str) -> None:
"""Set the lines for the modelwrtter to iterate over
Expand All @@ -104,18 +110,23 @@ def _write_changes(self, file_path: str) -> None:
except (IOError, OSError) as e:
raise ParameterWriterError(file_path, read=False) from e

def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> None:
"""Replace the tagged within the tagged file attached to this
def _replace_tags(
self, params: t.Dict[str, str], make_fatal: bool = False
) -> t.Dict[str, str]:
"""Replace the tagged parameters within the file attached to this
model. The tag defaults to ";"

:param model: The model instance
:type model: Model
:param make_fatal: (Optional) Set to True to force a fatal error
if a tag is not matched
:type make_fatal: bool
:returns: A dict of parameter names and values set for the file
:rtype: dict[str,str]
"""
edited = []
unused_tags: t.Dict[str, t.List[int]] = {}
used_params: t.Dict[str, str] = {}
for i, line in enumerate(self.lines):
search = re.search(self.regex, line)
if search:
Expand All @@ -126,6 +137,7 @@ def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> N
new_val = str(params[previous_value])
new_line = re.sub(self.regex, new_val, line, 1)
search = re.search(self.regex, new_line)
used_params[previous_value] = new_val
if not search:
edited.append(new_line)
else:
Expand All @@ -143,13 +155,12 @@ def _replace_tags(self, params: t.Dict[str, str], make_fatal: bool = False) -> N
else:
edited.append(line)
for tag, value in unused_tags.items():
missing_tag_message = (
f"Unused tag {tag} on line(s): {str(value)}"
)
missing_tag_message = f"Unused tag {tag} on line(s): {str(value)}"
if make_fatal:
raise SmartSimError(missing_tag_message)
logger.warning(missing_tag_message)
self.lines = edited
return used_params

def _is_ensemble_spec(
self, tagged_line: str, model_params: t.Dict[str, str]
Expand Down
48 changes: 31 additions & 17 deletions smartsim/entity/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@
import sys
import typing as t
import warnings
from os import path as osp

from .._core.utils.helpers import cat_arg_and_value, init_default
from ..error import EntityExistsError, SSUnsupportedError
from ..log import get_logger
from ..settings.base import BatchSettings, RunSettings
from .dbobject import DBModel, DBScript
from .entity import SmartSimEntity
from .files import EntityFiles
from ..settings.base import BatchSettings, RunSettings
from ..log import get_logger


logger = get_logger(__name__)


class Model(SmartSimEntity):
def __init__(
self,
Expand Down Expand Up @@ -163,6 +164,20 @@ def attach_generator_files(
to_copy = init_default([], to_copy, (list, str))
to_symlink = init_default([], to_symlink, (list, str))
to_configure = init_default([], to_configure, (list, str))

# Check that no file collides with the parameter file written
# by Generator. We check the basename, even though it is more
# restrictive than what we need (but it avoids relative path issues)
for strategy in [to_copy, to_symlink, to_configure]:
if strategy is not None and any(
osp.basename(filename) == "smartsim_params.txt"
for filename in strategy
):
raise ValueError(
"`smartsim_params.txt` is a file automatically "
+ "generated by SmartSim and cannot be ovewritten."
)

self.files = EntityFiles(to_configure, to_copy, to_symlink)

@property
Expand All @@ -177,8 +192,7 @@ def attached_files_table(self) -> str:
return str(self.files)

def print_attached_files(self) -> None:
"""Print a table of the attached files on std out
"""
"""Print a table of the attached files on std out"""
print(self.attached_files_table)

def colocate_db(self, *args: t.Any, **kwargs: t.Any) -> None:
Expand All @@ -187,7 +201,8 @@ def colocate_db(self, *args: t.Any, **kwargs: t.Any) -> None:
(
"`colocate_db` has been deprecated and will be removed in a \n"
"future release. Please use `colocate_db_tcp` or `colocate_db_uds`."
), FutureWarning
),
FutureWarning,
)
self.colocate_db_tcp(*args, **kwargs)

Expand Down Expand Up @@ -333,8 +348,7 @@ def _set_colocated_db_settings(

# TODO list which db settings can be extras
common_options["custom_pinning"] = self._create_pinning_string(
common_options["custom_pinning"],
common_options["cpus"]
common_options["custom_pinning"], common_options["cpus"]
)

colo_db_config = {}
Expand All @@ -358,13 +372,13 @@ def _set_colocated_db_settings(

@staticmethod
def _create_pinning_string(
pin_ids: t.Optional[t.Iterable[t.Union[int, t.Iterable[int]]]],
cpus: int
) -> t.Optional[str]:
pin_ids: t.Optional[t.Iterable[t.Union[int, t.Iterable[int]]]], cpus: int
) -> t.Optional[str]:
"""Create a comma-separated string CPU ids. By default, None returns
0,1,...,cpus-1; an empty iterable will disable pinning altogether,
and an iterable constructs a comma separate string (e.g. 0,2,5)
"""

def _stringify_id(_id: int) -> str:
"""Return the cPU id as a string if an int, otherwise raise a ValueError"""
if isinstance(_id, int):
Expand All @@ -389,14 +403,14 @@ def _stringify_id(_id: int) -> str:
warnings.warn(
"CPU pinning is not supported on MacOSX. Ignoring pinning "
"specification.",
RuntimeWarning
RuntimeWarning,
)
return None
raise TypeError(_invalid_input_message)
# Flatten the iterable into a list and check to make sure that the resulting
# elements are all ints
if pin_ids is None:
return ','.join(_stringify_id(i) for i in range(cpus))
return ",".join(_stringify_id(i) for i in range(cpus))
if not pin_ids:
return None
if isinstance(pin_ids, collections.abc.Iterable):
Expand All @@ -406,7 +420,7 @@ def _stringify_id(_id: int) -> str:
pin_list.extend([_stringify_id(j) for j in pin_id])
else:
pin_list.append(_stringify_id(pin_id))
return ','.join(sorted(set(pin_list)))
return ",".join(sorted(set(pin_list)))
raise TypeError(_invalid_input_message)

def params_to_args(self) -> None:
Expand All @@ -433,7 +447,7 @@ def add_ml_model(
backend: str,
model: t.Optional[str] = None,
model_path: t.Optional[str] = None,
device: t.Literal["CPU","GPU"] = "CPU",
device: t.Literal["CPU", "GPU"] = "CPU",
devices_per_node: int = 1,
batch_size: int = 0,
min_batch_size: int = 0,
Expand Down Expand Up @@ -495,7 +509,7 @@ def add_script(
name: str,
script: t.Optional[str] = None,
script_path: t.Optional[str] = None,
device: t.Literal["CPU","GPU"] = "CPU",
device: t.Literal["CPU", "GPU"] = "CPU",
devices_per_node: int = 1,
) -> None:
"""TorchScript to launch with this Model instance
Expand Down Expand Up @@ -539,7 +553,7 @@ def add_function(
self,
name: str,
function: t.Optional[str] = None,
device: t.Literal["CPU","GPU"] = "CPU",
device: t.Literal["CPU", "GPU"] = "CPU",
devices_per_node: int = 1,
) -> None:
"""TorchScript function to launch with this Model instance
Expand Down
10 changes: 8 additions & 2 deletions smartsim/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@
raise

def generate(
self, *args: t.Any, tag: t.Optional[str] = None, overwrite: bool = False
self,

Check warning on line 234 in smartsim/experiment.py

View check run for this annotation

Codecov / codecov/patch

smartsim/experiment.py#L234

Added line #L234 was not covered by tests
*args: t.Any,
tag: t.Optional[str] = None,
overwrite: bool = False,
verbose: bool = False,
) -> None:
"""Generate the file structure for an ``Experiment``

Expand All @@ -251,9 +255,11 @@
:param overwrite: overwrite existing folders and contents,
defaults to False
:type overwrite: bool, optional
:param verbose: log parameter settings to std out
:type verbose: bool
"""
try:
generator = Generator(self.exp_path, overwrite=overwrite)
generator = Generator(self.exp_path, overwrite=overwrite, verbose=verbose)
if tag:
generator.set_tag(tag)
generator.generate_experiment(*args)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Model name: dir_test_0
File name Parameters
-------------------------- ---------------
dir_test/dir_test_0/in.atm Name Value
------ -------
THERMO 10
STEPS 10

Loading
Loading