profile
viewpoint
Matthew Johnson mattjj Google San Francisco people.csail.mit.edu/~mattjj research scientist @ Google Brain

google/jax 15208

Composable transformations of Python+NumPy programs: differentiate, vectorize, JIT to GPU/TPU, and more

google-research/dex-lang 1118

Research language for array processing in the Haskell/ML family

mattjj/autodidact 667

A pedagogical implementation of Autograd

google/xls 663

XLS: Accelerated HW Synthesis

jacobjinkelly/easy-neural-ode 187

Code for the paper "Learning Differential Equations that are Easy to Solve"

duvenaud/relax 151

Optimizing control variates for black-box gradient estimation

google-research/autoconj 36

Recognizing and exploiting conjugacy without a domain-specific language

duvenaud/jaxde 27

Prototypes of differentiable differential equation solvers in JAX.

mattjj/config-fish 4

my fish configuration

mattjj/config-vim 4

my .vim

PR opened google/jax

Reviewers
add an assert primitive pull ready

The assert primitive has an effectful API and so it can't be staged out; it's only a trace-time primitive. It can be discharged to the functional form.

We might want to have separate transforms for discharging errors and for adding error checks. But right now they're just bundled together in the checkify transform.

+73 -0

0 comment

2 changed files

pr created time in 9 minutes

push eventmattjj/jax

Jake VanderPlas

commit sha 0e4e30f4e54805009bad17d69e49d0f086cc021f

DOC: add documentation for configuration functionality

view details

Jake VanderPlas

commit sha 47e88ded050e2f4ee2e6e9607363c23c5fef2018

[x64] ensure scatter functionality preserves weak_type

view details

Peter Hawkins

commit sha 9394350727f28323cec4d868f9553075b637b4df

Refactor uses of xla.call_translations to use xla.register_translation. No functional changes intended. PiperOrigin-RevId: 413443279

view details

Jake VanderPlas

commit sha b977028022ddcf490f8b06752a1889ba628ecb31

lax.convert_element_type: better validation for new_dtype

view details

jax authors

commit sha 7869a6cb759909f6da5f82d163d815e1fa969f21

Merge pull request #8753 from mattjj:checkify PiperOrigin-RevId: 413513067

view details

jax authors

commit sha 404c3c7d2538c4553b68b38d3a7949e477dd3c42

Merge pull request #8718 from jakevdp:config-doc PiperOrigin-RevId: 413630185

view details

Peter Hawkins

commit sha ffb7ec165184c4e3b6d9c08ac017ad086d4e9cdb

Update Bazel to 4.2.1. Fixes #8573

view details

Peter Hawkins

commit sha 3597b445599181557c3d56bb677672a4981f643e

Express xmap tile/untile logic in lax via xla.lower_fun(). The lax APIs are simpler and avoid the need to port the code to MHLO. PiperOrigin-RevId: 413657577

view details

jax authors

commit sha b4dcf2b08d09c97934a9d8e7ec14e6e2a5ea8142

Merge pull request #8766 from hawkinsp:bazel421 PiperOrigin-RevId: 413671952

view details

Jake VanderPlas

commit sha da319cf30292c9a54e1d41c7f71a8db1961f86d9

[sparse] refactor jax.experimental.sparse Why? Better organization, and to avoid issues with circular imports. PiperOrigin-RevId: 413679493

view details

jax authors

commit sha ea98cf42d99a58120e6a68411969a7e0bb3aa928

Merge pull request #8754 from jakevdp:lax-dtype-none PiperOrigin-RevId: 413690787

view details

jax authors

commit sha 99182372c9aa3230e47b2aec9733ca52ea497b59

Merge pull request #8743 from jakevdp:scatter-weak-type PiperOrigin-RevId: 413691628

view details

Matthew Johnson

commit sha 768b076420ff3e07947b3c42e048702172448ec2

add an assert primitive The assert primitive has an effectful API and so it can't be staged out; it's only a trace-time primitive. It can be discharged to the functional form. We might want to have separate transforms for discharging errors and for adding error checks. But right now they're just bundled together in the checkify transform.

view details

push time in 11 minutes

PullRequestReviewEvent
PullRequestReviewEvent

Pull request review commentgoogle/jax

