1 /* Copyright 2017 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 16 #include "tensorflow/compiler/xla/service/tuple_points_to_analysis.h" 17 18 #include <ostream> 19 #include <utility> 20 #include <vector> 21 22 #include "tensorflow/compiler/xla/map_util.h" 23 #include "tensorflow/compiler/xla/service/hlo_instruction.h" 24 #include "tensorflow/compiler/xla/shape_util.h" 25 #include "tensorflow/compiler/xla/types.h" 26 #include "tensorflow/compiler/xla/util.h" 27 #include "tensorflow/core/lib/core/errors.h" 28 #include "tensorflow/core/lib/strings/str_util.h" 29 #include "tensorflow/core/lib/strings/strcat.h" 30 #include "tensorflow/core/lib/strings/stringprintf.h" 31 #include "tensorflow/core/platform/logging.h" 32 33 namespace xla { 34 35 string BufferAlias::ToString() const { 36 return tensorflow::strings::StrCat("BufferAlias(", instruction_->name(), "[", 37 tensorflow::str_util::Join(index_, ","), 38 "])"); 39 } 40 41 std::ostream& operator<<(std::ostream& out, const BufferAlias& buffer_alias) { 42 out << buffer_alias.ToString(); 43 return out; 44 } 45 46 bool PointsToSet::IsAmbiguous() const { 47 bool ambiguous = false; 48 ForEachElement( 49 [&ambiguous](const ShapeIndex& /*index*/, const BufferList& points_to) { 50 ambiguous |= points_to.size() > 1; 51 }); 52 return ambiguous; 53 } 54 55 bool PointsToSet::IsDistinct() const { 56 bool distinct = true; 57 std::set<const LogicalBuffer*> all_points_to; 58 ForEachElement([&distinct, &all_points_to](const ShapeIndex& /*index*/, 59 const BufferList& points_to) { 60 for (auto& buffer : points_to) { 61 if (all_points_to.count(buffer) != 0) { 62 distinct = false; 63 } 64 all_points_to.insert(buffer); 65 } 66 }); 67 return distinct; 68 } 69 70 size_t PointsToSet::size() const { 71 // Because pointed-to elements may be duplicated we have to create a flattened 72 // set and return the size. 73 return CreateFlattenedSet().size(); 74 } 75 76 PointsToSet::BufferSet PointsToSet::CreateFlattenedSet() const { 77 BufferSet flat_set; 78 ForEachElement( 79 [&flat_set](const ShapeIndex& /*index*/, const BufferList& buffers) { 80 flat_set.insert(buffers.begin(), buffers.end()); 81 }); 82 return flat_set; 83 } 84 85 bool PointsToSet::ContainsBuffer(const LogicalBuffer& buffer) const { 86 bool found = false; 87 ForEachElement([&found, &buffer](const ShapeIndex& /*index*/, 88 const BufferList& pointed_to_buffers) { 89 if (!found && 90 std::find(pointed_to_buffers.begin(), pointed_to_buffers.end(), 91 &buffer) != pointed_to_buffers.end()) { 92 found = true; 93 } 94 }); 95 return found; 96 } 97 98 bool PointsToSet::ContainsBufferAtIndex(const LogicalBuffer& buffer, 99 const ShapeIndex& index) const { 100 const auto& pointed_to_buffers = element(index); 101 return std::find(pointed_to_buffers.begin(), pointed_to_buffers.end(), 102 &buffer) != pointed_to_buffers.end(); 103 } 104 105 void PointsToSet::AddPointedToBuffer(const LogicalBuffer& buffer, 106 const ShapeIndex& index) { 107 if (ContainsBufferAtIndex(buffer, index)) { 108 return; 109 } 110 mutable_element(index)->push_back(&buffer); 111 } 112 113 const PointsToSet::SourceSet& PointsToSet::tuple_sources( 114 const ShapeIndex& index) const { 115 return tree_.element(index).tuple_sources; 116 } 117 118 void PointsToSet::add_tuple_source(const ShapeIndex& index, 119 HloInstruction* tuple) { 120 tree_.mutable_element(index)->tuple_sources.insert(tuple); 121 } 122 123 namespace { 124 125 // Gather fusion instructions from 'instruction' into 'fusion_instructions'. 126 void GatherFusionInstructions( 127 HloInstruction* instruction, 128 std::vector<HloInstruction*>* fusion_instructions) { 129 CHECK_EQ(HloOpcode::kFusion, instruction->opcode()); 130 for (auto* fused : instruction->fused_instructions()) { 131 if (fused->opcode() == HloOpcode::kFusion) { 132 GatherFusionInstructions(fused, fusion_instructions); 133 } 134 } 135 fusion_instructions->push_back(instruction); 136 } 137 138 } // namespace 139 140 /* static */ StatusOr<std::unique_ptr<TuplePointsToAnalysis>> 141 TuplePointsToAnalysis::Run(const HloModule* module) { 142 auto logical_buffer_analysis = LogicalBufferAnalysis::Run(module); 143 std::unique_ptr<TuplePointsToAnalysis> analysis(new TuplePointsToAnalysis( 144 module, logical_buffer_analysis.ConsumeValueOrDie())); 145 TF_RETURN_IF_ERROR(analysis->Analyze()); 146 return std::move(analysis); 147 } 148 149 Status TuplePointsToAnalysis::Analyze() { 150 per_instruction_.clear(); 151 per_instruction_.resize(module_->NumUniqueInstructionIds()); 152 153 logical_buffer_aliases_.clear(); 154 logical_buffer_aliases_.resize( 155 logical_buffer_analysis_->num_logical_buffers()); 156 157 std::vector<HloInstruction*> fusion_instructions; 158 for (auto* computation : module_->MakeNonfusionComputations()) { 159 TF_RETURN_IF_ERROR(computation->Accept(this)); 160 TF_RETURN_IF_ERROR( 161 PopulateDefinedBuffersAndAliases(computation->instructions())); 162 for (auto* instruction : computation->instructions()) { 163 if (instruction->opcode() == HloOpcode::kFusion) { 164 GatherFusionInstructions(instruction, &fusion_instructions); 165 } 166 } 167 } 168 // Run points-to analysis on fusion instructions in 'computation'. 169 for (auto* instruction : fusion_instructions) { 170 TF_RETURN_IF_ERROR(instruction->fused_expression_root()->Accept(this)); 171 TF_RETURN_IF_ERROR( 172 PopulateDefinedBuffersAndAliases(instruction->fused_instructions())); 173 } 174 175 XLA_VLOG_LINES(3, ToString()); 176 177 return Status::OK(); 178 } 179 180 Status TuplePointsToAnalysis::PopulateDefinedBuffersAndAliases(const decltype( 181 std::declval<HloComputation>().instructions())& instructions) { 182 for (auto* instruction : instructions) { 183 PerInstruction* pi = PerInst(instruction); 184 TF_RETURN_IF_ERROR(GatherBuffersDefinedByInstruction( 185 instruction, &pi->instruction_defined_buffers)); 186 187 const PointsToSet& points_to_set = GetPointsToSet(instruction); 188 points_to_set.ForEachElement( 189 [this, &instruction]( 190 const ShapeIndex& index, 191 const PointsToSet::BufferList& pointed_to_buffers) { 192 for (const LogicalBuffer* buffer : pointed_to_buffers) { 193 logical_buffer_aliases_[buffer->id()].emplace_back(instruction, 194 index); 195 } 196 }); 197 } 198 return Status::OK(); 199 } 200 201 Status TuplePointsToAnalysis::DefaultAction(HloInstruction* hlo_instruction) { 202 // Create trivial points-to set for instruction. Each points-to set at index i 203 // contains a single element LogicalBuffer(hlo_instruction, i). This indicates 204 // that this instruction is the source of all buffers in its own output. 205 PointsToSet& points_to_set = CreateEmptyPointsToSet(hlo_instruction); 206 points_to_set.ForEachMutableElement( 207 [this, hlo_instruction](const ShapeIndex& index, 208 PointsToSet::BufferList* buffers) { 209 buffers->push_back( 210 &logical_buffer_analysis_->GetBuffer(hlo_instruction, index)); 211 }); 212 213 if (ShapeUtil::IsTuple(hlo_instruction->shape())) { 214 // If the hlo instruction is a tuple-shaped, then trivially the instruction 215 // itself is the source of the tuple. 216 points_to_set.add_tuple_source({}, hlo_instruction); 217 } 218 219 return Status::OK(); 220 } 221 222 Status TuplePointsToAnalysis::HandleGetTupleElement( 223 HloInstruction* get_tuple_element) { 224 // GetTupleElement forwards a pointer to a particular element of the tuple 225 // operand. 226 int64 element_index = get_tuple_element->tuple_index(); 227 228 PointsToSet& points_to_set = CreateEmptyPointsToSet(get_tuple_element); 229 const PointsToSet& operand_points_to_set = 230 *PerInst(get_tuple_element->operand(0))->points_to_set; 231 232 // Copy the points-to set (and tuple sources) at index {element_index} of the 233 // operand to the points-to set for this GetTupleElement instruction. 234 points_to_set.ForEachMutableElement( 235 [&, this](const ShapeIndex& target_index, 236 PointsToSet::BufferList* points_to) { 237 // Construct an index into the operand by prepending element_index to 238 // the index for the GetTupleElement instruction's points-to set. 239 ShapeIndex src_index; 240 src_index.push_back(element_index); 241 for (auto element : target_index) { 242 src_index.push_back(element); 243 } 244 245 *points_to = operand_points_to_set.element(src_index); 246 for (HloInstruction* tuple : 247 operand_points_to_set.tuple_sources(src_index)) { 248 points_to_set.add_tuple_source(target_index, tuple); 249 } 250 }); 251 252 return Status::OK(); 253 } 254 255 Status TuplePointsToAnalysis::HandleCopy(HloInstruction* copy) { 256 // A kCopy instruction performs a shallow copy of the operand. The top-level 257 // buffer (index={}) is newly created, but all other buffers (in the case of a 258 // tuple shape) come from the operand 259 PointsToSet& points_to_set = CreateCopiedPointsToSet(copy, copy->operand(0)); 260 points_to_set.mutable_element(/*index=*/{})->clear(); 261 points_to_set.AddPointedToBuffer( 262 logical_buffer_analysis_->GetBuffer(copy, /*index=*/{}), 263 /*index=*/{}); 264 265 return Status::OK(); 266 } 267 268 Status TuplePointsToAnalysis::HandleBitcast(HloInstruction* bitcast) { 269 // A kBitcast instruction aliases its operand. That is, the buffer of its 270 // result *is* the buffer of its operand, so just copy the operands points-to 271 // set. 272 CreateCopiedPointsToSet(bitcast, bitcast->operand(0)); 273 return Status::OK(); 274 } 275 276 Status TuplePointsToAnalysis::HandleSlice(HloInstruction* slice) { 277 // A kSlice instruction aliases its operand if the backend lowers it to an 278 // in-place implementation. 279 if (slice->IsInPlaceSlice()) { 280 CreateCopiedPointsToSet(slice, slice->operand(0)); 281 return Status::OK(); 282 } 283 return DefaultAction(slice); 284 } 285 286 Status TuplePointsToAnalysis::HandleRecvDone(HloInstruction* recv_done) { 287 // RecvDone aliases its input (Recv) tuple element {0} to its output. 288 PointsToSet& points_to_set = CreateEmptyPointsToSet(recv_done); 289 const PointsToSet& operand_points_to_set = 290 GetPointsToSet(recv_done->operand(0)); 291 292 // Recursively copy the points to set of the operand tuple {0}. 293 points_to_set.ForEachMutableElement( 294 [this, &points_to_set, &operand_points_to_set]( 295 const ShapeIndex& index, PointsToSet::BufferList* buffers) { 296 ShapeIndex src_index({0}); 297 for (auto element : index) { 298 src_index.push_back(element); 299 } 300 *buffers = operand_points_to_set.element(src_index); 301 for (auto& tuple_source : 302 operand_points_to_set.tuple_sources(src_index)) { 303 points_to_set.add_tuple_source(index, tuple_source); 304 } 305 }); 306 return Status::OK(); 307 } 308 309 Status TuplePointsToAnalysis::HandleSend(HloInstruction* send) { 310 // Send creates a tuple of {aliased operand, U32 context}. 311 PointsToSet& points_to_set = CreateEmptyPointsToSet(send); 312 313 // Creates the points to set for the tuple and its element at {1}. 314 auto top_buffer = points_to_set.mutable_element(ShapeIndex({})); 315 top_buffer->push_back( 316 &logical_buffer_analysis_->GetBuffer(send, ShapeIndex({}))); 317 points_to_set.add_tuple_source({}, send); 318 319 auto context_buffer = points_to_set.mutable_element(ShapeIndex({1})); 320 context_buffer->push_back( 321 &logical_buffer_analysis_->GetBuffer(send, ShapeIndex({1}))); 322 323 // Recursively copy the points to set of the operand to output tuple {0}. 324 const PointsToSet& operand_points_to_set = GetPointsToSet(send->operand(0)); 325 operand_points_to_set.ForEachElement( 326 [&points_to_set, &operand_points_to_set]( 327 const ShapeIndex& src_index, 328 const PointsToSet::BufferList& points_to) { 329 ShapeIndex target_index({0}); 330 for (auto element : src_index) { 331 target_index.push_back(element); 332 } 333 *points_to_set.mutable_element(target_index) = points_to; 334 335 for (HloInstruction* tuple : 336 operand_points_to_set.tuple_sources(src_index)) { 337 points_to_set.add_tuple_source(target_index, tuple); 338 } 339 }); 340 341 return Status::OK(); 342 } 343 344 Status TuplePointsToAnalysis::HandleTuple(HloInstruction* tuple) { 345 tensorflow::gtl::ArraySlice<HloInstruction*> operands(tuple->operands()); 346 PointsToSet& points_to_set = CreateEmptyPointsToSet(tuple); 347 points_to_set.AddPointedToBuffer( 348 logical_buffer_analysis_->GetBuffer(tuple, /*index=*/{}), 349 /*index=*/{}); 350 351 // A tuple contains references to all input operands and transitively any 352 // references in those operands. 353 for (int64 i = 0; i < operands.size(); ++i) { 354 const PointsToSet& operand_points_to_set = 355 *PerInst(operands[i])->points_to_set; 356 357 // Copy the points-to set (and tuple sources) of the operand into the 358 // respective subtree of the tuple instructions points-to set. 359 operand_points_to_set.ForEachElement( 360 [&points_to_set, &operand_points_to_set, i]( 361 const ShapeIndex& src_index, 362 const PointsToSet::BufferList& points_to) { 363 ShapeIndex target_index; 364 target_index.push_back(i); 365 for (auto element : src_index) { 366 target_index.push_back(element); 367 } 368 369 *points_to_set.mutable_element(target_index) = points_to; 370 371 for (HloInstruction* tuple : 372 operand_points_to_set.tuple_sources(src_index)) { 373 points_to_set.add_tuple_source(target_index, tuple); 374 } 375 }); 376 } 377 378 points_to_set.add_tuple_source({}, tuple); 379 380 return Status::OK(); 381 } 382 383 Status TuplePointsToAnalysis::HandleSelect(HloInstruction* select) { 384 // Select allocates a new buffer and then shallow copies the on_true or 385 // on_false buffer into this new buffer. Which side is chosen cannot be 386 // determined statically so conservatively set the points-to set to the union 387 // of these on_true and on_false operands. 388 // 389 // First create a copy of the on_true points-to set (and tuple sources), then 390 // add in elements of the on_false points-to set (tuple sources). 391 auto on_true = select->operand(1); 392 auto on_false = select->operand(2); 393 PointsToSet& points_to_set = CreateCopiedPointsToSet(select, on_true); 394 const PointsToSet& false_points_to_set = *PerInst(on_false)->points_to_set; 395 points_to_set.ForEachMutableElement( 396 [&](const ShapeIndex& index, PointsToSet::BufferList* buffers) { 397 for (const LogicalBuffer* false_buffer : 398 false_points_to_set.element(index)) { 399 points_to_set.AddPointedToBuffer(*false_buffer, index); 400 } 401 402 for (HloInstruction* tuple : false_points_to_set.tuple_sources(index)) { 403 points_to_set.add_tuple_source(index, tuple); 404 } 405 }); 406 407 // Select creates a new (top-level) buffer to store its result, so its 408 // respective element in the points-to set should contain only itself. 409 points_to_set.mutable_element({})->clear(); 410 points_to_set.AddPointedToBuffer( 411 logical_buffer_analysis_->GetBuffer(select, /*index=*/{}), 412 /*index=*/{}); 413 return Status::OK(); 414 } 415 416 const PointsToSet& TuplePointsToAnalysis::GetPointsToSet( 417 const HloInstruction* hlo_instruction) const { 418 return *PerInst(hlo_instruction)->points_to_set; 419 } 420 421 PointsToSet& TuplePointsToAnalysis::CreateEmptyPointsToSet( 422 const HloInstruction* instruction) { 423 PerInstruction* pi = PerInst(instruction); 424 CHECK(pi->points_to_set == nullptr) 425 << "instruction should not have been present in the map."; 426 auto set = MakeUnique<PointsToSet>(&instruction->shape()); 427 pi->points_to_set = std::move(set); 428 // Return *set using the iterator returned by emplace. 429 return *pi->points_to_set; 430 } 431 432 bool TuplePointsToAnalysis::InstructionDefinesBufferAtIndex( 433 const HloInstruction* instruction, const ShapeIndex& index) const { 434 const auto& buffers = GetPointsToSet(instruction).element(index); 435 return (buffers.size() == 1 && buffers[0]->instruction() == instruction); 436 } 437 438 Status TuplePointsToAnalysis::VerifyBuffer(const LogicalBuffer& buffer) const { 439 if (!InstructionDefinesBufferAtIndex(buffer.instruction(), buffer.index())) { 440 // kSlice ops that are lowered to an in-place version are expected to not 441 // define their output buffer. 442 if (buffer.instruction()->opcode() != HloOpcode::kSlice || 443 !buffer.instruction()->IsInPlaceSlice()) { 444 return FailedPrecondition( 445 "LogicalBuffer %s is ill-defined: instruction %s does not define a " 446 "buffer at that index", 447 buffer.ToString().c_str(), buffer.instruction()->name().c_str()); 448 } 449 } 450 451 if (buffer.id() < 0 || 452 buffer.id() >= logical_buffer_analysis_->num_logical_buffers()) { 453 return FailedPrecondition( 454 "LogicalBuffer %s is ill-defined: invalid id %lld", 455 buffer.ToString().c_str(), buffer.id()); 456 } 457 if (GetBuffer(buffer.id()).instruction() != buffer.instruction() || 458 GetBuffer(buffer.id()).index() != buffer.index()) { 459 return FailedPrecondition( 460 "LogicalBuffer %s is ill-defined: buffer with same id differs: %s", 461 buffer.ToString().c_str(), GetBuffer(buffer.id()).ToString().c_str()); 462 } 463 464 return Status::OK(); 465 } 466 467 const LogicalBuffer& TuplePointsToAnalysis::GetBuffer( 468 LogicalBuffer::Id id) const { 469 CHECK_GE(id, 0); 470 CHECK_LT(id, logical_buffer_analysis_->num_logical_buffers()); 471 return logical_buffer_analysis_->GetBuffer(id); 472 } 473 474 StatusOr<const LogicalBuffer*> TuplePointsToAnalysis::GetBufferDefinedAt( 475 const HloInstruction* instruction, const ShapeIndex& index) const { 476 const auto& buffers = GetPointsToSet(instruction).element(index); 477 if (buffers.size() != 1 || buffers[0]->instruction() != instruction) { 478 return FailedPrecondition( 479 "instruction %s does not define buffer at index {%s}", 480 instruction->name().c_str(), 481 tensorflow::str_util::Join(index, ",").c_str()); 482 } 483 return buffers[0]; 484 } 485 486 const TuplePointsToAnalysis::BufferAliasVector& 487 TuplePointsToAnalysis::GetBufferAliases(const LogicalBuffer& buffer) const { 488 return logical_buffer_aliases_.at(buffer.id()); 489 } 490 491 const TuplePointsToAnalysis::BufferDefinitionVector& 492 TuplePointsToAnalysis::GetBuffersDefinedByInstruction( 493 const HloInstruction* instruction) const { 494 return PerInst(instruction)->instruction_defined_buffers; 495 } 496 497 Status TuplePointsToAnalysis::GatherBuffersDefinedByInstruction( 498 const HloInstruction* instruction, 499 TuplePointsToAnalysis::BufferDefinitionVector* buffers) { 500 GetPointsToSet(instruction) 501 .ForEachElement([this, buffers, instruction]( 502 const ShapeIndex& index, 503 const PointsToSet::BufferList& source_buffers) { 504 // Add buffers which 'instruction' is the source of. 505 CHECK(!source_buffers.empty()); 506 if (source_buffers.size() == 1 && 507 source_buffers[0]->instruction() == instruction) { 508 // If this instruction is the source of this buffer the 509 // indices must match. 510 DCHECK(source_buffers[0]->index() == index); 511 buffers->push_back(source_buffers[0]); 512 } else { 513 // If the points-to set includes more than one buffer then 514 // necessarily this instruction did not produce the 515 // buffer. 516 for (const LogicalBuffer* source_buffer : source_buffers) { 517 DCHECK(source_buffer->instruction() != instruction); 518 } 519 } 520 }); 521 return Status::OK(); 522 } 523 524 PointsToSet& TuplePointsToAnalysis::CreateCopiedPointsToSet( 525 const HloInstruction* instruction, const HloInstruction* src) { 526 // PointsToSet doesn't have a copy constructor so copy over element-by-element 527 // from src PointsToSet. 528 PointsToSet& dst_points_to_set = CreateEmptyPointsToSet(instruction); 529 const PointsToSet& src_points_to_set = GetPointsToSet(src); 530 dst_points_to_set.ForEachMutableElement( 531 [this, &dst_points_to_set, &src_points_to_set]( 532 const ShapeIndex& index, PointsToSet::BufferList* buffers) { 533 *buffers = src_points_to_set.element(index); 534 for (auto& tuple_source : src_points_to_set.tuple_sources(index)) { 535 dst_points_to_set.add_tuple_source(index, tuple_source); 536 } 537 }); 538 return *PerInst(instruction)->points_to_set; 539 } 540 541 string TuplePointsToAnalysis::ToString() const { 542 string output = tensorflow::strings::Printf( 543 "TuplePointsToSet for module %s:\n", module_->name().c_str()); 544 for (const auto* computation : module_->MakeNonfusionComputations()) { 545 const char* entry = 546 computation == module_->entry_computation() ? "entry " : ""; 547 tensorflow::strings::StrAppend(&output, entry, "computation ", 548 computation->name(), ":\n"); 549 for (const HloInstruction* instruction : 550 computation->MakeInstructionPostOrder()) { 551 InstructionToString(instruction, &output); 552 if (instruction->opcode() == HloOpcode::kFusion) { 553 for (auto* fused : instruction->fused_instructions()) { 554 InstructionToString(fused, &output); 555 } 556 } 557 } 558 } 559 560 tensorflow::strings::StrAppend(&output, "LogicalBuffers:\n"); 561 for (const auto& b : logical_buffer_analysis_->logical_buffers()) { 562 tensorflow::strings::StrAppend(&output, " buffer ", b->ToString(), ":\n"); 563 for (const BufferAlias& alias : logical_buffer_aliases_.at(b->id())) { 564 tensorflow::strings::StrAppend(&output, " alias ", alias.ToString(), 565 "\n"); 566 } 567 } 568 return output; 569 } 570 571 void TuplePointsToAnalysis::InstructionToString( 572 const HloInstruction* instruction, string* output) const { 573 const string prefix = instruction->IsFused() ? " " : ""; 574 tensorflow::strings::StrAppend(output, prefix, " instruction ", 575 instruction->ToShortString(), ":\n"); 576 const PointsToSet& points_to_set = GetPointsToSet(instruction); 577 points_to_set.ForEachElement([&prefix, &output]( 578 const ShapeIndex& index, 579 const PointsToSet::BufferList& points_to) { 580 tensorflow::strings::StrAppend( 581 output, prefix, " {", tensorflow::str_util::Join(index, ","), "}: ", 582 tensorflow::str_util::Join( 583 points_to, ", ", 584 [](string* out, const LogicalBuffer* source) { 585 out->append(source->ToString()); 586 }), 587 "\n"); 588 }); 589 } 590 591 } // namespace xla 592