• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""Class to represent a device."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import print_function
20
21from tensorflow.python.util.tf_export import tf_export
22
23
24# EPU represents for TPU embedding for now. Subject to change in future.
25_VALID_DEVICE_TYPES = frozenset({"CPU", "GPU", "TPU", "CUSTOM", "EPU"})
26
27
28# ==============================================================================
29# == Global Implementation Details =============================================
30# ==============================================================================
31_STRING_TO_COMPONENTS_CACHE = {}
32_COMPONENTS_TO_STRING_CACHE = {}
33
34
35def _as_str_or_none(inp):
36  return None if inp is None else str(inp)
37
38
39def _as_int_or_none(inp):
40  return None if inp is None else int(inp)
41
42
43def _as_device_str_or_none(device_type):
44  # For backwards compatibility only, we support lowercase variants of
45  # cpu and gpu but turn them into uppercase here.
46  if device_type in ("cpu", "gpu"):
47    return device_type.upper()
48  return _as_str_or_none(device_type)
49
50
51@tf_export("DeviceSpec", v1=[])
52class DeviceSpecV2(object):
53  """Represents a (possibly partial) specification for a TensorFlow device.
54
55  `DeviceSpec`s are used throughout TensorFlow to describe where state is stored
56  and computations occur. Using `DeviceSpec` allows you to parse device spec
57  strings to verify their validity, merge them or compose them programmatically.
58
59  Example:
60
61  ```python
62  # Place the operations on device "GPU:0" in the "ps" job.
63  device_spec = DeviceSpec(job="ps", device_type="GPU", device_index=0)
64  with tf.device(device_spec.to_string()):
65    # Both my_var and squared_var will be placed on /job:ps/device:GPU:0.
66    my_var = tf.Variable(..., name="my_variable")
67    squared_var = tf.square(my_var)
68  ```
69
70  With eager execution disabled (by default in TensorFlow 1.x and by calling
71  disable_eager_execution() in TensorFlow 2.x), the following syntax
72  can be used:
73
74  ```python
75  tf.compat.v1.disable_eager_execution()
76
77  # Same as previous
78  device_spec = DeviceSpec(job="ps", device_type="GPU", device_index=0)
79  # No need of .to_string() method.
80  with tf.device(device_spec):
81    my_var = tf.Variable(..., name="my_variable")
82    squared_var = tf.square(my_var)
83  ```
84
85  If a `DeviceSpec` is partially specified, it will be merged with other
86  `DeviceSpec`s according to the scope in which it is defined. `DeviceSpec`
87  components defined in inner scopes take precedence over those defined in
88  outer scopes.
89
90  ```python
91  gpu0_spec = DeviceSpec(job="ps", device_type="GPU", device_index=0)
92  with tf.device(DeviceSpec(job="train").to_string()):
93    with tf.device(gpu0_spec.to_string()):
94      # Nodes created here will be assigned to /job:ps/device:GPU:0.
95    with tf.device(DeviceSpec(device_type="GPU", device_index=1).to_string()):
96      # Nodes created here will be assigned to /job:train/device:GPU:1.
97  ```
98
99  A `DeviceSpec` consists of 5 components -- each of
100  which is optionally specified:
101
102  * Job: The job name.
103  * Replica: The replica index.
104  * Task: The task index.
105  * Device type: The device type string (e.g. "CPU" or "GPU").
106  * Device index: The device index.
107  """
108
109  __slots__ = ("_job", "_replica", "_task", "_device_type", "_device_index",
110               "_as_string", "_hash")
111
112  def __init__(self, job=None, replica=None, task=None, device_type=None,
113               device_index=None):
114    """Create a new `DeviceSpec` object.
115
116    Args:
117      job: string.  Optional job name.
118      replica: int.  Optional replica index.
119      task: int.  Optional task index.
120      device_type: Optional device type string (e.g. "CPU" or "GPU")
121      device_index: int.  Optional device index.  If left
122        unspecified, device represents 'any' device_index.
123    """
124    self._job = _as_str_or_none(job)
125    self._replica = _as_int_or_none(replica)
126    self._task = _as_int_or_none(task)
127    self._device_type = _as_device_str_or_none(device_type)
128    self._device_index = _as_int_or_none(device_index)
129    self._as_string = self._components_to_string(
130        job=self._job, replica=self._replica, task=self._task,
131        device_type=self._device_type, device_index=self._device_index)
132    self._hash = hash(self.to_string())
133
134  def to_string(self):
135    """Return a string representation of this `DeviceSpec`.
136
137    Returns:
138      a string of the form
139      /job:<name>/replica:<id>/task:<id>/device:<device_type>:<id>.
140    """
141    return self._as_string
142
143  @classmethod
144  def from_string(cls, spec):
145    """Construct a `DeviceSpec` from a string.
146
147    Args:
148      spec: a string of the form
149       /job:<name>/replica:<id>/task:<id>/device:CPU:<id>
150      or
151       /job:<name>/replica:<id>/task:<id>/device:GPU:<id>
152      as cpu and gpu are mutually exclusive.
153      All entries are optional.
154
155    Returns:
156      A DeviceSpec.
157    """
158    return cls(*cls._string_to_components(spec))
159
160  def parse_from_string(self, spec):
161    """Parse a `DeviceSpec` name into its components.
162
163    **2.x behavior change**:
164
165    In TensorFlow 1.x, this function mutates its own state and returns itself.
166    In 2.x, DeviceSpecs are immutable, and this function will return a
167      DeviceSpec which contains the spec.
168
169    * Recommended:
170
171      ```
172      # my_spec and my_updated_spec are unrelated.
173      my_spec = tf.DeviceSpec.from_string("/CPU:0")
174      my_updated_spec = tf.DeviceSpec.from_string("/GPU:0")
175      with tf.device(my_updated_spec):
176        ...
177      ```
178
179    * Will work in 1.x and 2.x (though deprecated in 2.x):
180
181      ```
182      my_spec = tf.DeviceSpec.from_string("/CPU:0")
183      my_updated_spec = my_spec.parse_from_string("/GPU:0")
184      with tf.device(my_updated_spec):
185        ...
186      ```
187
188    * Will NOT work in 2.x:
189
190      ```
191      my_spec = tf.DeviceSpec.from_string("/CPU:0")
192      my_spec.parse_from_string("/GPU:0")  # <== Will not update my_spec
193      with tf.device(my_spec):
194        ...
195      ```
196
197    In general, `DeviceSpec.from_string` should completely replace
198    `DeviceSpec.parse_from_string`, and `DeviceSpec.replace` should
199    completely replace setting attributes directly.
200
201    Args:
202      spec: an optional string of the form
203       /job:<name>/replica:<id>/task:<id>/device:CPU:<id>
204      or
205       /job:<name>/replica:<id>/task:<id>/device:GPU:<id>
206      as cpu and gpu are mutually exclusive.
207      All entries are optional.
208
209    Returns:
210      The `DeviceSpec`.
211
212    Raises:
213      ValueError: if the spec was not valid.
214    """
215    return self.from_string(spec)
216
217  def make_merged_spec(self, dev):
218    """Returns a new DeviceSpec which incorporates `dev`.
219
220    When combining specs, `dev` will take precedence over the current spec.
221    So for instance:
222    ```
223    first_spec = tf.DeviceSpec(job=0, device_type="CPU")
224    second_spec = tf.DeviceSpec(device_type="GPU")
225    combined_spec = first_spec.make_merged_spec(second_spec)
226    ```
227
228    is equivalent to:
229    ```
230    combined_spec = tf.DeviceSpec(job=0, device_type="GPU")
231    ```
232
233    Args:
234      dev: a `DeviceSpec`
235
236    Returns:
237      A new `DeviceSpec` which combines `self` and `dev`
238    """
239    return self.__class__(*self._get_combined_properties(dev))
240
241  def replace(self, **kwargs):
242    """Convenience method for making a new DeviceSpec by overriding fields.
243
244    For instance:
245    ```
246    my_spec = DeviceSpec=(job="my_job", device="CPU")
247    my_updated_spec = my_spec.replace(device="GPU")
248    my_other_spec = my_spec.replace(device=None)
249    ```
250
251    Args:
252      **kwargs: This method takes the same args as the DeviceSpec constructor
253
254    Returns:
255      A DeviceSpec with the fields specified in kwargs overridden.
256    """
257    init_kwargs = dict(
258        job=self.job, replica=self.replica, task=self.task,
259        device_type=self.device_type, device_index=self.device_index)
260
261    # Explicitly provided kwargs take precedence.
262    init_kwargs.update(kwargs)
263    return self.__class__(**init_kwargs)
264
265  @property
266  def job(self):
267    return self._job
268
269  @property
270  def replica(self):
271    return self._replica
272
273  @property
274  def task(self):
275    return self._task
276
277  @property
278  def device_type(self):
279    return self._device_type
280
281  @property
282  def device_index(self):
283    return self._device_index
284
285  def _get_combined_properties(self, dev):
286    """Combine the current DeviceSpec with another DeviceSpec.
287
288    The combination of DeviceSpecs is will give priority to dev.
289
290    Args:
291      dev: a `DeviceSpec`
292
293    Returns:
294      A tuple of (job, replica, task, device_type, device_index) which
295      represents the combination of self and dev.
296    """
297    return (
298        dev.job if dev.job is not None else self.job,
299        dev.replica if dev.replica is not None else self.replica,
300        dev.task if dev.task is not None else self.task,
301        dev.device_type if dev.device_type is not None else self.device_type,
302        dev.device_index if dev.device_index is not None else self.device_index,
303    )
304
305  @staticmethod
306  def _string_to_components(spec=None):
307    """Stateless portion of device spec string parsing.
308
309    Args:
310      spec: An optional string specifying a device specification.
311
312    Returns:
313      The parsed components of `spec`. Note that the result of this function
314      must go through attribute setters of DeviceSpec, and should therefore NOT
315      be used directly.
316    """
317    cached_result = _STRING_TO_COMPONENTS_CACHE.get(spec)
318    if cached_result is not None:
319      return cached_result
320
321    raw_spec = spec  # keep a copy of the original to update the cache
322    job, replica, task, device_type, device_index = None, None, None, None, None
323
324    spec = spec or ""
325    splits = [x.split(":") for x in spec.split("/")]
326    for y in splits:
327      ly = len(y)
328      if y:
329        # NOTE(taylorrobie): these will go through setters later.
330        if ly == 2 and y[0] == "job":
331          job = y[1]
332        elif ly == 2 and y[0] == "replica":
333          replica = y[1]
334        elif ly == 2 and y[0] == "task":
335          task = y[1]
336        elif ((ly == 1 or ly == 2) and (y[0].upper() in _VALID_DEVICE_TYPES)):
337          if device_type is not None:
338            raise ValueError("Cannot specify multiple device types: %s" % spec)
339          device_type = y[0].upper()
340          if ly == 2 and y[1] != "*":
341            device_index = int(y[1])
342        elif ly == 3 and y[0] == "device":
343          if device_type is not None:
344            raise ValueError("Cannot specify multiple device types: %s" % spec)
345          device_type = y[1]
346          if y[2] != "*":
347            device_index = int(y[2])
348        elif ly and y[0] != "":  # pylint: disable=g-explicit-bool-comparison
349          raise ValueError("Unknown attribute: '%s' in '%s'" % (y[0], spec))
350
351    output = (job, replica, task, device_type, device_index)
352    _STRING_TO_COMPONENTS_CACHE[raw_spec] = output
353    return output
354
355  @staticmethod
356  def _components_to_string(job, replica, task, device_type, device_index):
357    """Stateless portion of `to_string` (separated to allow caching)."""
358    key = (job, replica, task, device_type, device_index)
359    cached_result = _COMPONENTS_TO_STRING_CACHE.get(key)
360    if cached_result is not None:
361      return cached_result
362
363    output = []
364    if job is not None:
365      output.append("/job:" + job)
366    if replica is not None:
367      output.append("/replica:" + str(replica))
368    if task is not None:
369      output.append("/task:" + str(task))
370    if device_type is not None:
371      device_index_string = "*"
372      if device_index is not None:
373        # Unlike the others, device_index is stored as an int.
374        device_index_string = str(device_index)
375      output.append("/device:%s:%s" % (device_type, device_index_string))
376
377    output = "".join(output)
378    _COMPONENTS_TO_STRING_CACHE[key] = output
379    return output
380
381  def __eq__(self, other):
382    """Checks if the `other` DeviceSpec is same as the current instance, eg have
383
384       same value for all the internal fields.
385
386    Args:
387      other: Another DeviceSpec
388
389    Returns:
390      Return `True` if `other` is also a DeviceSpec instance and has same value
391      as the current instance.
392      Return `False` otherwise.
393    """
394    return (isinstance(other, self.__class__) and
395            self.to_string() == other.to_string())
396
397  def __hash__(self):
398    return self._hash
399
400
401@tf_export(v1=["DeviceSpec"])  # pylint: disable=missing-docstring
402class DeviceSpecV1(DeviceSpecV2):
403  __doc__ = DeviceSpecV2.__doc__
404  __slots__ = DeviceSpecV2.__slots__
405
406  @DeviceSpecV2.job.setter
407  def job(self, job):
408    self._job = _as_str_or_none(job)
409    self._as_string, self._hash = None, None
410
411  @DeviceSpecV2.replica.setter
412  def replica(self, replica):
413    self._replica = _as_int_or_none(replica)
414    self._as_string, self._hash = None, None
415
416  @DeviceSpecV2.task.setter
417  def task(self, task):
418    self._task = _as_int_or_none(task)
419    self._as_string, self._hash = None, None
420
421  @DeviceSpecV2.device_type.setter
422  def device_type(self, device_type):
423    self._device_type = _as_device_str_or_none(device_type)
424    self._as_string, self._hash = None, None
425
426  @DeviceSpecV2.device_index.setter
427  def device_index(self, device_index):
428    self._device_index = _as_int_or_none(device_index)
429    self._as_string, self._hash = None, None
430
431  def __hash__(self):
432    if self._hash is None:
433      self._hash = hash(self.to_string())
434    return self._hash
435
436  def to_string(self):
437    if self._as_string is None:
438      self._as_string = self._components_to_string(
439          job=self.job, replica=self.replica, task=self.task,
440          device_type=self.device_type, device_index=self.device_index)
441    return self._as_string
442
443  def parse_from_string(self, spec):
444    (self.job, self.replica, self.task, self.device_type, self.device_index
445    ) = self._string_to_components(spec)
446
447    return self
448
449  def merge_from(self, dev):
450    """Merge the properties of "dev" into this `DeviceSpec`.
451
452    Note: Will be removed in TensorFlow 2.x since DeviceSpecs will become
453          immutable.
454
455    Args:
456      dev: a `DeviceSpec`.
457    """
458    (self.job, self.replica, self.task, self.device_type, self.device_index
459    ) = self._get_combined_properties(dev)
460
461  # Use parent class docstrings for public methods.
462  to_string.__doc__ = DeviceSpecV2.to_string.__doc__
463  parse_from_string.__doc__ = DeviceSpecV2.parse_from_string.__doc__
464