add more detail about aux_data in pytree docs

 show_example(Special(1., 2.)) ```  The set of Python types that are considered internal pytree nodes is extensible,-through a global registry of types, and values of registered types are traversed-recursively. To register a new type, you can use-{func}`~jax.tree_util.register_pytree_node`:+through a global registry of types. Values of registered types are traversed+recursively, so a custom pytree type can itself contain pytrees.++To register a new type, you can use {func}`~jax.tree_util.register_pytree_node`+to specify how to flatten and unflatten instances of the new type. That is, you+register two functions: the first function accepts instances of the new type and+returns a pair where the first element is an iterable of children (for example,+the values of a dictionary) and the second element is auxiliary metadata needed

Nice! That seems like a good rule of thumb.

mattjj

comment created time in 19 hours

pull request commentgoogle/jax

add jaxpr parts of simple effect types

Ah, I remembered why I called it effects and not has_effects: we expect to generalize this to a set of effects rather than just a boolean. Then the has_effects name doesn't seem ideal.

mattjj

comment created time in 20 hours

Pull request review commentgoogle/jax

add more detail about aux_data in pytree docs

 show_example(Special(1., 2.)) ```  The set of Python types that are considered internal pytree nodes is extensible,-through a global registry of types, and values of registered types are traversed-recursively. To register a new type, you can use-{func}`~jax.tree_util.register_pytree_node`:+through a global registry of types. Values of registered types are traversed+recursively, so a custom pytree type can itself contain pytrees.++To register a new type, you can use {func}`~jax.tree_util.register_pytree_node`+to specify how to flatten and unflatten instances of the new type. That is, you+register two functions: the first function accepts instances of the new type and+returns a pair where the first element is an iterable of children (for example,+the values of a dictionary) and the second element is auxiliary metadata needed

(The reason I don't like the word "static" here is that I think of that as jit- or staging-specific jargon, which doesn't cover all cases here, e.g. in how pytree metadata interacts with vmap or AD. It's hard to think of a better term though...)

mattjj

comment created time in 20 hours

PullRequestReviewEvent

pull request commentgoogle/jax

add jaxpr parts of simple effect types

I am wondering what is the semantics of the effects flag. Is the following true:

Yes those sound right to me

If the above is true, then one can think of Jaxpr.effects also as a property that scans the eqns (maybe this implementation would actually simplify the code because you do not need to propagate the effects flag). Maybe then it is more important to keep an effects flag for eqn.

That could work.

At a high level, I'm trying to think of effects as encoded on the arrow types. A JaxprType (which we don't have an explicit object for in main JAX, though there is one in Autodidax), being an arrow type, would thus include what effects it has (in addition to the input and output types). Likewise a parameterized primitive (e.g. a cond_p with particular branch jaxprs) would have such a type.

For the purpose of efficiently querying and checking types, we might want to add explicit annotations to the jaxpr syntax (i.e. data structure). For that purpose it seems we could arguably put these annotations wherever we find convenient. I first put them on jaxprs so we wouldn't have to recurse into sub-jaxprs when querying the type of a jaxpr. But having them on eqns would serve that purpose also, and has a nice correspondence to how they're computed (i.e. to abstract eval rules).

I'll give that a shot and see what happens!

Nit: wdyt about renaming effects to has_effects

Love it!

mattjj

comment created time in 21 hours

Pull request review commentgoogle/jax

add jaxpr parts of simple effect types

 def _rewrite_while_outfeed_cond(eqn: core.JaxprEqn, eqns: List[core.JaxprEqn],                       new_body_invars_body_constvars + [new_body_invars_pred] +                       new_body_invars_carry + [new_body_invars_token, new_body_invars_itoken]),                  ([new_body_pred2] + new_body_carry2 + [new_body_token3, new_body_itoken3]),-                 new_body_eqns), [])+                 new_body_eqns, False), [])

I was thinking eventually we may not want to keep the host_callback primitives, replacing them with new primitives.

I hardcoded 'False' here just as a placeholder encoding current behavior, but I should've left a TODO. I can update it now...

mattjj

comment created time in 21 hours

PullRequestReviewEvent

Pull request review commentgoogle/jax

