-
Notifications
You must be signed in to change notification settings - Fork 222
Expand file tree
/
Copy pathkernel_cache.cpp
More file actions
679 lines (564 loc) · 22.2 KB
/
Copy pathkernel_cache.cpp
File metadata and controls
679 lines (564 loc) · 22.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
/*
* This file is part of AdaptiveCpp, an implementation of SYCL and C++ standard
* parallelism for CPUs and GPUs.
*
* Copyright The AdaptiveCpp Contributors
*
* AdaptiveCpp is released under the BSD 2-Clause "Simplified" License.
* See file LICENSE in the project root for full license details.
*/
// SPDX-License-Identifier: BSD-2-Clause
#include "hipSYCL/runtime/kernel_cache.hpp"
#include "hipSYCL/common/debug.hpp"
#include "hipSYCL/common/filesystem.hpp"
#include "hipSYCL/common/hcf_container.hpp"
#include "hipSYCL/runtime/kernel_configuration.hpp"
#include "hipSYCL/runtime/backend.hpp"
#include <atomic>
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <memory>
#include <mutex>
#ifdef __linux__
#include <link.h>
#include <dlfcn.h>
#include <cstdio>
#endif
namespace hipsycl {
namespace rt {
namespace {
#ifdef __linux__
using hcf_registration_func = void(*)();
// This function is supposed to scan an ELF object for the content of the
// __acpp_sscp_hcf_registration_func_ptrs section, where the compiler
// has put function ptrs to void() functions, which, when invoked, will
// hand over the HCF content to the runtime.
//
// Behold its unholiness!
static int scan_elf(struct dl_phdr_info *info, size_t size, void *data) {
const char *target_section = "__acpp_sscp_hcf_registration_func_ptrs";
// Base address in RAM where this object is loaded
uintptr_t base = reinterpret_cast<uintptr_t>(info->dlpi_addr);
// This is where the unholiness begins. The actually important parts
// of the section table are not loaded into memory by the dynamic linker
// because it's not needed at runtime.
// Yes, we're going to read ELF headers from disk.
//
// First, get the path of the ELF object.
// For the main executable, dlpi_name is "". We must use /proc/self/exe.
const char *path = (info->dlpi_name && info->dlpi_name[0] != '\0')
? info->dlpi_name
: "/proc/self/exe";
HIPSYCL_DEBUG_INFO << "kernel_cache: ELF scan: Scanning " << path
<< std::endl;
FILE *f = fopen(path, "rb");
if (!f)
return 0;
// Read ELF Header from disk
Elf64_Ehdr ehdr;
if (fread(&ehdr, sizeof(ehdr), 1, f) != 1 ||
memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) {
fclose(f);
return 0;
}
// Read Section Headers from disk
std::vector<Elf64_Shdr> shdrs(ehdr.e_shnum);
fseek(f, ehdr.e_shoff, SEEK_SET);
if (fread(shdrs.data(), sizeof(Elf64_Shdr), ehdr.e_shnum, f) !=
ehdr.e_shnum) {
fclose(f);
return 0;
}
// Read Section Header String Table from disk
std::vector<char> shstrtab(shdrs[ehdr.e_shstrndx].sh_size);
fseek(f, shdrs[ehdr.e_shstrndx].sh_offset, SEEK_SET);
fread(shstrtab.data(), 1, shstrtab.size(), f);
fclose(f); // Finally done with the file on disk
// Find the target section
for (int i = 0; i < ehdr.e_shnum; ++i) {
if (strcmp(shstrtab.data() + shdrs[i].sh_name, target_section) == 0) {
HIPSYCL_DEBUG_INFO << "kernel_cache: ELF scan: Found " << target_section << " section!"
<< std::endl;
// If sh_addr is 0, the section wasn't loaded into memory. Shouldn't happen.
if (shdrs[i].sh_addr == 0)
break;
// UNHOLINESS PEAK:
// sh_addr is the virtual address where the section *should* be.
// By adding the base load address, we get the exact location in RAM.
// Because this is RAM, the function pointers inside are ALREADY RELOCATED
// by ld.so!
const hcf_registration_func *start =
reinterpret_cast<const hcf_registration_func *>(base +
shdrs[i].sh_addr);
size_t count = shdrs[i].sh_size / sizeof(hcf_registration_func);
HIPSYCL_DEBUG_INFO << "kernel_cache: ELF scan: About to invoke " << count
<< " HCF registration function(s)!" << std::endl;
for (size_t j = 0; j < count; ++j) {
auto f_ptr = start[j];
f_ptr(); // At last! Call the registration function
}
break;
}
}
return 0;
}
void scan_binary_for_hcf() {
HIPSYCL_DEBUG_INFO << "kernel_cache: Scanning binary for HCF sections"
<< std::endl;
dl_iterate_phdr(scan_elf, nullptr);
}
#else
void scan_binary_for_hcf() {
// Currently only supported on Linux
}
#endif
template<class F>
void for_each_device_image(const common::hcf_container& hcf, F&& handler) {
if(hcf.root_node()->has_subnode("images")) {
auto* img_node = hcf.root_node()->get_subnode("images");
auto image_names = img_node->get_subnodes();
for(const auto& image_name : image_names) {
handler(img_node->get_subnode(image_name));
}
}
}
template<class F>
void for_each_exported_symbol_list(const common::hcf_container& hcf, F&& handler) {
for_each_device_image(hcf, [&](const common::hcf_container::node* image_node){
auto exported_symbols = image_node->get_as_list("exported-symbols");
handler(image_node, exported_symbols);
});
}
static std::atomic<bool> hcf_cache_shut_down = false;
}
extern "C" void __acpp_register_hcf(const char* hcf, unsigned long long size) {
std::string hcf_data{hcf, size};
hcf_cache::get().register_hcf_object(common::hcf_container{hcf_data});
}
extern "C" void __acpp_unregister_hcf(unsigned long long hcf_object_id) {
if(!hcf_cache_shut_down)
hcf_cache::get().unregister_hcf_object(hcf_object_id);
}
hcf_kernel_info::hcf_kernel_info(
hcf_object_id id, const common::hcf_container::node *kernel_node)
: _id{id} {
if(!kernel_node->has_subnode("image-providers"))
return;
_image_providers = kernel_node->get_as_list("image-providers");
// investigate parameters
auto *parameters_node = kernel_node->get_subnode("parameters");
if (!parameters_node)
return;
std::size_t num_subnodes = parameters_node->get_subnodes().size();
for (int i = 0; i < num_subnodes; ++i) {
const auto *param_info_node =
parameters_node->get_subnode(std::to_string(i));
if (!param_info_node)
return;
auto *byte_size = param_info_node->get_value("byte-size");
auto *byte_offset = param_info_node->get_value("byte-offset");
auto *original_index = param_info_node->get_value("original-index");
auto *type = param_info_node->get_value("type");
if (!byte_size)
return;
if (!byte_offset)
return;
if (!type)
return;
std::size_t arg_size = std::stoll(*byte_size);
std::size_t arg_offset = std::stoll(*byte_offset);
std::size_t arg_original_index = std::stoll(*original_index);
if(*type == "pointer") {
_arg_types.push_back(pointer);
} else {
_arg_types.push_back(other);
}
_arg_offsets.push_back(arg_offset);
_arg_sizes.push_back(arg_size);
_original_arg_indices.push_back(arg_original_index);
// Let's accept annotation nodes not being provided
_string_annotations.push_back({});
_known_annotations.push_back({});
if(auto* annotation_node = param_info_node->get_subnode("annotations")){
for(const auto& entry : annotation_node->key_value_pairs) {
// Ignore entries that are not set to "1" for now
if(entry.second == "1") {
if(entry.first == "specialized") {
_known_annotations.back().push_back(annotation_type::specialized);
} else if(entry.first == "fcall_specialized_config") {
_known_annotations.back().push_back(
annotation_type::fcall_specialized_config);
} else if(entry.first == "restrict") {
_known_annotations.back().push_back(annotation_type::noalias);
} else {
_string_annotations.back().push_back(entry.first);
}
}
}
}
}
if(const auto* flags_node = kernel_node->get_subnode("compile-flags")) {
for(const auto& flag : flags_node->key_value_pairs) {
auto f = to_build_flag(flag.first);
if(f.has_value())
_compilation_flags.push_back(f.value());
}
}
if(const auto* options_node = kernel_node->get_subnode("compile-options")) {
for(const auto& option : options_node->key_value_pairs) {
auto o = to_build_option(option.first);
if(o.has_value())
_compilation_options.push_back(
std::make_pair(o.value(), option.second));
}
}
if(auto* hsps_node = kernel_node->get_subnode("host-side-parameter-sizes")) {
std::size_t num_entries = hsps_node->key_value_pairs.size();
for(int i = 0; i < num_entries; ++i) {
const std::string *s = hsps_node->get_value(std::to_string(i));
if(!s)
return;
_host_side_parameter_sizes.push_back(std::stoll(*s));
}
}
_parsing_successful = true;
}
std::size_t hcf_kernel_info::get_num_parameters() const {
return _arg_sizes.size();
}
const std::vector<std::size_t> &
hcf_kernel_info::get_host_side_parameter_sizes() const {
return _host_side_parameter_sizes;
}
bool hcf_kernel_info::is_valid() const {
return _parsing_successful;
}
std::size_t hcf_kernel_info::get_argument_offset(std::size_t i) const {
return _arg_offsets[i];
}
std::size_t hcf_kernel_info::get_argument_size(std::size_t i) const {
return _arg_sizes[i];
}
std::size_t hcf_kernel_info::get_original_argument_index(std::size_t i) const {
return _original_arg_indices[i];
}
hcf_kernel_info::argument_type hcf_kernel_info::get_argument_type(std::size_t i) const {
return _arg_types[i];
}
const std::vector<std::string> &
hcf_kernel_info::get_string_annotations(std::size_t i) const {
return _string_annotations[i];
}
const std::vector<hcf_kernel_info::annotation_type> &
hcf_kernel_info::get_known_annotations(std::size_t i) const {
return _known_annotations[i];
}
const std::vector<std::string> &
hcf_kernel_info::get_images_containing_kernel() const {
return _image_providers;
}
hcf_object_id hcf_kernel_info::get_hcf_object_id() const {
return _id;
}
const std::vector<kernel_build_flag> &
hcf_kernel_info::get_compilation_flags() const {
return _compilation_flags;
}
const std::vector<std::pair<kernel_build_option, std::string>> &
hcf_kernel_info::get_compilation_options() const {
return _compilation_options;
}
const std::string& hcf_image_info::get_format() const {
return _format;
}
const std::string& hcf_image_info::get_variant() const {
return _variant;
}
hcf_image_info::hcf_image_info(const common::hcf_container *hcf,
const common::hcf_container::node *image_node) {
assert(hcf);
assert(image_node);
if(!image_node->has_key("format"))
return;
if(!image_node->has_key("variant"))
return;
_format = *image_node->get_value("format");
_variant = *image_node->get_value("variant");
// Currently we need to obtain contained kernels list
// by walking through all kernels, and matching against their image-providers.
auto* kernels = hcf->root_node()->get_subnode("kernels");
if(!kernels)
return;
std::string image_name = image_node->node_id;
for(const auto& kernel : kernels->get_subnodes()) {
std::vector<std::string> image_providers =
kernels->get_subnode(kernel)->get_as_list("image-providers");
for(const std::string& provider : image_providers) {
if(provider == image_name)
_contained_kernels.push_back(kernel);
}
}
_parsing_successful = true;
}
const std::vector<std::string> &hcf_image_info::get_contained_kernels() const {
return _contained_kernels;
}
bool hcf_image_info::is_valid() const {
return _parsing_successful;
}
hcf_cache& hcf_cache::get() {
static hcf_cache c;
return c;
}
hcf_cache::~hcf_cache() {
hcf_cache_shut_down = true;
}
hcf_object_id hcf_cache::register_hcf_object(const common::hcf_container &obj) {
std::lock_guard<std::mutex> lock{_mutex};
if (!obj.root_node()->has_key("object-id")) {
HIPSYCL_DEBUG_ERROR
<< "hcf_cache: Invalid hcf object (missing object id)" << std::endl;
}
const std::string *data = obj.root_node()->get_value("object-id");
assert(data);
hcf_object_id id = std::stoull(*data);
HIPSYCL_DEBUG_INFO << "hcf_cache: Registering HCF object " << id << "..." << std::endl;
if (_hcf_objects.count(id) > 0) {
HIPSYCL_DEBUG_INFO << "hcf_cache: Attempted to register an HCF object with an "
"id that is already available; this might either be harmless "
"(e.g. when full ELF scanning is triggered) or could "
"indicate an HCF id collision, which would be very bad."
<< std::endl;
} else {
common::hcf_container* stored_obj = new common::hcf_container{obj};
_hcf_objects[id] = std::unique_ptr<common::hcf_container>{stored_obj};
// Check if the HCF exports some symbols
for_each_exported_symbol_list(
// Don't use obj here, since we have copied it into the cache, and need
// to ensure that the pointers to image nodes are stable
*stored_obj,
[&](const common::hcf_container::node *image_node,
const std::vector<std::string> &exported_symbols) {
for (const auto &symbol : exported_symbols) {
_exported_symbol_providers[symbol].push_back(
device_image_id{id, image_node});
HIPSYCL_DEBUG_INFO << "hcf_cache: Symbol " << symbol
<< " is registered as exported by object " << id
<< " and image " << image_node->node_id
<< " @" << image_node << std::endl;
}
});
// See if stored object has kernel nodes that we can parse
if(auto* kernels_node = stored_obj->root_node()->get_subnode("kernels")) {
for(const auto& kernel_name : kernels_node->get_subnodes()) {
std::unique_ptr<hcf_kernel_info> kernel_info{
new hcf_kernel_info{id, kernels_node->get_subnode(kernel_name)}};
if(kernel_info->is_valid()) {
HIPSYCL_DEBUG_INFO << "hcf_cache: Registering kernel info for kernel "
<< kernel_name << " from HCF object " << id
<< std::endl;
HIPSYCL_DEBUG_INFO << " kernel_info: hcf object id = "
<< kernel_info->get_hcf_object_id() << std::endl;
for(int i = 0; i < kernel_info->get_num_parameters(); ++i) {
HIPSYCL_DEBUG_INFO
<< " kernel_info: parameter " << i
<< ": offset = " << kernel_info->get_argument_offset(i)
<< " size = " << kernel_info->get_argument_size(i)
<< " original index = "
<< kernel_info->get_original_argument_index(i) << std::endl;
}
_hcf_kernel_info[generate_info_id(id, kernel_name)] =
std::move(kernel_info);
}
}
}
// Same for image nodes
if(auto* images_node = stored_obj->root_node()->get_subnode("images")) {
for(const auto& image_name : images_node->get_subnodes()) {
std::unique_ptr<hcf_image_info> image_info{new hcf_image_info{
stored_obj, images_node->get_subnode(image_name)}};
if(image_info->is_valid()) {
HIPSYCL_DEBUG_INFO << "hcf_cache: Registering image info for image "
<< image_name << " from HCF object " << id
<< std::endl;
_hcf_image_info[generate_info_id(id, image_name)] =
std::move(image_info);
}
}
}
}
std::string hcf_dump_dir =
application::get_settings().get<setting::hcf_dump_directory>();
if(!hcf_dump_dir.empty()) {
std::string out_filename = hcf_dump_dir;
if(out_filename.back() != '/' && out_filename.back() != '\\')
out_filename += '/';
out_filename += "hipsycl_object_"+std::to_string(id)+".hcf";
std::ofstream out_file(out_filename.c_str(), std::ios::binary);
if(!out_file.is_open()) {
HIPSYCL_DEBUG_ERROR << "Could not open file " << out_filename
<< " for writing." << std::endl;
} else {
std::string hcf_data = obj.serialize();
out_file.write(hcf_data.c_str(), hcf_data.size());
}
}
return id;
}
void hcf_cache::unregister_hcf_object(hcf_object_id id) {
std::lock_guard<std::mutex> lock{_mutex};
auto it = _hcf_objects.find(id);
if(it != _hcf_objects.end()) {
// First remove the HCF object as a symbol provider for runtime linking and
// symbol resolution. This ensures that it gets no longer selected
// for symbol resolution.
// 1. Go through each symbol list (all device images) exported by this
// HCF file
for_each_exported_symbol_list(
*(it->second), [&](const common::hcf_container::node* image_node,
const std::vector<std::string> &exported_symbols) {
// 2. Iterate over all symbols exported in this HCF
for (const auto &symbol : exported_symbols) {
// 3. Remove all references to this HCF in the symbol providers map
auto& symbol_providers = _exported_symbol_providers[symbol];
symbol_providers.erase(
std::remove_if(symbol_providers.begin(), symbol_providers.end(),
[&](const device_image_id &img) {
return img.hcf_id == id;
}),
symbol_providers.end());
}
});
// Then we can remove the HCF itself.
// Note: We don't necessarily need to remove the HCF kernel info, since
// just maintaining this data won't have any side effects as long as
// the HCF object is no longer selected for execution.
_hcf_objects.erase(id);
}
}
const common::hcf_container* hcf_cache::get_hcf(hcf_object_id obj) const {
std::lock_guard<std::mutex> lock{_mutex};
auto it = _hcf_objects.find(obj);
if(it == _hcf_objects.end())
return nullptr;
return it->second.get();
}
const hcf_kernel_info *
hcf_cache::get_kernel_info(hcf_object_id obj,
std::string_view kernel_name) const {
auto query_cache = [&, this]() -> const hcf_kernel_info* {
std::lock_guard<std::mutex> lock{_mutex};
auto it = _hcf_kernel_info.find(generate_info_id(obj, kernel_name));
if(it != _hcf_kernel_info.end())
return it->second.get();
return nullptr;
};
if(auto* result = query_cache()) {
return result;
} else {
// Could be that we are in a static initialization order scenario;
// try to discover all HCFs by actively scanning the binary
scan_binary_for_hcf();
// Try again
return query_cache();
}
}
const hcf_kernel_info *
hcf_cache::get_kernel_info(hcf_object_id obj,
const std::string &kernel_name) const {
return get_kernel_info(obj, std::string_view{kernel_name});
}
const hcf_image_info *
hcf_cache::get_image_info(hcf_object_id obj,
const std::string &image_name) const {
auto query_cache = [&, this]() -> const hcf_image_info * {
std::lock_guard<std::mutex> lock{_mutex};
auto it = _hcf_image_info.find(generate_info_id(obj, image_name));
if (it == _hcf_image_info.end())
return nullptr;
return it->second.get();
};
if(auto* result = query_cache()) {
return result;
} else {
// Could be that we are in a static initialization order scenario;
// try to discover all HCFs by actively scanning the binary
scan_binary_for_hcf();
// Try again
return query_cache();
}
}
std::shared_ptr<kernel_cache> kernel_cache::get() {
// required since kernel_cache has a private default constructor
struct make_shared_enabler : public kernel_cache {};
static std::shared_ptr<kernel_cache> c = std::make_shared<make_shared_enabler>();
return c;
}
void kernel_cache::unload() {
std::lock_guard<std::mutex> lock{_mutex};
_code_objects.clear();
}
const code_object* kernel_cache::get_code_object(code_object_id id) const {
std::lock_guard<std::mutex> lock{_mutex};
return get_code_object_impl(id);
}
const code_object* kernel_cache::get_code_object_impl(code_object_id id) const {
auto it = _code_objects.find(id);
if(it == _code_objects.end())
return nullptr;
return it->second.get();
}
std::string kernel_cache::get_persistent_cache_file(code_object_id id_of_binary) {
using namespace common::filesystem;
std::string cache_dir = persistent_storage::get().get_jit_cache_dir();
return join_path(cache_dir, kernel_configuration::to_string(id_of_binary)+".jit");
}
bool kernel_cache::persistent_cache_lookup(code_object_id id_of_binary,
std::string &out) const {
std::string filename;
bool filename_lookup_succeeded =
common::filesystem::persistent_storage::get()
.get_this_app_db()
.read_access([&](const common::db::appdb_data &appdb) {
auto binary = appdb.binaries.find(id_of_binary);
if (binary == appdb.binaries.end())
return false;
filename = binary->second.jit_cache_filename;
return true;
});
if(!filename_lookup_succeeded)
return false;
std::ifstream file{filename, std::ios::in | std::ios::binary | std::ios::ate};
if(!file.is_open())
return false;
HIPSYCL_DEBUG_INFO << "kernel_cache: Persistent cache hit for id "
<< kernel_configuration::to_string(id_of_binary)
<< " in file " << filename << std::endl;
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
out.resize(file_size);
file.read(out.data(), file_size);
return true;
}
void kernel_cache::persistent_cache_store(code_object_id id_of_binary,
const std::string &data) const {
if(application::get_settings().get<setting::no_jit_cache_population>())
return;
std::string filename = get_persistent_cache_file(id_of_binary);
HIPSYCL_DEBUG_INFO << "kernel_cache: Storing compiled binary with id "
<< kernel_configuration::to_string(id_of_binary)
<< " in persistent cache file " << filename << std::endl;
if(!common::filesystem::atomic_write(filename, data)) {
HIPSYCL_DEBUG_ERROR
<< "Could not store JIT result in persistent kernel cache in file "
<< filename << std::endl;
}
common::filesystem::persistent_storage::get()
.get_this_app_db()
.read_write_access([&](common::db::appdb_data &appdb) {
appdb.binaries[id_of_binary].jit_cache_filename = filename;
});
}
} // rt
} // hipsycl