Esempio n. 1
0
class Arena(object):
    """A wrapper around arena that adds W&B config options"""
    def __init__(self,
                 args,
                 wandb_project=None,
                 wandb_api_key=None,
                 wandb_run_id=None,
                 timeout_minutes=10):
        self.api = InternalApi()
        self.args = args
        self.wandb_run_id = wandb_run_id or _short_id()
        self.wandb_project = wandb_project
        self.wandb_api_key = wandb_api_key or self.api.api_key
        self.timeout_minutes = timeout_minutes
        self.workers = int(self._parse_flag("--workers", 1)[1] or "0")
        self.entity = None

    def _parse_flag(self, flag, default=-1):
        index = next((i for i, arg in enumerate(self.args)
                      if re.match(r"{}[= ]".format(flag), arg)), default)
        if index > -1 and len(self.args) > index:
            if "=" in self.args[index]:
                val = self.args[index].split("=", 1)[1]
            elif " " in self.args[index]:
                val = self.args[index].split(" ", 1)[1]
            else:
                val = True
        else:
            val = None
        return index, val

    def submit(self):
        try:
            from minio import Minio
            from google.cloud import storage
        except ImportError:
            raise ValueError(
                "Required libraries for kubeflow aren't installed, run `pip install wandb[kubeflow]`"
            )

        print('Submitting arena {} job 🚀'.format(self.args[0]))

        # TODO: require command?
        opt_index, _ = self._parse_flag("--", len(self.args) - 1)
        name_index, name = self._parse_flag("--name")
        if name_index == -1:
            name = "wandb"
            name_index = len(self.args) - 1
            self.args.insert(name_index, None)
        name = '-'.join([name, _short_id(5)])
        self.args[name_index] = "--name=" + name

        projo = self.wandb_project or self.api.settings("project")
        if projo:
            if "/" in projo:
                ent, projo = projo.split("/")
                self.args.insert(opt_index,
                                 "--env=WANDB_ENTITY={}".format(ent))
        else:
            _, git = self._parse_flag("--syncSource")
            _, image = self._parse_flag("--image")
            if git:
                projo = git.split("/")[-1].replace(".git", "")
            elif image:
                projo = image.split(":")[0]
            if projo:
                projo = self.api.format_project(projo)
                self.args.insert(opt_index,
                                 "--env=WANDB_PROJECT={}".format(projo))

        if self.wandb_api_key:
            self.args.insert(
                opt_index, "--env=WANDB_API_KEY={}".format(self.wandb_api_key))
        else:
            # Extract the secret, ideally this would be a secret env in the TFjob YAML
            try:
                kube_args = {"o": "json"}
                index, namespace = self._parse_flag("--namespace")
                if namespace:
                    kube_args["namespace"] = namespace
                secret = json.loads(
                    str(sh.kubectl("get", "secret", "wandb", **kube_args)))
            except sh.ErrorReturnCode:
                secret = {}
            if secret.get("data"):
                print("Found wandb k8s secret, adding to environment")
                api_key = secret["data"].get("api_key")
                if api_key:
                    self.args.insert(
                        opt_index, "--env=WANDB_API_KEY=" +
                        base64.b64decode(api_key).decode("utf8"))
                    self.wandb_api_key = api_key
        if self.wandb_api_key:
            try:
                # TODO: support someone overriding entity
                if self.workers <= 1:
                    res = self.api.upsert_run(name=self.wandb_run_id,
                                              project=projo)
                    wandb_run_path = os.path.join(
                        res["project"]["entity"]["name"],
                        res["project"]["name"], "runs", res["name"])
                    print(
                        'Run configured with W&B\nview live results here: {}'.
                        format("https://app.wandb.ai/" + wandb_run_path))
                    self.args.insert(
                        opt_index, "--env=WANDB_RUN_ID={}".format(res["name"]))
                    self.args.insert(opt_index, "--env=WANDB_RESUME=allow")
                else:
                    res = self.api.viewer()
                    self.args.insert(opt_index,
                                     "--env=WANDB_RUN_GROUP=" + name)
                    wandb_run_path = os.path.join(res["entity"], projo,
                                                  "groups", name)
                    print(
                        'Distributed run configured with W&B\nview live results here: {}'
                        .format("https://app.wandb.ai/" + wandb_run_path))
            except CommError:
                print("Failed to talk to W&B")
        else:
            print(
                "Couldn't authenticate with W&B, run `wandb login` on your local machine"
            )
        index, gcs_url = self._parse_flag("--logdir")
        tensorboard = self._parse_flag("--tensorboard")[0] > -1
        if gcs_url and wandb_run_path:
            pipeline_metadata(gcs_url, wandb_run_path, tensorboard)
        elif wandb_run_path:
            print("--logdir isn't set, skipping pipeline asset saving.")
        cmd = arena(["submit"] + self.args)
        print("Arena job {} submitted, watching state for upto {} minutes".
              format(name, self.timeout_minutes))
        total_time = 0
        poll_rate = 10
        while True:
            # TODO: parse JSON when it's supported
            status = str(arena("get", name)).split("\n")
            rows = [
                row for row in (re.split(r"\s+", row) for row in status)
                if len(row) == 6 and "s" in row[3]
            ]
            if len(rows) <= 1:
                print("Final status: ", rows)
                break
            status = [row[1] for row in rows[1:]]
            runtime = [row[3] for row in rows[1:]]
            print("Status: {} {}".format(status, runtime))
            if not all([s in ("PENDING", "RUNNING") for s in status]):
                if not any([s in ("PENDING", "RUNNING") for s in status]):
                    print("Job finished with statuses: {}".format(status))
                    if any([s == "FAILED" for s in status]):
                        arena("logs", name, _fg=True)
                    break
            time.sleep(poll_rate)
            total_time += 10
            if total_time > 90:
                poll_rate = 30
            if total_time > self.timeout_minutes * 60:
                print("Timeout exceeded")