add jaxpr parts of simple effect types

 def _cond_typecheck(*avals, branches, linear):         all(_map(core.typematch, jaxpr0.out_avals, jaxpr.out_avals)),         f'cond branches 0 and {i+1} have mismatching output types: '         f'{jaxpr0_out_avals_str} vs {_avals_short(jaxpr.out_avals)}')+    core.typecheck_assert(

I'm thinking of the effect as part of the type, and we still need all branches to have the same type. That just means that if one branch of a cond has an effect, we need to lift all the other branches to have that effect. Hence why this checking rule requires all branches to have the same type.

WDYT?

mattjj

comment created time in 21 hours

PullRequestReviewEvent

Pull request review commentgoogle/jax

add jaxpr parts of simple effect types

 def write(x: Atom, b: bool) -> None:   new_jaxpr = Jaxpr((),                     [v for v, b in zip(jaxpr.invars, used_inputs)   if b],                     [v for v, b in zip(jaxpr.outvars, used_outputs) if b],-                    new_eqns[::-1])+                    new_eqns[::-1], False)

I hardcoded this here based on an idea (from the "IO and RNG" effects doc draft) that linear jaxprs would be effect-free. That is, we wrote signatures like this:

io_jvp : (a -> {IO} b) -> (a, T a) -> {IO} (b, T b)
io_lin : (a -> {IO} b) -> a -> {IO} (b, T a -o T b)
io_vjp : (a -> {IO} b) -> a -> {IO} (b, T b -o T a)

However, while that's likely true for some effects (particularly the ones considered in that IO and RNG doc), it may not be true for all. In particular, we might want debug prints in the backward pass!

Given we're now focused on effects which we want to include in linear jaxprs, I will indeed remove this hardcoding!

mattjj

comment created time in 21 hours

PullRequestReviewEvent

Pull request review commentgoogle/jax

add jax.experimental.checkify transformation

+# Copyright 2021 Google LLC+#+# Licensed under the Apache License, Version 2.0 (the "License");+# you may not use this file except in compliance with the License.+# You may obtain a copy of the License at+#+#     https://www.apache.org/licenses/LICENSE-2.0+#+# Unless required by applicable law or agreed to in writing, software+# distributed under the License is distributed on an "AS IS" BASIS,+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+# See the License for the specific language governing permissions and+# limitations under the License.++from dataclasses import dataclass+from functools import partial+import itertools as it+from typing import Union, Optional, Callable, Dict++import numpy as np++import jax.numpy as jnp++from jax import core+from jax import linear_util as lu+from jax.api_util import flatten_fun+from jax.interpreters import partial_eval as pe+from jax.tree_util import tree_flatten, tree_unflatten, register_pytree_node+from jax._src import source_info_util, traceback_util+from jax._src.lax import lax+from jax._src.lax import slicing+from jax._src.lax import control_flow+from jax._src.util import as_hashable_function, unzip2++source_info_util.register_exclusion(__file__)+traceback_util.register_exclusion(__file__)+++## Utils++def popattr(obj, attrname):+  val = getattr(obj, attrname)+  delattr(obj, attrname)+  return val++def setnewattr(obj, name, val):+  sentinel = object()+  assert getattr(obj, name, sentinel) is sentinel+  setattr(obj, name, val)++## Error value data type and functional assert.++@dataclass(frozen=True)+class Error:+  err: Union[bool, core.Tracer]+  code: Union[int, core.Tracer]+  msgs: Dict[int, str]++  def get(self) -> Optional[str]:+    assert np.shape(self.err) == np.shape(self.code)+    if np.size(self.err) == 1:+      if self.err:+        return self.msgs[int(self.code)]+    else:+      return '\n'.join(f'at mapped index {", ".join(map(str, idx))}: '  # type: ignore+                       f'{self.msgs[int(self.code[idx])]}'              # type: ignore+                       for idx, e in np.ndenumerate(self.err) if e) or None+    return None++register_pytree_node(Error,+                     lambda e: ((e.err, e.code), tuple(sorted(e.msgs.items()))),+                     lambda msgs, data: Error(*data, dict(msgs)))  # type: ignore++init_error = Error(False, 0, {})+next_code = it.count(1).__next__  # globally unique ids, could be uuid4+++Bool = Union[bool, core.Tracer]+Int = Union[int, core.Tracer]++def assert_func(error: Error, pred: Bool, msg: str) -> Error:+  code = next_code()+  out_err = error.err | jnp.logical_not(pred)+  out_code = lax.select(error.err, error.code, code)+  return Error(out_err, out_code, {code: msg, **error.msgs})++## Checkify transformation for plumbing functional error values.++class ErrorTrace(core.Trace):+  pure = lift = sublift = lambda self, val: ErrorTracer(self, val)++  def process_primitive(self, primitive, tracers, params):+    in_vals = [t.val for t in tracers]+    rule = error_checks.get(primitive)+    if rule:+      out, self.main.error = rule(self.main.error, *in_vals, **params)+    else:+      out = primitive.bind(*in_vals, **params)+    if primitive.multiple_results:+      return [ErrorTracer(self, x) for x in out]+    else:+      return ErrorTracer(self, out)++  def process_call(self, primitive, f, tracers, params):+    in_vals = [t.val for t in tracers]+    e = popattr(self.main, 'error')

