Commit Graph

224 Commits

Author SHA1 Message Date
Balaji V. Iyer
adf838daee [mlir][Vectorizer] Added support to Vectorize tensor.unpack (#76087)
Added support to vectorized tensor.unpack. The unpack Op is split into a
`vector.transfer_read`, `vector.transpose`, `vector.shape_cast` and a
`vector.transfer_write`.
2024-02-20 16:10:14 -06:00
srcarroll
9466c4e629 [MLIR][tensor] Improve tensor.pack verifier to catch more cases with unconditional runtime errors (#77217)
Previously, the `tensor.pack` verifier detects unconditional runtime
errors only when tile sizes are static. Now, dynamic tiles are
considered and we only require that the input and either corresponding
tile or output size are static to determine if it will unconditionally
produce errors at runtime.
2024-02-19 12:27:24 -06:00
Max191
7880b2c858 [mlir] Add direct vectorization lowering for tensor.pack ops (#78660)
This PR adds a direct vectorization lowering of `tensor.pack` into
`mask(vector.transfer_read)`->`vector.shape_cast`->`vector.transpose`->`vector.transfer_write`.
2024-02-07 14:11:11 -05:00
Han-Chung Wang
1b7b40bf5d [mlir][Linalg] Support lowerUnPack for identity out_dims_perm cases. (#79594) 2024-01-26 05:46:53 -08:00
Mehdi Amini
3af5ab21b8 Apply clang-tidy fixes for readability-identifier-naming in Transforms.cpp (NFC) 2024-01-22 17:34:55 -08:00
Mehdi Amini
c0fe2b8963 Apply clang-tidy fixes for modernize-loop-convert in Transforms.cpp (NFC) 2024-01-22 17:34:55 -08:00
Matthias Springer
0a8e3dd432 [mlir][Interfaces] DestinationStyleOpInterface: Rename hasTensor/BufferSemantics (#77574)
Rename interface functions as follows:
* `hasTensorSemantics` -> `hasPureTensorSemantics`
* `hasBufferSemantics` -> `hasPureBufferSemantics`

These two functions return "true" if the op has tensor/buffer operands
but not buffer/tensor operands.

Also drop the "ranked" part from the interface, i.e., do not distinguish
between ranked/unranked types.

The new function names describe the functions more accurately. They also
align their semantics with the notion of "tensor semantics" with the
bufferization framework. (An op is supposed to be bufferized if it has
tensor operands, and we don't care if it also has memref operands.)

This change is in preparation of #75273, which adds
`BufferizableOpInterface::hasTensorSemantics`. By renaming the functions
in the `DestinationStyleOpInterface`, we can avoid name clashes between
the two interfaces.
2024-01-12 10:02:54 +01:00
srcarroll
b26ee97537 [MLIR][Linalg] Support dynamic sizes in lower_unpack (#75494) 2023-12-18 19:02:04 +01:00
long.chen
1609f1c2a5 [mlir][affine][nfc] cleanup deprecated T.cast style functions (#71269)
detail see the docment: https://mlir.llvm.org/deprecation/

Not all changes are made manually, most of them are made through a clang
tool I wrote https://github.com/lipracer/cpp-refactor.
2023-11-14 13:01:19 +08:00
qcolombet
7050ff4615 [mlir] Fix lower_unpack when dynamic dimensions are involved (#68423)
When lowering `tensor.unpack`, we need to use the sizes of the
destination tensor in the final `tensor.extract_slice` operation. Prior
to this patch, when the destination tensor had dynamic dimensions, we
would compute them from the result of the `tensor.unpack` operation
instead of its destination argument.

This would produce invalid IR because the `tensor.dim` operations would
need to appear before the `tensor.extract_slice` operation, but the
input of the `tensor.dim` operations would consume the final result of
the lowering of `tensor.unpack`, which happens after the
`tensor.extract_slice` operation. In other words, the definition
wouldn't dominate its uses.

I.e., we were generating:
```
%dynDim = tensor.dim %defLater, ... <-- %defLater defined below
%res = tensor.extract_slice ..., %dynDim, ...
%defLater = linalg.copy (ins %res)
```

Note: I checked the implementation of `lower_pack` and the code is
correct as far as I can tell.
2023-10-06 22:09:58 +02:00
qcolombet
a44b787e06 [MLIR][linalg] Fix unpack rewriter for dynamic shapes (#67096)
Prior to this patch, `GeneralizeOuterUnitDimsUnPackOpPattern` would
assert that we cannot create a `tensor.empty` operation with dynamic
shapes.

The problem stems from the fact that we were not using the right builder
for the `tensor.empty` operation. Indeed, each dynamic dim needs to be
specified by an input variable.

Simply provide the dynamic dimensions to the `tensor.empty` builder to
fix that.
2023-09-22 12:23:47 +02:00
Matthias Springer
0b2197b0cf [mlir][Interfaces] Clean up DestinationStyleOpInterface (#67015)
* "init" operands are specified with `MutableOperandRange` (which gives
access to the underlying `OpOperand *`). No more magic numbers.
* Remove most interface methods and make them helper functions. Only
`getInitsMutable` should be implemented.
* Provide separate helper functions for accessing mutable/immutable
operands (`OpOperand`/`Value`, in line with #66515): `getInitsMutable`
and `getInits` (same naming convention as auto-generated op accessors).
`getInputOperands` was not renamed because this function cannot return a
`MutableOperandRange` (because the operands are not necessarily
consecutive). `OpOperandVector` is no longer needed.
* The new `getDpsInits`/`getDpsInitsMutable` is more efficient than the
old `getDpsInitOperands` because no `SmallVector` is created. The new
functions return a range of operands.
* Fix a bug in `getDpsInputOperands`: out-of-bounds operands were
potentially returned.
2023-09-21 18:04:08 +02:00
Spenser Bauman
f32427e044 [mlir][linalg] Fix lowering of tensor.pack operations
Tensor pack operations are optimistically lowered to pad + insert_slice
when the pack operation only pads the input tensor. The existing
lowering emits insert_slice operations which do not meet the
rank-reducibility requirements of insert_slice.

This change updates the logic in linalg::lowerPack to first check the
rank-reducibility requirement. When the requirement is not met, the
lowering will emit the full sequence of pad + expand + transpose.

Reviewed By: chelini

Differential Revision: https://reviews.llvm.org/D159382
2023-09-05 21:08:13 +02:00
Lorenzo Chelini
d2f2ef84e8 [MLIR][Linalg] Respect DPS in lower_unpack
`tensor.unpack` implements the DPS (Destination Passing Style) interface
and expects the result to be "stored" in the `outs` operand, but this is
not the case with the current decomposition as the final operation is a
`tensor.extract_slice` that does not implement DPS. Add a `linalg.copy`
to fix the problem.

Reviewed By: springerm

Differential Revision: https://reviews.llvm.org/D158393
2023-08-22 09:38:48 +02:00
Lorenzo Chelini
d21beb598f [MLIR][Linalg] Avoid padding attribute in pack when possible
If we deal with statically known tensors and tiles and a given tile
perfectly divides a given dimension, we can omit the padding attribute.
As a bonus point, we can now run pack and unpack propagation
(currently, we bail out during propagation if we have the padding
attribute).

Reviewed By: nicolasvasilache

Differential Revision: https://reviews.llvm.org/D154607
2023-07-11 11:32:51 +02:00
Lorenzo Chelini
4d74c845a1 [MLIR][Linalg] Expose packMatmulGreedily in Transforms.h (NFC)
Make the transformation accessible to other drivers (i.e., passes).
2023-07-06 11:59:17 +02:00
Matthias Springer
be6d96e9f8 [mlir][linalg] Remove redundant dimension size helper functions
Differential Revision: https://reviews.llvm.org/D154211
2023-07-03 09:07:18 +02:00
Matthias Springer
02c662077c [mlir][linalg][NFC] Move padding transformation to separate file
Also remove `LinalgPaddingPattern`, which has no uses. (There is a transform dialect op that is used for testing instead.)

Differential Revision: https://reviews.llvm.org/D153512
2023-06-27 14:45:03 +02:00
Matthias Springer
498f5ae94d [mlir][linalg] Remove duplicate tensor.pad lowering pattern
There is another transform that lowers tensor.pad to tensor.empty + linalg.fill + tensor.insert_slice: `transform.structured.rewrite_in_destination_passing_style`. Delete the other transform.

Differential Revision: https://reviews.llvm.org/D153429
2023-06-22 11:35:27 +02:00
Matthias Springer
6596b0dde8 [mlir][tensor] Clean up tensor::DimOp usage
* Remove duplicate functions. `tensor::getMixedSize` and `tensor::getMixedSizes` should be used.
* Use `tensor::getMixedSize` instead of `createOrFold<tensor::DimOp>`. This is more efficient. `createOrFold` will create an op an immediately try to fold it. In case of a static dimension size, an attribute can be used directly.

Differential Revision: https://reviews.llvm.org/D153332
2023-06-22 10:56:17 +02:00
Matthias Springer
2b7ded215d [mlir][linalg] Add option to pad Linalg ops to a specified multiple
A multiple (int64_t) can optionally be specified for every padding dimension.

Differential Revision: https://reviews.llvm.org/D152262
2023-06-07 08:54:36 +02:00
Matthias Springer
efa16ee20a [mlir][linalg][NFC] Simplify padOperandToSmallestStaticBoundingBox
The implementation is based on `ValueBoundsOpInterface` to compute upper bounds for tensor dim sizes. It is not necessary to skip over certain ops and reify shape dims; `ValueBoundsOpInterface` already takes care of that.

Differential Revision: https://reviews.llvm.org/D152256
2023-06-07 08:54:07 +02:00
Tres Popp
68f58812e3 [mlir] Move casting calls from methods to function calls
The MLIR classes Type/Attribute/Operation/Op/Value support
cast/dyn_cast/isa/dyn_cast_or_null functionality through llvm's doCast
functionality in addition to defining methods with the same name.
This change begins the migration of uses of the method to the
corresponding function call as has been decided as more consistent.

Note that there still exist classes that only define methods directly,
such as AffineExpr, and this does not include work currently to support
a functional cast/isa call.

Context:
- https://mlir.llvm.org/deprecation/ at "Use the free function variants
  for dyn_cast/cast/isa/…"
- Original discussion at https://discourse.llvm.org/t/preferred-casting-style-going-forward/68443

Implementation:
This patch updates all remaining uses of the deprecated functionality in
mlir/. This was done with clang-tidy as described below and further
modifications to GPUBase.td and OpenMPOpsInterfaces.td.

Steps are described per line, as comments are removed by git:
0. Retrieve the change from the following to build clang-tidy with an
   additional check:
   main...tpopp:llvm-project:tidy-cast-check
1. Build clang-tidy
2. Run clang-tidy over your entire codebase while disabling all checks
   and enabling the one relevant one. Run on all header files also.
3. Delete .inc files that were also modified, so the next build rebuilds
   them to a pure state.

```
ninja -C $BUILD_DIR clang-tidy

run-clang-tidy -clang-tidy-binary=$BUILD_DIR/bin/clang-tidy -checks='-*,misc-cast-functions'\
               -header-filter=mlir/ mlir/* -fix

rm -rf $BUILD_DIR/tools/mlir/**/*.inc
```

Differential Revision: https://reviews.llvm.org/D151542
2023-05-26 10:29:55 +02:00
Hanhan Wang
58e4231b34 [mlir][linalg] Fix tensor.pad sizes computation in lowerPack.
The padded sizes should be derived from destination tensor, not source
tensor. There could be more than one incomplete tile in padding domain.

Reviewed By: qedawkins

Differential Revision: https://reviews.llvm.org/D150726
2023-05-18 15:36:50 -07:00
Tres Popp
5550c82189 [mlir] Move casting calls from methods to function calls
The MLIR classes Type/Attribute/Operation/Op/Value support
cast/dyn_cast/isa/dyn_cast_or_null functionality through llvm's doCast
functionality in addition to defining methods with the same name.
This change begins the migration of uses of the method to the
corresponding function call as has been decided as more consistent.

Note that there still exist classes that only define methods directly,
such as AffineExpr, and this does not include work currently to support
a functional cast/isa call.

Caveats include:
- This clang-tidy script probably has more problems.
- This only touches C++ code, so nothing that is being generated.

Context:
- https://mlir.llvm.org/deprecation/ at "Use the free function variants
  for dyn_cast/cast/isa/…"
- Original discussion at https://discourse.llvm.org/t/preferred-casting-style-going-forward/68443

Implementation:
This first patch was created with the following steps. The intention is
to only do automated changes at first, so I waste less time if it's
reverted, and so the first mass change is more clear as an example to
other teams that will need to follow similar steps.

Steps are described per line, as comments are removed by git:
0. Retrieve the change from the following to build clang-tidy with an
   additional check:
   https://github.com/llvm/llvm-project/compare/main...tpopp:llvm-project:tidy-cast-check
1. Build clang-tidy
2. Run clang-tidy over your entire codebase while disabling all checks
   and enabling the one relevant one. Run on all header files also.
3. Delete .inc files that were also modified, so the next build rebuilds
   them to a pure state.
4. Some changes have been deleted for the following reasons:
   - Some files had a variable also named cast
   - Some files had not included a header file that defines the cast
     functions
   - Some files are definitions of the classes that have the casting
     methods, so the code still refers to the method instead of the
     function without adding a prefix or removing the method declaration
     at the same time.

```
ninja -C $BUILD_DIR clang-tidy

run-clang-tidy -clang-tidy-binary=$BUILD_DIR/bin/clang-tidy -checks='-*,misc-cast-functions'\
               -header-filter=mlir/ mlir/* -fix

rm -rf $BUILD_DIR/tools/mlir/**/*.inc

git restore mlir/lib/IR mlir/lib/Dialect/DLTI/DLTI.cpp\
            mlir/lib/Dialect/Complex/IR/ComplexDialect.cpp\
            mlir/lib/**/IR/\
            mlir/lib/Dialect/SparseTensor/Transforms/SparseVectorization.cpp\
            mlir/lib/Dialect/Vector/Transforms/LowerVectorMultiReduction.cpp\
            mlir/test/lib/Dialect/Test/TestTypes.cpp\
            mlir/test/lib/Dialect/Transform/TestTransformDialectExtension.cpp\
            mlir/test/lib/Dialect/Test/TestAttributes.cpp\
            mlir/unittests/TableGen/EnumsGenTest.cpp\
            mlir/test/python/lib/PythonTestCAPI.cpp\
            mlir/include/mlir/IR/
```

Differential Revision: https://reviews.llvm.org/D150123
2023-05-12 11:21:25 +02:00
Hanhan Wang
9d3057c1cf [mlir][Linalg] Add support for lowerPack on dynamic outer shapes.
The revision adds support for tensor.pack op decomposition when all
inner tile sizes are static. The generated tensor.expand_shape op is
still valid because only one of the expanding dimension is dynamic.

Reviewed By: mravishankar

Differential Revision: https://reviews.llvm.org/D150233
2023-05-11 10:47:19 -07:00
Hanhan Wang
1492ae750b [mlir][Linalg] Use ReifyRankedShapedTypeOpInterface for pad transforms.
The information is not tied to tensor.empty op and tensor.extract_slice
op. We can infer smallest static bounding box for pad transform if
they implement ReifyRankedShapedTypeOpInterface. The revision extends
the usability for downstream projects. No tests are added because the
existing tests cover the change, and most of MLIR
ReifyRankedShapedTypeOpInterface ops are covered in the tests, except
tensor.generate and bufferization.alloc_tensor ops.

Reviewed By: mravishankar

Differential Revision: https://reviews.llvm.org/D150227
2023-05-10 17:23:46 -07:00
Quentin Colombet
ad6700b520 [Transform] Support more case for the transform pad operation
Don't choke on `outs` arguments that are not produced by `tensor.empty` or
`tensor.extract_slice`.

When the `outs` argument has a static shape we have all the necessary
information to proceed with the padding.

This makes the `transform.structured.pad` a little bit more resilient.

Differential Revision: https://reviews.llvm.org/D150112
2023-05-08 15:40:38 +02:00
Quinn Dawkins
009c053e3f [mlir][linalg] Allow outer dims perm and untiled dims in pack/unpack generalization
Extends the pack/unpack generalization patterns to work for any packing
op with only full tiles. This produces a combination of rank-reduced
insert/extract slice ops paired with a transpose on the reduced shape,
similar to what the pattern currently produces for fully tiled
pack/unpacks. Note that only the outer dims are rank-reduced in this
pattern, leaving the shape of the inner tile intact.

Differential Revision: https://reviews.llvm.org/D147555
2023-05-02 12:26:45 -04:00
Hanhan Wang
6f87b50be6 [mlir][linalg] Add support for lowering pack with outer_dims_perm.
Reviewed By: chelini, qcolombet

Differential Revision: https://reviews.llvm.org/D148845
2023-04-24 10:39:37 -07:00
Hanhan Wang
ddcc50721a [mlir][linalg] Expose lowerPack and lowerUnPack utils.
Reviewed By: qcolombet

Differential Revision: https://reviews.llvm.org/D148867
2023-04-21 15:23:16 -07:00
Rahul Kayaith
6089d612a5 [mlir] Prevent implicit downcasting to interfaces
Currently conversions to interfaces may happen implicitly (e.g.
`Attribute -> TypedAttr`), failing a runtime assert if the interface
isn't actually implemented. This change marks the `Interface(ValueT)`
constructor as explicit so that a cast is required.

Where it was straightforward to I adjusted code to not require casts,
otherwise I just made them explicit.

Depends on D148491, D148492

Reviewed By: rriddle

Differential Revision: https://reviews.llvm.org/D148493
2023-04-20 16:31:54 -04:00
Nicolas Vasilache
caaa8e5e23 [mlir][Linalg] NFC - Improve debug messages around padding 2023-04-11 06:36:41 -07:00
Matthias Springer
eabb6ccdc8 [mlir][linalg] Replace getUpperBoundForIndex implementation
Use `reifyValueBound` instead, which is more general not hard-coded to a specific list supported ops.

Also add a `closedUB` parameter to the ValueBoundsOpInterface API.

Differential Revision: https://reviews.llvm.org/D146356
2023-04-11 10:32:34 +09:00
Adrian Kuegel
499e1a54f5 [mlir] Apply ClangTidy performance finding (NFC). 2023-03-20 10:14:02 +01:00
Mahesh Ravishankar
809e3d8c98 [mlir][TilingInterface] Modify TilingInterface methods to better return the state of the transformed IR.
Currently the `getTiledImplementation` and `generateResultTileValue`
return just `SmallVector<Operation *>` and `FailureOr<Value>`.

- For `getTiledImplementation` returning empty implies tiling wasnt
  done. There is also an implicit assumption that the tiled operation
  results correspond to the tiled values of the result of the original
  operation. This cannot handle cases where the tiled implementation
  might use multiple operations to compute the tiled value for the
  results of the untiled operation. Sometimes, the tiled operation
  might not directly give the tiled values, and might require casts,
  etc to get a replacement.
- For `generateResultTileValue`, it is assumed that the op defining
  the returned `Value` is the operation that represents the tiled
  computation. Again presence of casts, etc violate this.

Instead make these methods return
```
struct TilingResult {
  SmallVector<Operation *> tiledOps;
  SmallVector<Value> tiledValues;
};
```

The `tiledOps` represent the operations generated that are relevant
for subsequent transformations. The `tiledValues` represent the tiled
values for the results of the original operation. This better
transmits the state of the transformed IR.

As a consequence the following methods also return `FailureOr<TilingResult>`
- `tensor::replaceExtractSliceWithTiledProducer`
- `tensor::bubbleUpPadSlice`

Differential Revision: https://reviews.llvm.org/D145133
2023-03-16 14:29:03 +00:00
Nicolas Vasilache
1cff4cbda3 [mlir][Transform] NFC - Various API cleanups and use RewriterBase in lieu of PatternRewriter
Depends on: D145685

Differential Revision: https://reviews.llvm.org/D145977
2023-03-14 04:23:12 -07:00
Matthias Springer
758329dc7c [mlir][NFC] reifyResultShapes: Add extra error checking
This change adds a new helper function `mlir::reifyResultShapes` that calls the corresponding interface method and also checks the result produced by the implementation when running in debug mode. Bugs due to incorrect interface implementations can be difficult to debug.

This helper function also reduces the amount of code needed at call sites: the cast to `ReifyRankedShapedTypeOpInterface` is done in the helper function.

Differential Revision: https://reviews.llvm.org/D145777
2023-03-10 11:37:54 +01:00
Devajith Valaparambil Sreeramaswamy
991945f441 [mlir][linalg] Downscale 2D convolution with unit dimensions to 1D convolution
Decompose conv_2d -> conv_1d.

This MR follows a similar approach to https://reviews.llvm.org/D112928.

This patch adds support to convert conv_2D operation with either unit height or unit width to conv_1D operation.

This is useful when 2D convolution is tiled to have a single dimension for either height or width and then can be vectorized once it is decomposed into 1D convolution.

This patch https://reviews.llvm.org/D145160 adds vector support for linalg.conv_1d operation and thereby allowing us to vectorize linalg.conv_2d operation after proper tiling.

This missing feature is reported here: https://discourse.llvm.org/t/vectorization-of-convolution-op/60458.

Reviewed By: hanchung

Differential Revision: https://reviews.llvm.org/D145162
2023-03-08 14:31:54 -08:00
Matthias Springer
2a5b13e722 [mlir][Interfaces] ReifyRankedShapedTypeOpInterface returns OpFoldResults
`reifyResultShapes` now returns `OpFoldResult`s instead of `Value`s. This is often more efficient because many transformations immediately attempt to extract a constant from the reified values.

Differential Revision: https://reviews.llvm.org/D145250
2023-03-06 08:41:28 +01:00
Nicolas Vasilache
1ea5b4853e [mlir][Linalg] NFC - Improve debug messages for padding 2023-03-01 23:58:26 -08:00
Nicolas Vasilache
6d2501bf00 [mlir][Linalg] Refactor transform.structured.pad to separate out hoisting
Depends on: D144717

Differential Revision: https://reviews.llvm.org/D144856
2023-02-28 03:26:57 -08:00
Nicolas Vasilache
bb2ae98581 [mlir][Linalg] NFC - Apply cleanups to transforms
Depends on: D144656

Differential Revision: https://reviews.llvm.org/D144717
2023-02-28 03:25:01 -08:00
Quinn Dawkins
bbf1d80d67 [mlir][tensor] Fix transpose permutation in tensor.pack generalization pattern
The generalization pattern for tensor.pack was inverting the
innerDimsPos permutation when normalizing. Thus, the transpose op
produced by the generalization would be incorrect.

Differential Revision: https://reviews.llvm.org/D144425
2023-02-22 14:49:49 -05:00
Hanhan Wang
061201ec3d [mlir][linalg] Enhance padding LinalgOps to handle tensor.empty cases.
Reviewed By: nicolasvasilache

Differential Revision: https://reviews.llvm.org/D143043
2023-02-08 18:39:34 -08:00
Nicolas Vasilache
4ca52c6e7e [mlir][Linalg] Add a transform.structured.lower_pack op
This revision introduces `transform.structured.lower_pack` which allows
rewriting a `tensor.pack` to `tensor.pad` + `tensor.expand_shape` + `linalg.transpose`.

The implementation is currently limited to static pack ops that do not have outer_dims permutations.

Differential Revision: https://reviews.llvm.org/D142881
2023-01-31 10:06:08 -08:00
Alexander Belyaev
dc37dc824a [mlir] Remove Linalg fusion-on-memrefs.
PSA: https://discourse.llvm.org/t/psa-retire-tileandfuselinalgops-method/63850

Differential Revision: https://reviews.llvm.org/D141807
2023-01-30 14:42:00 +01:00
Nicolas Vasilache
1d1a331351 [mlir][Linalg] NFC - Expose packing transpose implementation as a standalone functional-style API call 2023-01-24 07:47:40 -08:00
Nicolas Vasilache
02371c5d66 [mlir][Linalg] NFC - Expose packing implementation as a standalone functional-style API call 2023-01-24 02:27:40 -08:00
Jeff Niu
4d67b27817 [mlir] Add operations to BlockAndValueMapping and rename it to IRMapping
The patch adds operations to `BlockAndValueMapping` and renames it to `IRMapping`. When operations are cloned, old operations are mapped to the cloned operations. This allows mapping from an operation to a cloned operation. Example:

```
Operation *opWithRegion = ...
Operation *opInsideRegion = &opWithRegion->front().front();

IRMapping map
Operation *newOpWithRegion = opWithRegion->clone(map);
Operation *newOpInsideRegion = map.lookupOrNull(opInsideRegion);
```

Migration instructions:
All includes to `mlir/IR/BlockAndValueMapping.h` should be replaced with `mlir/IR/IRMapping.h`. All uses of `BlockAndValueMapping` need to be renamed to `IRMapping`.

Reviewed By: rriddle, mehdi_amini

Differential Revision: https://reviews.llvm.org/D139665
2023-01-12 13:16:05 -08:00