Esempio n. 2
0
class Artifact(object):
    """An artifact object you can write files into, and pass to log_artifact."""

    def __init__(self, name, type, description=None, metadata=None):
        if not re.match("^[a-zA-Z0-9_\-.]+$", name):
            raise ValueError(
                'Artifact name may only contain alphanumeric characters, dashes, underscores, and dots. Invalid name: "%s"'
                % name
            )
        # TODO: this shouldn't be a property of the artifact. It's a more like an
        # argument to log_artifact.
        storage_layout = StorageLayout.V2
        if env.get_use_v1_artifacts():
            storage_layout = StorageLayout.V1

        self._storage_policy = WandbStoragePolicy(
            config={
                "storageLayout": storage_layout,
                #  TODO: storage region
            }
        )
        self._api = InternalApi()
        self._final = False
        self._digest = None
        self._file_entries = None
        self._manifest = ArtifactManifestV1(self, self._storage_policy)
        self._cache = get_artifacts_cache()
        self._added_new = False
        self._added_objs = {}
        # You can write into this directory when creating artifact files
        self._artifact_dir = compat_tempfile.TemporaryDirectory(
            missing_ok_on_cleanup=True
        )
        self.type = type
        self.name = name
        self.description = description
        self.metadata = metadata

    @property
    def id(self):
        # The artifact hasn't been saved so an ID doesn't exist yet.
        return None

    @property
    def entity(self):
        # TODO: querying for default entity a good idea here?
        return self._api.settings("entity") or self._api.viewer().get("entity")

    @property
    def project(self):
        return self._api.settings("project")

    @property
    def manifest(self):
        self.finalize()
        return self._manifest

    @property
    def digest(self):
        self.finalize()
        # Digest will be none if the artifact hasn't been saved yet.
        return self._digest

    def _ensure_can_add(self):
        if self._final:
            raise ValueError("Can't add to finalized artifact.")

    def new_file(self, name, mode="w"):
        self._ensure_can_add()
        path = os.path.join(self._artifact_dir.name, name.lstrip("/"))
        if os.path.exists(path):
            raise ValueError('File with name "%s" already exists' % name)
        util.mkdir_exists_ok(os.path.dirname(path))
        self._added_new = True
        return open(path, mode)

    def add_file(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isfile(local_path):
            raise ValueError("Path is not a file: %s" % local_path)

        name = name or os.path.basename(local_path)
        entry = ArtifactManifestEntry(
            name,
            None,
            digest=md5_file_b64(local_path),
            size=os.path.getsize(local_path),
            local_path=local_path,
        )
        self._manifest.add_entry(entry)
        return entry

    def add_dir(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isdir(local_path):
            raise ValueError("Path is not a directory: %s" % local_path)

        termlog(
            "Adding directory to artifact (%s)... "
            % os.path.join(".", os.path.normpath(local_path)),
            newline=False,
        )
        start_time = time.time()

        paths = []
        for dirpath, _, filenames in os.walk(local_path, followlinks=True):
            for fname in filenames:
                physical_path = os.path.join(dirpath, fname)
                logical_path = os.path.relpath(physical_path, start=local_path)
                if name is not None:
                    logical_path = os.path.join(name, logical_path)
                paths.append((logical_path, physical_path))

        def add_manifest_file(log_phy_path):
            logical_path, physical_path = log_phy_path
            self._manifest.add_entry(
                ArtifactManifestEntry(
                    logical_path,
                    None,
                    digest=md5_file_b64(physical_path),
                    size=os.path.getsize(physical_path),
                    local_path=physical_path,
                )
            )

        import multiprocessing.dummy  # this uses threads

        NUM_THREADS = 8
        pool = multiprocessing.dummy.Pool(NUM_THREADS)
        pool.map(add_manifest_file, paths)
        pool.close()
        pool.join()

        termlog("Done. %.1fs" % (time.time() - start_time), prefix=False)

    def add_reference(self, uri, name=None, checksum=True, max_objects=None):
        if (
            isinstance(uri, object)
            and hasattr(uri, "parent_artifact")
            and uri.parent_artifact != self
        ):
            ref_url_fn = getattr(uri, "ref_url")
            if callable(ref_url_fn):
                uri = ref_url_fn()
        url = urlparse(uri)
        if not url.scheme:
            raise ValueError(
                "References must be URIs. To reference a local file, use file://"
            )
        if self._final:
            raise ValueError("Can't add to finalized artifact.")
        manifest_entries = self._storage_policy.store_reference(
            self, uri, name=name, checksum=checksum, max_objects=max_objects
        )
        for entry in manifest_entries:
            self._manifest.add_entry(entry)

        return manifest_entries

    # TODO: name this add_obj?
    def add(self, obj, name):
        """adds `obj` to the artifact, located at `name`. You can use Artifact#get(`name`) after downloading
        the artifact to retrieve this object.
        
        Arguments:
        - `obj`:wandb.Media - the object to save
        - `name`:str - the path to save
        """
        if isinstance(obj, Media):

            # If the object is coming from another artifact, save it as a reference
            if hasattr(obj, "_source") and obj._source is not None:
                suffix = "." + obj.get_json_suffix() + ".json"
                ref_path = obj._source["artifact"].get_path(
                    obj._source["name"] + suffix
                )
                path = name + suffix
                return self.add_reference(ref_path, path)[0]

            # Otherwise, save the object directly via json
            elif type(obj) in JSONABLE_MEDIA_CLASSES:
                obj_id = id(obj)
                if obj_id in self._added_objs:
                    return self._added_objs[obj_id]
                val = obj.to_json(self)
                if "_type" not in val:
                    val["_type"] = obj.get_json_suffix()
                suffix = val["_type"] + ".json"
                if not name.endswith(suffix):
                    name = name + "." + suffix
                entry = self._manifest.get_entry_by_path(name)
                if entry is not None:
                    return entry
                with self.new_file(name) as f:
                    import json

                    # TODO: Do we need to open with utf-8 codec?
                    f.write(json.dumps(obj.to_json(self), sort_keys=True))
                # Note, we add the file from our temp directory.
                # It will be added again later on finalize, but succeed since
                # the checksum should match
                entry = self.add_file(os.path.join(self._artifact_dir.name, name), name)
                self._added_objs[obj_id] = entry
                return entry
            else:
                ValueError("Can't add obj of type {} to artifact".format(type(obj)))
        else:
            raise ValueError("Can't add obj to artifact")

    def get_added_local_path_name(self, local_path):
        """If local_path was already added to artifact, return its internal name."""
        entry = self._manifest.get_entry_by_local_path(local_path)
        if entry is None:
            return None
        return entry.path

    def get_path(self, name):
        raise ValueError("Cannot load paths from an artifact before it has been saved")

    def download(self):
        raise ValueError("Cannot call download on an artifact before it has been saved")

    def finalize(self):
        if self._final:
            return self._file_entries

        # Record any created files in the manifest.
        if self._added_new:
            self.add_dir(self._artifact_dir.name)

        # mark final after all files are added
        self._final = True
        self._digest = self._manifest.digest()

        # If there are new files, move them into the artifact cache now. Our temp
        # self._artifact_dir may not be available by the time file pusher syncs
        # these files.
        if self._added_new:
            # Update the file entries for new files to point at their new location.
            def remap_entry(entry):
                if entry.local_path is None or not entry.local_path.startswith(
                    self._artifact_dir.name
                ):
                    return entry
                rel_path = os.path.relpath(
                    entry.local_path, start=self._artifact_dir.name
                )
                local_path = os.path.join(self._artifact_dir.name, rel_path)
                cache_path, hit = self._cache.check_md5_obj_path(
                    entry.digest, entry.size
                )
                if not hit:
                    shutil.copyfile(local_path, cache_path)
                entry.local_path = cache_path

            for entry in self._manifest.entries.values():
                remap_entry(entry)
Esempio n. 3
0
class Artifact(object):
    """An artifact object you can write files into, and pass to log_artifact."""
    def __init__(self, name, type, description=None, metadata=None):
        if not re.match("^[a-zA-Z0-9_\-.]+$", name):
            raise ValueError(
                'Artifact name may only contain alphanumeric characters, dashes, underscores, and dots. Invalid name: "%s"'
                % name)
        # TODO: this shouldn't be a property of the artifact. It's a more like an
        # argument to log_artifact.
        storage_layout = StorageLayout.V2
        if env.get_use_v1_artifacts():
            storage_layout = StorageLayout.V1

        self._storage_policy = WandbStoragePolicy(
            config={
                "storageLayout": storage_layout,
                #  TODO: storage region
            })
        self._api = InternalApi()
        self._final = False
        self._digest = None
        self._file_entries = None
        self._manifest = ArtifactManifestV1(self, self._storage_policy)
        self._cache = get_artifacts_cache()
        self._added_new = False
        # You can write into this directory when creating artifact files
        self._artifact_dir = compat_tempfile.TemporaryDirectory(
            missing_ok_on_cleanup=True)
        self.type = type
        self.name = name
        self.description = description
        self.metadata = metadata

    @property
    def id(self):
        # The artifact hasn't been saved so an ID doesn't exist yet.
        return None

    @property
    def entity(self):
        # TODO: querying for default entity a good idea here?
        return self._api.settings("entity") or self._api.viewer().get("entity")

    @property
    def project(self):
        return self._api.settings("project")

    @property
    def manifest(self):
        self.finalize()
        return self._manifest

    @property
    def digest(self):
        self.finalize()
        # Digest will be none if the artifact hasn't been saved yet.
        return self._digest

    def _ensure_can_add(self):
        if self._final:
            raise ValueError("Can't add to finalized artifact.")

    def new_file(self, name, mode="w"):
        self._ensure_can_add()
        path = os.path.join(self._artifact_dir.name, name.lstrip("/"))
        if os.path.exists(path):
            raise ValueError('File with name "%s" already exists' % name)
        util.mkdir_exists_ok(os.path.dirname(path))
        self._added_new = True
        return open(path, mode)

    def add_file(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isfile(local_path):
            raise ValueError("Path is not a file: %s" % local_path)

        name = name or os.path.basename(local_path)
        entry = ArtifactManifestEntry(
            name,
            None,
            digest=md5_file_b64(local_path),
            size=os.path.getsize(local_path),
            local_path=local_path,
        )
        self._manifest.add_entry(entry)

    def add_dir(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isdir(local_path):
            raise ValueError("Path is not a directory: %s" % local_path)

        termlog(
            "Adding directory to artifact (%s)... " %
            os.path.join(".", os.path.normpath(local_path)),
            newline=False,
        )
        start_time = time.time()

        paths = []
        for dirpath, _, filenames in os.walk(local_path, followlinks=True):
            for fname in filenames:
                physical_path = os.path.join(dirpath, fname)
                logical_path = os.path.relpath(physical_path, start=local_path)
                if name is not None:
                    logical_path = os.path.join(name, logical_path)
                paths.append((logical_path, physical_path))

        def add_manifest_file(log_phy_path):
            logical_path, physical_path = log_phy_path
            self._manifest.add_entry(
                ArtifactManifestEntry(
                    logical_path,
                    None,
                    digest=md5_file_b64(physical_path),
                    size=os.path.getsize(physical_path),
                    local_path=physical_path,
                ))

        import multiprocessing.dummy  # this uses threads

        NUM_THREADS = 8
        pool = multiprocessing.dummy.Pool(NUM_THREADS)
        pool.map(add_manifest_file, paths)
        pool.close()
        pool.join()

        termlog("Done. %.1fs" % (time.time() - start_time), prefix=False)

    def add_reference(self, uri, name=None, checksum=True, max_objects=None):
        url = urlparse(uri)
        if not url.scheme:
            raise ValueError(
                "References must be URIs. To reference a local file, use file://"
            )
        if self._final:
            raise ValueError("Can't add to finalized artifact.")
        manifest_entries = self._storage_policy.store_reference(
            self, uri, name=name, checksum=checksum, max_objects=max_objects)
        for entry in manifest_entries:
            self._manifest.add_entry(entry)

    def get_path(self, name):
        raise ValueError(
            "Cannot load paths from an artifact before it has been saved")

    def download(self):
        raise ValueError(
            "Cannot call download on an artifact before it has been saved")

    def finalize(self):
        if self._final:
            return self._file_entries

        # Record any created files in the manifest.
        if self._added_new:
            self.add_dir(self._artifact_dir.name)

        # mark final after all files are added
        self._final = True
        self._digest = self._manifest.digest()

        # If there are new files, move them into the artifact cache now. Our temp
        # self._artifact_dir may not be available by the time file pusher syncs
        # these files.
        if self._added_new:
            # Update the file entries for new files to point at their new location.
            def remap_entry(entry):
                if entry.local_path is None or not entry.local_path.startswith(
                        self._artifact_dir.name):
                    return entry
                rel_path = os.path.relpath(entry.local_path,
                                           start=self._artifact_dir.name)
                local_path = os.path.join(self._artifact_dir.name, rel_path)
                cache_path, hit = self._cache.check_md5_obj_path(
                    entry.digest, entry.size)
                if not hit:
                    shutil.copyfile(local_path, cache_path)
                entry.local_path = cache_path

            for entry in self._manifest.entries.values():
                remap_entry(entry)
Esempio n. 4
0
class Artifact(object):
    """An artifact object you can write files into, and pass to log_artifact."""
    def __init__(self, name, type, description=None, metadata=None):
        if not re.match(r"^[a-zA-Z0-9_\-.]+$", name):
            raise ValueError(
                'Artifact name may only contain alphanumeric characters, dashes, underscores, and dots. Invalid name: "%s"'
                % name)
        # TODO: this shouldn't be a property of the artifact. It's a more like an
        # argument to log_artifact.
        storage_layout = StorageLayout.V2
        if env.get_use_v1_artifacts():
            storage_layout = StorageLayout.V1

        self._storage_policy = WandbStoragePolicy(
            config={
                "storageLayout": storage_layout,
                #  TODO: storage region
            })
        self._api = InternalApi()
        self._final = False
        self._digest = None
        self._file_entries = None
        self._manifest = ArtifactManifestV1(self, self._storage_policy)
        self._cache = get_artifacts_cache()
        self._added_new = False
        self._added_objs = {}
        # You can write into this directory when creating artifact files
        self._artifact_dir = compat_tempfile.TemporaryDirectory(
            missing_ok_on_cleanup=True)
        self.type = type
        self.name = name
        self.description = description
        self.metadata = metadata

    @property
    def id(self):
        # The artifact hasn't been saved so an ID doesn't exist yet.
        return None

    @property
    def entity(self):
        # TODO: querying for default entity a good idea here?
        return self._api.settings("entity") or self._api.viewer().get("entity")

    @property
    def project(self):
        return self._api.settings("project")

    @property
    def manifest(self):
        self.finalize()
        return self._manifest

    @property
    def digest(self):
        self.finalize()
        # Digest will be none if the artifact hasn't been saved yet.
        return self._digest

    def _ensure_can_add(self):
        if self._final:
            raise ValueError("Can't add to finalized artifact.")

    def new_file(self, name, mode="w"):
        self._ensure_can_add()
        path = os.path.join(self._artifact_dir.name, name.lstrip("/"))
        if os.path.exists(path):
            raise ValueError('File with name "%s" already exists at "%s"' %
                             (name, path))
        util.mkdir_exists_ok(os.path.dirname(path))
        self._added_new = True
        return open(path, mode)

    def add_file(self, local_path, name=None, is_tmp=False):
        """Adds a local file to the artifact

        Args:
            local_path (str): path to the file
            name (str, optional): new path and filename to assign inside artifact. Defaults to None.
            is_tmp (bool, optional): If true, then the file is renamed deterministically. Defaults to False.

        Returns:
            ArtifactManifestEntry: the added entry
        """
        self._ensure_can_add()
        if not os.path.isfile(local_path):
            raise ValueError("Path is not a file: %s" % local_path)

        name = name or os.path.basename(local_path)
        digest = md5_file_b64(local_path)

        if is_tmp:
            file_path, file_name = os.path.split(name)
            file_name_parts = file_name.split(".")
            file_name_parts[0] = b64_string_to_hex(digest)[:8]
            name = os.path.join(file_path, ".".join(file_name_parts))

        entry = ArtifactManifestEntry(
            name,
            None,
            digest=digest,
            size=os.path.getsize(local_path),
            local_path=local_path,
        )

        self._manifest.add_entry(entry)
        return entry

    def add_dir(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isdir(local_path):
            raise ValueError("Path is not a directory: %s" % local_path)

        termlog(
            "Adding directory to artifact (%s)... " %
            os.path.join(".", os.path.normpath(local_path)),
            newline=False,
        )
        start_time = time.time()

        paths = []
        for dirpath, _, filenames in os.walk(local_path, followlinks=True):
            for fname in filenames:
                physical_path = os.path.join(dirpath, fname)
                logical_path = os.path.relpath(physical_path, start=local_path)
                if name is not None:
                    logical_path = os.path.join(name, logical_path)
                paths.append((logical_path, physical_path))

        def add_manifest_file(log_phy_path):
            logical_path, physical_path = log_phy_path
            self._manifest.add_entry(
                ArtifactManifestEntry(
                    logical_path,
                    None,
                    digest=md5_file_b64(physical_path),
                    size=os.path.getsize(physical_path),
                    local_path=physical_path,
                ))

        import multiprocessing.dummy  # this uses threads

        NUM_THREADS = 8
        pool = multiprocessing.dummy.Pool(NUM_THREADS)
        pool.map(add_manifest_file, paths)
        pool.close()
        pool.join()

        termlog("Done. %.1fs" % (time.time() - start_time), prefix=False)

    def add_reference(self, uri, name=None, checksum=True, max_objects=None):
        """adds `uri` to the artifact via a reference, located at `name`. 
        You can use `Artifact.get_path(name)` to retrieve this object.
        
        Arguments:
            uri (str) - the URI path of the reference to add. Can be an object returned from
                Artifact.get_path to store a reference to another artifact's entry.
            name (str) - the path to save
        """

        # This is a bit of a hack, we want to check if the uri is a of the type
        # ArtifactEntry which is a private class returned by Artifact.get_path in
        # wandb/apis/public.py. If so, then recover the reference URL.
        if (isinstance(uri, object) and hasattr(uri, "parent_artifact")
                and uri.parent_artifact != self):
            ref_url_fn = getattr(uri, "ref_url")
            uri = ref_url_fn()
        url = urlparse(uri)
        if not url.scheme:
            raise ValueError(
                "References must be URIs. To reference a local file, use file://"
            )
        if self._final:
            raise ValueError("Can't add to finalized artifact.")
        manifest_entries = self._storage_policy.store_reference(
            self, uri, name=name, checksum=checksum, max_objects=max_objects)
        for entry in manifest_entries:
            self._manifest.add_entry(entry)

        return manifest_entries

    def add(self, obj, name):
        """Adds `obj` to the artifact, located at `name`. You can
        use `Artifact.get(name)` after downloading the artifact to retrieve this object.
        
        Arguments:
            obj (wandb.WBValue): The object to save in an artifact
            name (str): The path to save
        """

        # Validate that the object is wandb.Media type
        if not isinstance(obj, WBValue):
            raise ValueError("Can only add `obj` which subclass wandb.WBValue")

        obj_id = id(obj)
        if obj_id in self._added_objs:
            return self._added_objs[obj_id]

        # If the object is coming from another artifact, save it as a reference
        if obj.artifact_source is not None:
            ref_path = obj.artifact_source["artifact"].get_path(
                type(obj).with_suffix(obj.artifact_source["name"]))
            return self.add_reference(ref_path, type(obj).with_suffix(name))[0]

        val = obj.to_json(self)
        name = obj.with_suffix(name)
        entry = self._manifest.get_entry_by_path(name)
        if entry is not None:
            return entry
        with self.new_file(name) as f:
            import json

            # TODO: Do we need to open with utf-8 codec?
            f.write(json.dumps(val, sort_keys=True))

        # Note, we add the file from our temp directory.
        # It will be added again later on finalize, but succeed since
        # the checksum should match
        entry = self.add_file(os.path.join(self._artifact_dir.name, name),
                              name)
        self._added_objs[obj_id] = entry

        return entry

    def get_added_local_path_name(self, local_path):
        """If local_path was already added to artifact, return its internal name."""
        entry = self._manifest.get_entry_by_local_path(local_path)
        if entry is None:
            return None
        return entry.path

    def get_path(self, name):
        raise ValueError(
            "Cannot load paths from an artifact before it has been saved")

    def download(self):
        raise ValueError(
            "Cannot call download on an artifact before it has been saved")

    def get(self):
        raise ValueError(
            "Cannot call get on an artifact before it has been saved")

    def finalize(self):
        if self._final:
            return self._file_entries

        # Record any created files in the manifest.
        if self._added_new:
            self.add_dir(self._artifact_dir.name)

        # mark final after all files are added
        self._final = True
        self._digest = self._manifest.digest()

        # If there are new files, move them into the artifact cache now. Our temp
        # self._artifact_dir may not be available by the time file pusher syncs
        # these files.
        if self._added_new:
            # Update the file entries for new files to point at their new location.
            def remap_entry(entry):
                if entry.local_path is None or not entry.local_path.startswith(
                        self._artifact_dir.name):
                    return entry
                rel_path = os.path.relpath(entry.local_path,
                                           start=self._artifact_dir.name)
                local_path = os.path.join(self._artifact_dir.name, rel_path)
                cache_path, hit = self._cache.check_md5_obj_path(
                    entry.digest, entry.size)
                if not hit:
                    shutil.copyfile(local_path, cache_path)
                entry.local_path = cache_path

            for entry in self._manifest.entries.values():
                remap_entry(entry)
Esempio n. 5
0
class Artifact(ArtifactInterface):
    """
    Constructs an empty artifact whose contents can be populated using its
    `add` family of functions. Once the artifact has all the desired files,
    you can call `wandb.log_artifact()` to log it.


    Arguments:
        name: (str) A human-readable name for this artifact, which is how you
            can identify this artifact in the UI or reference it in `use_artifact`
            calls. Names can contain letters, numbers, underscores, hyphens, and
            dots. The name must be unique across a project.
        type: (str) The type of the artifact, which is used to organize and differentiate
            artifacts. Common types include `dataset` or `model`, but you can use any string
            containing letters, numbers, underscores, hyphens, and dots.
        description: (str, optional) Free text that offers a description of the artifact. The
            description is markdown rendered in the UI, so this is a good place to place tables,
            links, etc.
        metadata: (dict, optional) Structured data associated with the artifact,
            for example class distribution of a dataset. This will eventually be queryable
            and plottable in the UI. There is a hard limit of 100 total keys.

    Examples:
        Basic usage
        ```
        wandb.init()

        artifact = wandb.Artifact('mnist', type='dataset')
        artifact.add_dir('mnist/')
        wandb.log_artifact(artifact)
        ```

    Raises:
        Exception: if problem.

    Returns:
        An `Artifact` object.
    """

    _added_objs: dict
    _added_local_paths: dict
    _distributed_id: Optional[str]
    _metadata: dict

    def __init__(
        self,
        name: str,
        type: str,
        description: Optional[str] = None,
        metadata: Optional[dict] = None,
    ) -> None:
        if not re.match(r"^[a-zA-Z0-9_\-.]+$", name):
            raise ValueError(
                "Artifact name may only contain alphanumeric characters, dashes, underscores, and dots. "
                'Invalid name: "%s"' % name)
        # TODO: this shouldn't be a property of the artifact. It's a more like an
        # argument to log_artifact.
        storage_layout = StorageLayout.V2
        if env.get_use_v1_artifacts():
            storage_layout = StorageLayout.V1

        self._storage_policy = WandbStoragePolicy(
            config={
                "storageLayout": storage_layout,
                #  TODO: storage region
            })
        self._api = InternalApi()
        self._final = False
        self._digest = ""
        self._file_entries = None
        self._manifest = ArtifactManifestV1(self, self._storage_policy)
        self._cache = get_artifacts_cache()
        self._added_objs = {}
        self._added_local_paths = {}
        # You can write into this directory when creating artifact files
        self._artifact_dir = compat_tempfile.TemporaryDirectory(
            missing_ok_on_cleanup=True)
        self._type = type
        self._name = name
        self._description = description
        self._metadata = metadata or {}
        self._distributed_id = None

    @property
    def id(self) -> Optional[str]:
        # The artifact hasn't been saved so an ID doesn't exist yet.
        return None

    @property
    def entity(self) -> str:
        # TODO: querying for default entity a good idea here?
        return self._api.settings("entity") or self._api.viewer().get("entity")

    @property
    def project(self) -> str:
        return self._api.settings("project")

    @property
    def manifest(self) -> ArtifactManifest:
        self.finalize()
        return self._manifest

    @property
    def digest(self) -> str:
        self.finalize()
        # Digest will be none if the artifact hasn't been saved yet.
        return self._digest

    @property
    def description(self) -> Optional[str]:
        return self._description

    @description.setter
    def description(self, desc: Optional[str]) -> None:
        self._description = desc

    @property
    def metadata(self) -> dict:
        return self._metadata

    @metadata.setter
    def metadata(self, metadata: dict) -> None:
        self._metadata = metadata

    @property
    def type(self) -> str:
        return self._type

    @property
    def name(self) -> str:
        return self._name

    @property
    def state(self) -> str:
        return "PENDING"

    @property
    def size(self) -> int:
        return sum([entry.size for entry in self._manifest.entries])

    @property
    def distributed_id(self) -> Optional[str]:
        return self._distributed_id

    @distributed_id.setter
    def distributed_id(self, distributed_id: Optional[str]) -> None:
        self._distributed_id = distributed_id

    def _ensure_can_add(self):
        if self._final:
            raise ValueError("Can't add to finalized artifact.")

    @contextlib.contextmanager
    def new_file(self, name: str, mode: str = "w"):
        self._ensure_can_add()
        path = os.path.join(self._artifact_dir.name, name.lstrip("/"))
        if os.path.exists(path):
            raise ValueError('File with name "%s" already exists at "%s"' %
                             (name, path))

        util.mkdir_exists_ok(os.path.dirname(path))
        with util.fsync_open(path, mode) as f:
            yield f

        self.add_file(path, name=name)

    def add_file(
        self,
        local_path: str,
        name: Optional[str] = None,
        is_tmp: Optional[bool] = False,
    ):
        self._ensure_can_add()
        if not os.path.isfile(local_path):
            raise ValueError("Path is not a file: %s" % local_path)

        name = name or os.path.basename(local_path)
        digest = md5_file_b64(local_path)

        if is_tmp:
            file_path, file_name = os.path.split(name)
            file_name_parts = file_name.split(".")
            file_name_parts[0] = b64_string_to_hex(digest)[:8]
            name = os.path.join(file_path, ".".join(file_name_parts))

        return self._add_local_file(name, local_path, digest=digest)

    def add_dir(self, local_path: str, name: Optional[str] = None):
        self._ensure_can_add()
        if not os.path.isdir(local_path):
            raise ValueError("Path is not a directory: %s" % local_path)

        termlog(
            "Adding directory to artifact (%s)... " %
            os.path.join(".", os.path.normpath(local_path)),
            newline=False,
        )
        start_time = time.time()

        paths = []
        for dirpath, _, filenames in os.walk(local_path, followlinks=True):
            for fname in filenames:
                physical_path = os.path.join(dirpath, fname)
                logical_path = os.path.relpath(physical_path, start=local_path)
                if name is not None:
                    logical_path = os.path.join(name, logical_path)
                paths.append((logical_path, physical_path))

        def add_manifest_file(log_phy_path):
            logical_path, physical_path = log_phy_path
            self._add_local_file(logical_path, physical_path)

        import multiprocessing.dummy  # this uses threads

        NUM_THREADS = 8
        pool = multiprocessing.dummy.Pool(NUM_THREADS)
        pool.map(add_manifest_file, paths)
        pool.close()
        pool.join()

        termlog("Done. %.1fs" % (time.time() - start_time), prefix=False)

    def add_reference(
        self,
        uri: Union[ArtifactEntry, str],
        name: Optional[str] = None,
        checksum: bool = True,
        max_objects: Optional[int] = None,
    ):
        self._ensure_can_add()

        # This is a bit of a hack, we want to check if the uri is a of the type
        # ArtifactEntry which is a private class returned by Artifact.get_path in
        # wandb/apis/public.py. If so, then recover the reference URL.
        if isinstance(uri, ArtifactEntry) and uri.parent_artifact() != self:
            ref_url_fn = getattr(uri, "ref_url")
            uri = ref_url_fn()
        url = urlparse(str(uri))
        if not url.scheme:
            raise ValueError(
                "References must be URIs. To reference a local file, use file://"
            )

        manifest_entries = self._storage_policy.store_reference(
            self, uri, name=name, checksum=checksum, max_objects=max_objects)
        for entry in manifest_entries:
            self._manifest.add_entry(entry)

        return manifest_entries

    def add(self, obj: WBValue, name: str):
        self._ensure_can_add()

        # Validate that the object is wandb.Media type
        if not isinstance(obj, WBValue):
            raise ValueError("Can only add `obj` which subclass wandb.WBValue")

        obj_id = id(obj)
        if obj_id in self._added_objs:
            return self._added_objs[obj_id]["entry"]

        # If the object is coming from another artifact, save it as a reference
        if obj.artifact_source and obj.artifact_source.name:
            ref_path = obj.artifact_source.artifact.get_path(
                type(obj).with_suffix(obj.artifact_source.name))
            return self.add_reference(ref_path, type(obj).with_suffix(name))[0]

        val = obj.to_json(self)
        name = obj.with_suffix(name)
        entry = self._manifest.get_entry_by_path(name)
        if entry is not None:
            return entry
        with self.new_file(name) as f:
            import json

            # TODO: Do we need to open with utf-8 codec?
            f.write(json.dumps(val, sort_keys=True))

        # Note, we add the file from our temp directory.
        # It will be added again later on finalize, but succeed since
        # the checksum should match
        entry = self.add_file(os.path.join(self._artifact_dir.name, name),
                              name)
        self._added_objs[obj_id] = {"entry": entry, "obj": obj}

        return entry

    def get_path(self, name: str):
        raise ValueError(
            "Cannot load paths from an artifact before it has been saved")

    def get(self, name: str):
        raise ValueError(
            "Cannot call get on an artifact before it has been saved")

    def download(self, root: str = None, recursive: bool = False):
        raise ValueError(
            "Cannot call download on an artifact before it has been saved")

    def get_added_local_path_name(self, local_path: str):
        """
        Get the artifact relative name of a file added by a local filesystem path.

        Arguments:
            local_path: (str) The local path to resolve into an artifact relative name.

        Returns:
            str: The artifact relative name.

        Examples:
            Basic usage
            ```
            artifact = wandb.Artifact('my_dataset', type='dataset')
            artifact.add_file('path/to/file.txt', name='artifact/path/file.txt')

            # Returns `artifact/path/file.txt`:
            name = artifact.get_added_local_path_name('path/to/file.txt')
            ```
        """
        entry = self._added_local_paths.get(local_path, None)
        if entry is None:
            return None
        return entry.path

    def finalize(self):
        """
        Marks this artifact as final, which disallows further additions to the artifact.
        This happens automatically when calling `log_artifact`.


        Returns:
            None
        """
        if self._final:
            return self._file_entries

        # mark final after all files are added
        self._final = True
        self._digest = self._manifest.digest()

    def _add_local_file(self, name, path, digest=None):
        digest = digest or md5_file_b64(path)
        size = os.path.getsize(path)

        cache_path, hit = self._cache.check_md5_obj_path(digest, size)
        if not hit:
            shutil.copyfile(path, cache_path)

        entry = ArtifactManifestEntry(
            name,
            None,
            digest=digest,
            size=size,
            local_path=cache_path,
        )

        self._manifest.add_entry(entry)
        self._added_local_paths[path] = entry
        return entry
Esempio n. 6
0
class Artifact(object):
    """An artifact object you can write files into, and pass to log_artifact."""

    # A local manifest contains the path to the local file in addition to the path within
    # the artifact.
    LocalArtifactManifestEntry = collections.namedtuple(
        'LocalArtifactManifestEntry', ('path', 'hash', 'local_path'))

    def __init__(self, name, type, description=None, metadata=None):
        if not re.match('^[a-zA-Z0-9_\-.]+$', name):
            raise ValueError(
                'Artifact name may only contain alphanumeric characters, dashes, underscores, and dots. Invalid name: "%s"'
                % name)
        if type is None:
            raise ValueError(
                "type is required when logging artifacts, specify \"dataset\", \"model\", or a custom type"
            )
        # TODO: this shouldn't be a property of the artifact. It's a more like an
        # argument to log_artifact.
        self._storage_policy = WandbStoragePolicy()
        self._file_specs = {}
        self._api = InternalApi()  # TODO: persist project in settings?
        self._final = False
        self._digest = None
        self._file_entries = None
        self._manifest = ArtifactManifestV1(self, self._storage_policy)
        self._cache = artifacts_cache.get_artifacts_cache()
        self._added_new = False
        # You can write into this directory when creating artifact files
        self._artifact_dir = compat_tempfile.TemporaryDirectory(
            missing_ok_on_cleanup=True)
        self.server_manifest = None
        self.type = type
        self.name = name
        self.description = description
        self.metadata = metadata

    @property
    def id(self):
        # The artifact hasn't been saved so an ID doesn't exist yet.
        return None

    @property
    def entity(self):
        # TODO: querying for default entity a good idea here?
        return self._api.settings('entity') or self._api.viewer().get("entity")

    @property
    def project(self):
        return self._api.settings('project')

    @property
    def manifest(self):
        self.finalize()
        return self._manifest

    # TODO: Currently this returns the L0 digest. Is this what we want?
    @property
    def digest(self):
        self.finalize()
        return self._digest

    def _ensure_can_add(self):
        if self._final:
            raise ValueError('Can\'t add to finalized artifact.')

    def new_file(self, name, mode='w'):
        self._ensure_can_add()
        path = os.path.join(self._artifact_dir.name, name.lstrip('/'))
        if os.path.exists(path):
            raise ValueError('File with name "%s" already exists' % name)
        util.mkdir_exists_ok(os.path.dirname(path))
        self._added_new = True
        return open(path, mode)

    def add_file(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isfile(local_path):
            raise ValueError('Path is not a file: %s' % local_path)

        name = name or os.path.basename(local_path)
        entry = ArtifactManifestEntry(name,
                                      None,
                                      digest=md5_file_b64(local_path),
                                      size=os.path.getsize(local_path),
                                      local_path=local_path)
        self._manifest.add_entry(entry)

    def add_dir(self, local_path, name=None):
        self._ensure_can_add()
        if not os.path.isdir(local_path):
            raise ValueError('Path is not a directory: %s' % local_path)

        termlog('Adding directory to artifact (%s)... ' %
                os.path.join('.', os.path.normpath(local_path)),
                newline=False)
        start_time = time.time()

        paths = []
        for dirpath, _, filenames in os.walk(local_path, followlinks=True):
            for fname in filenames:
                physical_path = os.path.join(dirpath, fname)
                logical_path = os.path.relpath(physical_path, start=local_path)
                if name is not None:
                    logical_path = os.path.join(name, logical_path)
                paths.append((logical_path, physical_path))

        def add_manifest_file(log_phy_path):
            logical_path, physical_path = log_phy_path
            self._manifest.add_entry(
                ArtifactManifestEntry(logical_path,
                                      None,
                                      digest=md5_file_b64(physical_path),
                                      size=os.path.getsize(physical_path),
                                      local_path=physical_path))

        import multiprocessing.dummy  # this uses threads
        NUM_THREADS = 8
        pool = multiprocessing.dummy.Pool(NUM_THREADS)
        pool.map(add_manifest_file, paths)
        pool.close()
        pool.join()

        termlog('Done. %.1fs' % (time.time() - start_time), prefix=False)

    def add_reference(self, uri, name=None, checksum=True, max_objects=None):
        url = urlparse(uri)
        if not url.scheme:
            raise ValueError(
                'References must be URIs. To reference a local file, use file://'
            )
        if self._final:
            raise ValueError('Can\'t add to finalized artifact.')
        manifest_entries = self._storage_policy.store_reference(
            self, uri, name=name, checksum=checksum, max_objects=max_objects)
        for entry in manifest_entries:
            self._manifest.add_entry(entry)

    def get_path(self, name):
        raise ValueError(
            'Cannot load paths from an artifact before it has been saved')

    def download(self):
        raise ValueError(
            'Cannot call download on an artifact before it has been saved')

    def finalize(self):
        if self._final:
            return self._file_entries

        # Record any created files in the manifest.
        if self._added_new:
            self.add_dir(self._artifact_dir.name)

        # mark final after all files are added
        self._final = True

        # Add the manifest itself as a file.
        with tempfile.NamedTemporaryFile('w+', suffix=".json",
                                         delete=False) as fp:
            json.dump(self._manifest.to_manifest_json(), fp, indent=4)
            self._file_specs['wandb_manifest.json'] = fp.name
            manifest_file = fp.name

        # Calculate the server manifest
        file_entries = []
        for name, local_path in self._file_specs.items():
            file_entries.append(
                self.LocalArtifactManifestEntry(name, md5_file_b64(local_path),
                                                os.path.abspath(local_path)))
        self.server_manifest = ServerManifestV1(file_entries)
        self._digest = self._manifest.digest()

        # If there are new files, move them into the artifact cache now. Our temp
        # self._artifact_dir may not be available by the time file pusher syncs
        # these files.
        if self._added_new:
            # Update the file entries for new files to point at their new location.
            def remap_entry(entry):
                if entry.local_path is None or not entry.local_path.startswith(
                        self._artifact_dir.name):
                    return entry
                rel_path = os.path.relpath(entry.local_path,
                                           start=self._artifact_dir.name)
                local_path = os.path.join(self._artifact_dir.name, rel_path)
                cache_path, hit = self._cache.check_md5_obj_path(
                    entry.digest, entry.size)
                if not hit:
                    shutil.copyfile(local_path, cache_path)
                entry.local_path = cache_path

            for entry in self._manifest.entries.values():
                remap_entry(entry)
Esempio n. 7
0
def init(ctx, project, entity, reset):
    from wandb.old.core import _set_stage_dir, __stage_dir__, wandb_dir

    if __stage_dir__ is None:
        _set_stage_dir("wandb")

    # non interactive init
    if reset or project or entity:
        api = InternalApi()
        if reset:
            api.clear_setting("entity", persist=True)
            api.clear_setting("project", persist=True)
            # TODO(jhr): clear more settings?
        if entity:
            api.set_setting("entity", entity, persist=True)
        if project:
            api.set_setting("project", project, persist=True)
        return

    if os.path.isdir(wandb_dir()) and os.path.exists(
        os.path.join(wandb_dir(), "settings")
    ):
        click.confirm(
            click.style(
                "This directory has been configured previously, should we re-configure it?",
                bold=True,
            ),
            abort=True,
        )
    else:
        click.echo(
            click.style("Let's setup this directory for W&B!", fg="green", bold=True)
        )
    api = InternalApi()
    if api.api_key is None:
        ctx.invoke(login)

    viewer = api.viewer()

    # Viewer can be `None` in case your API information became invalid, or
    # in testing if you switch hosts.
    if not viewer:
        click.echo(
            click.style(
                "Your login information seems to be invalid: can you log in again please?",
                fg="red",
                bold=True,
            )
        )
        ctx.invoke(login)

    # This shouldn't happen.
    viewer = api.viewer()
    if not viewer:
        click.echo(
            click.style(
                "We're sorry, there was a problem logging you in. Please send us a note at [email protected] and tell us how this happened.",
                fg="red",
                bold=True,
            )
        )
        sys.exit(1)

    # At this point we should be logged in successfully.
    if len(viewer["teams"]["edges"]) > 1:
        team_names = [e["node"]["name"] for e in viewer["teams"]["edges"]]
        question = {
            "type": "list",
            "name": "team_name",
            "message": "Which team should we use?",
            "choices": team_names
            # TODO(jhr): disabling manual entry for cling
            # 'choices': team_names + ["Manual Entry"]
        }
        result = whaaaaat.prompt([question])
        # result can be empty on click
        if result:
            entity = result["team_name"]
        else:
            entity = "Manual Entry"
        if entity == "Manual Entry":
            entity = click.prompt("Enter the name of the team you want to use")
    else:
        entity = viewer.get("entity") or click.prompt(
            "What username or team should we use?"
        )

    # TODO: this error handling sucks and the output isn't pretty
    try:
        project = prompt_for_project(ctx, entity)
    except ClickWandbException:
        raise ClickException("Could not find team: %s" % entity)

    api.set_setting("entity", entity, persist=True)
    api.set_setting("project", project, persist=True)
    api.set_setting("base_url", api.settings().get("base_url"), persist=True)

    util.mkdir_exists_ok(wandb_dir())
    with open(os.path.join(wandb_dir(), ".gitignore"), "w") as file:
        file.write("*\n!settings")

    click.echo(
        click.style("This directory is configured!  Next, track a run:\n", fg="green")
        + textwrap.dedent(
            """\
        * In your training script:
            {code1}
            {code2}
        * then `{run}`.
        """
        ).format(
            code1=click.style("import wandb", bold=True),
            code2=click.style('wandb.init(project="%s")' % project, bold=True),
            run=click.style("python <train.py>", bold=True),
        )
    )