Yeah exactly, just to be safe. (Sorry for mostly answering the wrong question!)

mattjj

comment created time in a day

PullRequestReviewEvent

Pull request review commentgoogle/jax

add jax.experimental.checkify transformation

+# Copyright 2021 Google LLC+#+# Licensed under the Apache License, Version 2.0 (the "License");+# you may not use this file except in compliance with the License.+# You may obtain a copy of the License at+#+#     https://www.apache.org/licenses/LICENSE-2.0+#+# Unless required by applicable law or agreed to in writing, software+# distributed under the License is distributed on an "AS IS" BASIS,+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+# See the License for the specific language governing permissions and+# limitations under the License.++from dataclasses import dataclass+from functools import partial+import itertools as it+from typing import Union, Optional, Callable, Dict++import numpy as np++import jax.numpy as jnp++from jax import core+from jax import linear_util as lu+from jax.api_util import flatten_fun+from jax.interpreters import partial_eval as pe+from jax.tree_util import tree_flatten, tree_unflatten, register_pytree_node+from jax._src import source_info_util, traceback_util+from jax._src.lax import lax+from jax._src.lax import slicing+from jax._src.lax import control_flow+from jax._src.util import as_hashable_function, unzip2++source_info_util.register_exclusion(__file__)+traceback_util.register_exclusion(__file__)+++## Utils++def popattr(obj, attrname):+  val = getattr(obj, attrname)+  delattr(obj, attrname)+  return val++def setnewattr(obj, name, val):+  sentinel = object()+  assert getattr(obj, name, sentinel) is sentinel+  setattr(obj, name, val)++## Error value data type and functional assert.++@dataclass(frozen=True)+class Error:+  err: Union[bool, core.Tracer]+  code: Union[int, core.Tracer]+  msgs: Dict[int, str]++  def get(self) -> Optional[str]:+    assert np.shape(self.err) == np.shape(self.code)+    if np.size(self.err) == 1:+      if self.err:+        return self.msgs[int(self.code)]+    else:+      return '\n'.join(f'at mapped index {", ".join(map(str, idx))}: '  # type: ignore+                       f'{self.msgs[int(self.code[idx])]}'              # type: ignore+                       for idx, e in np.ndenumerate(self.err) if e) or None+    return None++register_pytree_node(Error,+                     lambda e: ((e.err, e.code), tuple(sorted(e.msgs.items()))),+                     lambda msgs, data: Error(*data, dict(msgs)))  # type: ignore++init_error = Error(False, 0, {})+next_code = it.count(1).__next__  # globally unique ids, could be uuid4+++Bool = Union[bool, core.Tracer]+Int = Union[int, core.Tracer]++def assert_func(error: Error, pred: Bool, msg: str) -> Error:+  code = next_code()+  out_err = error.err | jnp.logical_not(pred)+  out_code = lax.select(error.err, error.code, code)+  return Error(out_err, out_code, {code: msg, **error.msgs})++## Checkify transformation for plumbing functional error values.++class ErrorTrace(core.Trace):+  pure = lift = sublift = lambda self, val: ErrorTracer(self, val)++  def process_primitive(self, primitive, tracers, params):+    in_vals = [t.val for t in tracers]+    rule = error_checks.get(primitive)+    if rule:+      out, self.main.error = rule(self.main.error, *in_vals, **params)+    else:+      out = primitive.bind(*in_vals, **params)+    if primitive.multiple_results:+      return [ErrorTracer(self, x) for x in out]+    else:+      return ErrorTracer(self, out)++  def process_call(self, primitive, f, tracers, params):+    in_vals = [t.val for t in tracers]+    e = popattr(self.main, 'error')

We want the error value (i.e. the boolean and code) to be threaded through the call in the sense that it should be passed as an argument to the call application (in the jaxpr) and returned as an output.

When not dealing with higher-order primitives like this, we just keep the error value stuck on the MainTrace instance, as global data. (A cleaner version of this is global_data in Autodidax, an explicit attribute on MainTrace, but it's the same idea.)

So at this line in the code we want to get the error value from the MainTrace instance so that we can pass it in as an argument to the called function. Then just on the "other side" of the call, we take that passed-in argument and glue it back on the MainTrace instance. In short, we take off the error value, pass it through the call as an explicit argument, then immediately glue it back on.

In that first step, we don't actually have to pop/delete the error attribute from the MainTrace instance, but I thought it was cleaner that way.

WDYT?

mattjj

comment created time in a day

PullRequestReviewEvent

Pull request review commentgoogle/jax

add jax.experimental.checkify transformation

+# Copyright 2021 Google LLC+#+# Licensed under the Apache License, Version 2.0 (the "License");+# you may not use this file except in compliance with the License.+# You may obtain a copy of the License at+#+#     https://www.apache.org/licenses/LICENSE-2.0+#+# Unless required by applicable law or agreed to in writing, software+# distributed under the License is distributed on an "AS IS" BASIS,+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+# See the License for the specific language governing permissions and+# limitations under the License.++from dataclasses import dataclass+from functools import partial+import itertools as it+from typing import Union, Optional, Callable, Dict++import numpy as np++import jax.numpy as jnp++from jax import core+from jax import linear_util as lu+from jax.api_util import flatten_fun+from jax.interpreters import partial_eval as pe+from jax.tree_util import tree_flatten, tree_unflatten, register_pytree_node+from jax._src import source_info_util, traceback_util+from jax._src.lax import lax+from jax._src.lax import slicing+from jax._src.lax import control_flow+from jax._src.util import as_hashable_function, unzip2++source_info_util.register_exclusion(__file__)+traceback_util.register_exclusion(__file__)+++## Utils++def popattr(obj, attrname):+  val = getattr(obj, attrname)+  delattr(obj, attrname)+  return val++def setnewattr(obj, name, val):+  sentinel = object()+  assert getattr(obj, name, sentinel) is sentinel+  setattr(obj, name, val)++## Error value data type and functional assert.++@dataclass(frozen=True)+class Error:+  err: Union[bool, core.Tracer]+  code: Union[int, core.Tracer]+  msgs: Dict[int, str]++  def get(self) -> Optional[str]:+    assert np.shape(self.err) == np.shape(self.code)+    if np.size(self.err) == 1:+      if self.err:+        return self.msgs[int(self.code)]+    else:+      return '\n'.join(f'at mapped index {", ".join(map(str, idx))}: '  # type: ignore+                       f'{self.msgs[int(self.code[idx])]}'              # type: ignore+                       for idx, e in np.ndenumerate(self.err) if e) or None+    return None++register_pytree_node(Error,+                     lambda e: ((e.err, e.code), tuple(sorted(e.msgs.items()))),+                     lambda msgs, data: Error(*data, dict(msgs)))  # type: ignore++init_error = Error(False, 0, {})+next_code = it.count(1).__next__  # globally unique ids, could be uuid4+++Bool = Union[bool, core.Tracer]+Int = Union[int, core.Tracer]++def assert_func(error: Error, pred: Bool, msg: str) -> Error:+  code = next_code()+  out_err = error.err | jnp.logical_not(pred)+  out_code = lax.select(error.err, error.code, code)+  return Error(out_err, out_code, {code: msg, **error.msgs})++## Checkify transformation for plumbing functional error values.++class ErrorTrace(core.Trace):+  pure = lift = sublift = lambda self, val: ErrorTracer(self, val)++  def process_primitive(self, primitive, tracers, params):+    in_vals = [t.val for t in tracers]+    rule = error_checks.get(primitive)+    if rule:+      out, self.main.error = rule(self.main.error, *in_vals, **params)+    else:+      out = primitive.bind(*in_vals, **params)+    if primitive.multiple_results:+      return [ErrorTracer(self, x) for x in out]+    else:+      return ErrorTracer(self, out)++  def process_call(self, primitive, f, tracers, params):+    in_vals = [t.val for t in tracers]+    e = popattr(self.main, 'error')+    f, msgs = check_errors_subtrace(f, self.main, tuple(e.msgs.items()))+    params_ = dict(params, donated_invars=(False, False, *params['donated_invars']))+    err, code, *out_vals = primitive.bind(f, e.err, e.code, *in_vals, **params_)+    setnewattr(self.main, 'error', Error(err, code, msgs()))+    return [ErrorTracer(self, x) for x in out_vals]++  def process_map(self, primitive, f, tracers, params):+    in_vals = [t.val for t in tracers]+    e = popattr(self.main, 'error')+    f, msgs = check_errors_subtrace(f, self.main, tuple(e.msgs.items()))++    @as_hashable_function(closure=params['out_axes_thunk'])+    def new_out_axes_thunk():+      return (0, 0, *params['out_axes_thunk']())++    params_ = dict(params, in_axes=(None, None, *params['in_axes']),+                   out_axes_thunk=new_out_axes_thunk,+                   donated_invars=(False, False, *params['donated_invars']))+    errs, codes, *outs = primitive.bind(f, e.err, e.code, *in_vals, **params_)+    err, code = _reduce_any_error(errs, codes)

Ah, good question. Yes what you said is accurate. I called it reduce_any because it's like a jnp.any, in that we want the output error boolean to be True if any of the input boolean array errs is True.

As for messages, actually because lax.sort_key_val is stable by default, the returned error message isn't an arbitrary one, but rather it's the one with the lowest index along the leading axis. But there are alternatives: we could also have a way to merge error messages (e.g. joining them with newlines) and thus report all the errors in the map. We should play with that!

mattjj

comment created time in a day

PullRequestReviewEvent

push eventmattjj/jax

Matthew Johnson

commit sha 659f8b794ff6b64c1264ce6398dfbf4e0a4b19bc

add skeleton checkify transformation

view details

push time in a day

push eventmattjj/jax

Matthew Johnson

commit sha fe1da12047003b6238c60cbacbdf535f82b69d51

add skeleton checkify transformation

view details

push time in a day

PR opened google/jax

Reviewers
add jax.experimental.checkify transformation

cf. #8468

+530 -5

0 comment

5 changed files

pr created time in a day

create barnchmattjj/jax

branch : checkify

created branch time in a day

issue commentgoogle/jax

Supported way to access MHLO from `lower()`

You can get the flat output avals or the pytree of output avals like this:

import jax
import jax.numpy as jnp
from jax.tree_util import tree_unflatten

@jax.jit
def f(x, y):
  return x * y + 2

l = f.lower(jnp.zeros((2, 3)), jnp.zeros((3,)))

flat_out_avals = l._lowering.compile_args[5]
out_avals_tree = tree_unflatten(l.out_tree, flat_out_avals)

The magic constant 5 comes from the way the XlaComputation constructor is called and how instances of that class just keep a tuple of arguments to pass to XlaCompiledComputation.from_xla_computation.

stellaraccident

comment created time in 2 days

PullRequestReviewEvent
PullRequestReviewEvent

Pull request review commentgoogle/jax

add more detail about aux_data in pytree docs

 show_example(Special(1., 2.)) ```  The set of Python types that are considered internal pytree nodes is extensible,-through a global registry of types, and values of registered types are traversed-recursively. To register a new type, you can use-{func}`~jax.tree_util.register_pytree_node`:+through a global registry of types. Values of registered types are traversed+recursively, so a custom pytree type can itself contain pytrees.++To register a new type, you can use {func}`~jax.tree_util.register_pytree_node`+to specify how to flatten and unflatten instances of the new type. That is, you+register two functions: the first function accepts instances of the new type and+returns a pair where the first element is an iterable of children (for example,+the values of a dictionary) and the second element is auxiliary metadata needed

Good idea. Though I don't like the word "static", I'll figure out a way to express that.

mattjj

comment created time in 2 days

PullRequestReviewEvent
more