Tidy: modernize-use-nullptr
[alexxy/gromacs.git] / src / gromacs / hardware / hardwaretopology.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2012,2013,2014,2015,2016,2017, by the GROMACS development team, led by
5  * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6  * and including many others, as listed in the AUTHORS file in the
7  * top-level source directory and at http://www.gromacs.org.
8  *
9  * GROMACS is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public License
11  * as published by the Free Software Foundation; either version 2.1
12  * of the License, or (at your option) any later version.
13  *
14  * GROMACS is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with GROMACS; if not, see
21  * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
23  *
24  * If you want to redistribute modifications to GROMACS, please
25  * consider that scientific software is very special. Version
26  * control is crucial - bugs must be traceable. We will be happy to
27  * consider code for inclusion in the official distribution, but
28  * derived work must not be called official GROMACS. Details are found
29  * in the README & COPYING files - if they are missing, get the
30  * official version at http://www.gromacs.org.
31  *
32  * To help us fund GROMACS development, we humbly ask that you cite
33  * the research papers on the package. Check out http://www.gromacs.org.
34  */
35
36 /*! \internal \file
37  * \brief
38  * Implements gmx::HardwareTopology.
39  *
40  * \author Erik Lindahl <erik.lindahl@gmail.com>
41  * \ingroup module_hardware
42  */
43
44 #include "gmxpre.h"
45
46 #include "hardwaretopology.h"
47
48 #include "config.h"
49
50 #include <cstdio>
51
52 #include <algorithm>
53 #include <vector>
54
55 #if GMX_HWLOC
56 #    include <hwloc.h>
57 #endif
58
59 #include "gromacs/hardware/cpuinfo.h"
60 #include "gromacs/utility/gmxassert.h"
61
62 #ifdef HAVE_UNISTD_H
63 #    include <unistd.h>       // sysconf()
64 #endif
65 #if GMX_NATIVE_WINDOWS
66 #    include <windows.h>      // GetSystemInfo()
67 #endif
68
69 //! Convenience macro to help us avoid ifdefs each time we use sysconf
70 #if !defined(_SC_NPROCESSORS_ONLN) && defined(_SC_NPROC_ONLN)
71 #    define _SC_NPROCESSORS_ONLN _SC_NPROC_ONLN
72 #endif
73
74 namespace gmx
75 {
76
77 namespace
78 {
79
80 /*****************************************************************************
81  *                                                                           *
82  *   Utility functions for extracting hardware topology from CpuInfo object  *
83  *                                                                           *
84  *****************************************************************************/
85
86 /*! \brief Initialize machine data from basic information in cpuinfo
87  *
88  *  \param  machine      Machine tree structure where information will be assigned
89  *                       if the cpuinfo object contains topology information.
90  *  \param  supportLevel If topology information is available in CpuInfo,
91  *                       this will be updated to reflect the amount of
92  *                       information written to the machine structure.
93  */
94 void
95 parseCpuInfo(HardwareTopology::Machine *        machine,
96              HardwareTopology::SupportLevel *   supportLevel)
97 {
98     CpuInfo cpuInfo(CpuInfo::detect());
99
100     if (!cpuInfo.logicalProcessors().empty())
101     {
102         int nSockets   = 0;
103         int nCores     = 0;
104         int nHwThreads = 0;
105
106         // Copy the logical processor information from cpuinfo
107         for (auto &l : cpuInfo.logicalProcessors())
108         {
109             machine->logicalProcessors.push_back( { l.socketRankInMachine, l.coreRankInSocket, l.hwThreadRankInCore, -1 } );
110             nSockets   = std::max(nSockets, l.socketRankInMachine);
111             nCores     = std::max(nCores, l.coreRankInSocket);
112             nHwThreads = std::max(nHwThreads, l.hwThreadRankInCore);
113         }
114
115         // Fill info form sockets/cores/hwthreads
116         int socketId   = 0;
117         int coreId     = 0;
118         int hwThreadId = 0;
119
120         machine->sockets.resize(nSockets + 1);
121         for (auto &s : machine->sockets)
122         {
123             s.id = socketId++;
124             s.cores.resize(nCores + 1);
125             for (auto &c : s.cores)
126             {
127                 c.id         = coreId++;
128                 c.numaNodeId = -1; // No numa information
129                 c.hwThreads.resize(nHwThreads + 1);
130                 for (auto &t : c.hwThreads)
131                 {
132                     t.id                 = hwThreadId++;
133                     t.logicalProcessorId = -1; // set as unassigned for now
134                 }
135             }
136         }
137
138         // Fill the logical processor id in the right place
139         for (std::size_t i = 0; i < machine->logicalProcessors.size(); i++)
140         {
141             const HardwareTopology::LogicalProcessor &l = machine->logicalProcessors[i];
142             machine->sockets[l.socketRankInMachine].cores[l.coreRankInSocket].hwThreads[l.hwThreadRankInCore].logicalProcessorId = static_cast<int>(i);
143         }
144         machine->logicalProcessorCount = machine->logicalProcessors.size();
145         *supportLevel                  = HardwareTopology::SupportLevel::Basic;
146     }
147     else
148     {
149         *supportLevel = HardwareTopology::SupportLevel::None;
150     }
151 }
152
153 #if GMX_HWLOC
154
155 #if HWLOC_API_VERSION < 0x00010b00
156 #    define HWLOC_OBJ_PACKAGE  HWLOC_OBJ_SOCKET
157 #    define HWLOC_OBJ_NUMANODE HWLOC_OBJ_NODE
158 #endif
159
160 /*****************************************************************************
161  *                                                                           *
162  *   Utility functions for extracting hardware topology from hwloc library   *
163  *                                                                           *
164  *****************************************************************************/
165
166 /*! \brief Return vector of all descendants of a given type in hwloc topology
167  *
168  *  \param obj   Non-null hwloc object.
169  *  \param type  hwloc object type to find. The routine will only search
170  *               on levels below obj.
171  *
172  *  \return vector containing all the objects of given type that are
173  *          descendants of the provided object. If no objects of this type
174  *          were found, the vector will be empty.
175  */
176 const std::vector<hwloc_obj_t>
177 getHwLocDescendantsByType(const hwloc_obj_t obj, const hwloc_obj_type_t type)
178 {
179     GMX_RELEASE_ASSERT(obj, "NULL hwloc object provided to getHwLocDescendantsByType()");
180
181     std::vector<hwloc_obj_t> v;
182
183     // Go through children; if this object has no children obj->arity is 0,
184     // and we'll return an empty vector.
185     for (std::size_t i = 0; i < obj->arity; i++)
186     {
187         // If the child is the type we're looking for, add it directly.
188         // Otherwise call this routine recursively for each child.
189         if (obj->children[i]->type == type)
190         {
191             v.push_back(obj->children[i]);
192         }
193         else
194         {
195             std::vector<hwloc_obj_t> v2 = getHwLocDescendantsByType(obj->children[i], type);
196             v.insert(v.end(), v2.begin(), v2.end());
197         }
198     }
199     return v;
200 }
201
202 /*! \brief Read information about sockets, cores and threads from hwloc topology
203  *
204  *  \param topo    hwloc topology handle that has been initialized and loaded
205  *  \param machine Pointer to the machine structure in the HardwareTopology
206  *                 class, where the tree of sockets/cores/threads will be written.
207  *
208  *  \return If all the data is found the return value is 0, otherwise non-zero.
209  */
210 int
211 parseHwLocSocketsCoresThreads(const hwloc_topology_t             topo,
212                               HardwareTopology::Machine *        machine)
213 {
214     const hwloc_obj_t              root         = hwloc_get_root_obj(topo);
215     std::vector<hwloc_obj_t>       hwlocSockets = getHwLocDescendantsByType(root, HWLOC_OBJ_PACKAGE);
216
217     machine->logicalProcessorCount = hwloc_get_nbobjs_by_type(topo, HWLOC_OBJ_PU);
218     machine->logicalProcessors.resize(machine->logicalProcessorCount);
219     machine->sockets.resize(hwlocSockets.size());
220
221     bool topologyOk = !hwlocSockets.empty(); // Fail if we have no sockets in machine
222
223     for (std::size_t i = 0; i < hwlocSockets.size() && topologyOk; i++)
224     {
225         // Assign information about this socket
226         machine->sockets[i].id = hwlocSockets[i]->logical_index;
227
228         // Get children (cores)
229         std::vector<hwloc_obj_t> hwlocCores = getHwLocDescendantsByType(hwlocSockets[i], HWLOC_OBJ_CORE);
230         machine->sockets[i].cores.resize(hwlocCores.size());
231
232         topologyOk = topologyOk && !hwlocCores.empty(); // Fail if we have no cores in socket
233
234         // Loop over child cores
235         for (std::size_t j = 0; j < hwlocCores.size() && topologyOk; j++)
236         {
237             // Assign information about this core
238             machine->sockets[i].cores[j].id         = hwlocCores[j]->logical_index;
239             machine->sockets[i].cores[j].numaNodeId = -1;
240
241             // Get children (hwthreads)
242             std::vector<hwloc_obj_t> hwlocPUs = getHwLocDescendantsByType(hwlocCores[j], HWLOC_OBJ_PU);
243             machine->sockets[i].cores[j].hwThreads.resize(hwlocPUs.size());
244
245             topologyOk = topologyOk && !hwlocPUs.empty(); // Fail if we have no hwthreads in core
246
247             // Loop over child hwthreads
248             for (std::size_t k = 0; k < hwlocPUs.size() && topologyOk; k++)
249             {
250                 // Assign information about this hwthread
251                 std::size_t logicalProcessorId                               = hwlocPUs[k]->os_index;
252                 machine->sockets[i].cores[j].hwThreads[k].id                 = hwlocPUs[k]->logical_index;
253                 machine->sockets[i].cores[j].hwThreads[k].logicalProcessorId = logicalProcessorId;
254
255                 if (logicalProcessorId < machine->logicalProcessors.size())
256                 {
257                     // Cross-assign data for this hwthread to the logicalprocess vector
258                     machine->logicalProcessors[logicalProcessorId].socketRankInMachine = static_cast<int>(i);
259                     machine->logicalProcessors[logicalProcessorId].coreRankInSocket    = static_cast<int>(j);
260                     machine->logicalProcessors[logicalProcessorId].hwThreadRankInCore  = static_cast<int>(k);
261                     machine->logicalProcessors[logicalProcessorId].numaNodeId          = -1;
262                 }
263                 else
264                 {
265                     topologyOk = false;
266                 }
267             }
268         }
269     }
270
271     if (topologyOk)
272     {
273         return 0;
274     }
275     else
276     {
277         machine->logicalProcessors.clear();
278         machine->sockets.clear();
279         return -1;
280     }
281 }
282
283 /*! \brief Read cache information from hwloc topology
284  *
285  *  \param topo    hwloc topology handle that has been initialized and loaded
286  *  \param machine Pointer to the machine structure in the HardwareTopology
287  *                 class, where cache data will be filled.
288  *
289  *  \return If any cache data is found the return value is 0, otherwise non-zero.
290  */
291 int
292 parseHwLocCache(const hwloc_topology_t             topo,
293                 HardwareTopology::Machine *        machine)
294 {
295     // Parse caches up to L5
296     for (int cachelevel : { 1, 2, 3, 4, 5})
297     {
298         int depth = hwloc_get_cache_type_depth(topo, cachelevel, HWLOC_OBJ_CACHE_DATA);
299
300         if (depth >= 0)
301         {
302             hwloc_obj_t cache = hwloc_get_next_obj_by_depth(topo, depth, nullptr);
303             if (cache != nullptr)
304             {
305                 std::vector<hwloc_obj_t> hwThreads = getHwLocDescendantsByType(cache, HWLOC_OBJ_PU);
306
307                 machine->caches.push_back( {
308                                                static_cast<int>(cache->attr->cache.depth),
309                                                static_cast<std::size_t>(cache->attr->cache.size),
310                                                static_cast<int>(cache->attr->cache.linesize),
311                                                static_cast<int>(cache->attr->cache.associativity),
312                                                std::max(static_cast<int>(hwThreads.size()), 1)
313                                            } );
314             }
315         }
316     }
317     return machine->caches.empty();
318 }
319
320
321 /*! \brief Read numa information from hwloc topology
322  *
323  *  \param topo    hwloc topology handle that has been initialized and loaded
324  *  \param machine Pointer to the machine structure in the HardwareTopology
325  *                 class, where numa information will be filled.
326  *
327  *  Hwloc should virtually always be able to detect numa information, but if
328  *  there is only a single numa node in the system it is not reported at all.
329  *  In this case we create a single numa node covering all cores.
330  *
331  *  This function uses the basic socket/core/thread information detected by
332  *  parseHwLocSocketsCoresThreads(), which means that routine must have
333  *  completed successfully before calling this one. If this is not the case,
334  *  you will get an error return code.
335  *
336  *  \return If the data found makes sense (either in the numa node or the
337  *          entire machine) the return value is 0, otherwise non-zero.
338  */
339 int
340 parseHwLocNuma(const hwloc_topology_t             topo,
341                HardwareTopology::Machine *        machine)
342 {
343     const hwloc_obj_t        root           = hwloc_get_root_obj(topo);
344     std::vector<hwloc_obj_t> hwlocNumaNodes = getHwLocDescendantsByType(root, HWLOC_OBJ_NUMANODE);
345     bool                     topologyOk     = true;
346
347     if (!hwlocNumaNodes.empty())
348     {
349         machine->numa.nodes.resize(hwlocNumaNodes.size());
350
351         for (std::size_t i = 0; i < hwlocNumaNodes.size(); i++)
352         {
353             machine->numa.nodes[i].id     = hwlocNumaNodes[i]->logical_index;
354             machine->numa.nodes[i].memory = hwlocNumaNodes[i]->memory.total_memory;
355             machine->numa.nodes[i].logicalProcessorId.clear();
356
357             // Get list of PUs in this numa node
358             std::vector<hwloc_obj_t> hwlocPUs = getHwLocDescendantsByType(hwlocNumaNodes[i], HWLOC_OBJ_PU);
359
360             for (auto &p : hwlocPUs)
361             {
362                 machine->numa.nodes[i].logicalProcessorId.push_back(p->os_index);
363
364                 GMX_RELEASE_ASSERT(p->os_index < machine->logicalProcessors.size(), "OS index of PU in hwloc larger than processor count");
365
366                 machine->logicalProcessors[p->os_index].numaNodeId = static_cast<int>(i);
367                 std::size_t s = machine->logicalProcessors[p->os_index].socketRankInMachine;
368                 std::size_t c = machine->logicalProcessors[p->os_index].coreRankInSocket;
369
370                 GMX_RELEASE_ASSERT(s < machine->sockets.size(), "Socket index in logicalProcessors larger than socket count");
371                 GMX_RELEASE_ASSERT(c < machine->sockets[s].cores.size(), "Core index in logicalProcessors larger than core count");
372                 // Set numaNodeId in core too
373                 machine->sockets[s].cores[c].numaNodeId = i;
374             }
375         }
376
377         int depth = hwloc_get_type_depth(topo, HWLOC_OBJ_NUMANODE);
378         const struct hwloc_distances_s * dist = hwloc_get_whole_distance_matrix_by_depth(topo, depth);
379         if (dist != nullptr && dist->nbobjs == hwlocNumaNodes.size())
380         {
381             machine->numa.baseLatency        = dist->latency_base;
382             machine->numa.maxRelativeLatency = dist->latency_max;
383             machine->numa.relativeLatency.resize(dist->nbobjs);
384             for (std::size_t i = 0; i < dist->nbobjs; i++)
385             {
386                 machine->numa.relativeLatency[i].resize(dist->nbobjs);
387                 for (std::size_t j = 0; j < dist->nbobjs; j++)
388                 {
389                     machine->numa.relativeLatency[i][j] = dist->latency[i*dist->nbobjs+j];
390                 }
391             }
392         }
393         else
394         {
395             topologyOk = false;
396         }
397     }
398     else
399     {
400         // No numa nodes found. Use the entire machine as a numa node.
401         const hwloc_obj_t hwlocMachine = hwloc_get_next_obj_by_type(topo, HWLOC_OBJ_MACHINE, nullptr);
402
403         if (hwlocMachine != nullptr)
404         {
405             machine->numa.nodes.resize(1);
406             machine->numa.nodes[0].id           = 0;
407             machine->numa.nodes[0].memory       = hwlocMachine->memory.total_memory;
408             machine->numa.baseLatency           = 10;
409             machine->numa.maxRelativeLatency    = 1;
410             machine->numa.relativeLatency       = { { 1.0 } };
411
412             for (int i = 0; i < machine->logicalProcessorCount; i++)
413             {
414                 machine->numa.nodes[0].logicalProcessorId.push_back(i);
415             }
416             for (auto &l : machine->logicalProcessors)
417             {
418                 l.numaNodeId = 0;
419             }
420             for (auto &s : machine->sockets)
421             {
422                 for (auto &c : s.cores)
423                 {
424                     c.numaNodeId = 0;
425                 }
426             }
427         }
428         else
429         {
430             topologyOk = false;
431         }
432     }
433
434     if (topologyOk)
435     {
436         return 0;
437     }
438     else
439     {
440         machine->numa.nodes.clear();
441         return -1;
442     }
443
444 }
445
446 /*! \brief Read PCI device information from hwloc topology
447  *
448  *  \param topo    hwloc topology handle that has been initialized and loaded
449  *  \param machine Pointer to the machine structure in the HardwareTopology
450  *                 class, where PCI device information will be filled.
451  * *
452  *  \return If any devices were found the return value is 0, otherwise non-zero.
453  */
454 int
455 parseHwLocDevices(const hwloc_topology_t             topo,
456                   HardwareTopology::Machine *        machine)
457 {
458     const hwloc_obj_t        root    = hwloc_get_root_obj(topo);
459     std::vector<hwloc_obj_t> pcidevs = getHwLocDescendantsByType(root, HWLOC_OBJ_PCI_DEVICE);
460
461     for (auto &p : pcidevs)
462     {
463         const hwloc_obj_t ancestor = hwloc_get_ancestor_obj_by_type(topo, HWLOC_OBJ_NUMANODE, p);
464         int               numaId;
465         if (ancestor != nullptr)
466         {
467             numaId = ancestor->logical_index;
468         }
469         else
470         {
471             // If we only have a single numa node we belong to it, otherwise set it to -1 (unknown)
472             numaId = (machine->numa.nodes.size() == 1) ?  0 : -1;
473         }
474
475         GMX_RELEASE_ASSERT(p->attr, "Attributes should not be NULL for hwloc PCI object");
476
477         machine->devices.push_back( {
478                                         p->attr->pcidev.vendor_id,
479                                         p->attr->pcidev.device_id,
480                                         p->attr->pcidev.class_id,
481                                         p->attr->pcidev.domain,
482                                         p->attr->pcidev.bus,
483                                         p->attr->pcidev.dev,
484                                         p->attr->pcidev.func,
485                                         numaId
486                                     } );
487     }
488     return pcidevs.empty();
489 }
490
491 void
492 parseHwLoc(HardwareTopology::Machine *        machine,
493            HardwareTopology::SupportLevel *   supportLevel,
494            bool *                             isThisSystem)
495 {
496     hwloc_topology_t    topo;
497
498     // Initialize a hwloc object, set flags to request IO device information too,
499     // try to load the topology, and get the root object. If either step fails,
500     // return that we do not have any support at all from hwloc.
501     if (hwloc_topology_init(&topo) != 0)
502     {
503         hwloc_topology_destroy(topo);
504         return; // SupportLevel::None.
505     }
506
507     hwloc_topology_set_flags(topo, HWLOC_TOPOLOGY_FLAG_IO_DEVICES);
508
509     if (hwloc_topology_load(topo) != 0 || hwloc_get_root_obj(topo) == nullptr)
510     {
511         hwloc_topology_destroy(topo);
512         return; // SupportLevel::None.
513     }
514
515     // If we get here, we can get a valid root object for the topology
516     *isThisSystem = hwloc_topology_is_thissystem(topo);
517
518     // Parse basic information about sockets, cores, and hardware threads
519     if (parseHwLocSocketsCoresThreads(topo, machine) == 0)
520     {
521         *supportLevel = HardwareTopology::SupportLevel::Basic;
522     }
523     else
524     {
525         hwloc_topology_destroy(topo);
526         return; // SupportLevel::None.
527     }
528
529     // Get information about cache and numa nodes
530     if (parseHwLocCache(topo, machine) == 0 && parseHwLocNuma(topo, machine) == 0)
531     {
532         *supportLevel = HardwareTopology::SupportLevel::Full;
533     }
534     else
535     {
536         hwloc_topology_destroy(topo);
537         return; // SupportLevel::Basic.
538     }
539
540     // PCI devices
541     if (parseHwLocDevices(topo, machine) == 0)
542     {
543         *supportLevel = HardwareTopology::SupportLevel::FullWithDevices;
544     }
545
546     hwloc_topology_destroy(topo);
547     return; // SupportLevel::Full or SupportLevel::FullWithDevices.
548 }
549
550 #endif
551
552 /*! \brief Try to detect the number of logical processors.
553  *
554  *  \return The number of hardware processing units, or 0 if it fails.
555  */
556 int
557 detectLogicalProcessorCount()
558 {
559     int count = 0;
560
561     {
562 #if GMX_NATIVE_WINDOWS
563         // Windows
564         SYSTEM_INFO sysinfo;
565         GetSystemInfo( &sysinfo );
566         count = sysinfo.dwNumberOfProcessors;
567 #elif defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)
568         // We are probably on Unix. Check if we have the argument to use before executing any calls
569         count = sysconf(_SC_NPROCESSORS_ONLN);
570 #else
571         count = 0; // Neither windows nor Unix.
572 #endif
573     }
574
575     return count;
576 }
577
578 }   // namespace anonymous
579
580 // static
581 HardwareTopology HardwareTopology::detect()
582 {
583     HardwareTopology result;
584
585 #if GMX_HWLOC
586     parseHwLoc(&result.machine_, &result.supportLevel_, &result.isThisSystem_);
587 #endif
588
589     // If something went wrong in hwloc (or if it was not present) we might
590     // have more information in cpuInfo
591     if (result.supportLevel_ < SupportLevel::Basic)
592     {
593         // There might be topology information in cpuInfo
594         parseCpuInfo(&result.machine_, &result.supportLevel_);
595     }
596     // If we did not manage to get anything from either hwloc or cpuInfo, find the cpu count at least
597     if (result.supportLevel_ == SupportLevel::None)
598     {
599         // No topology information; try to detect the number of logical processors at least
600         result.machine_.logicalProcessorCount = detectLogicalProcessorCount();
601         if (result.machine_.logicalProcessorCount > 0)
602         {
603             result.supportLevel_ = SupportLevel::LogicalProcessorCount;
604         }
605     }
606     return result;
607 }
608
609 HardwareTopology::Machine::Machine()
610 {
611     logicalProcessorCount   = 0;
612     numa.baseLatency        = 0.0;
613     numa.maxRelativeLatency = 0.0;
614 }
615
616
617 HardwareTopology::HardwareTopology()
618     : supportLevel_(SupportLevel::None),
619       machine_(),
620       isThisSystem_(true)
621 {
622 }
623
624 HardwareTopology::HardwareTopology(int logicalProcessorCount)
625     : supportLevel_(SupportLevel::None),
626       machine_(),
627       isThisSystem_(true)
628 {
629     if (logicalProcessorCount > 0)
630     {
631         machine_.logicalProcessorCount = logicalProcessorCount;
632         supportLevel_                  = SupportLevel::LogicalProcessorCount;
633     }
634 }
635
636 int HardwareTopology::numberOfCores() const
637 {
638     if (supportLevel() >= SupportLevel::Basic)
639     {
640         // We assume all sockets have the same number of cores as socket 0.
641         // Since topology information is present, we can assume there is at least one socket.
642         return machine().sockets.size() * machine().sockets[0].cores.size();
643     }
644     else if (supportLevel() >= SupportLevel::LogicalProcessorCount)
645     {
646         return machine().logicalProcessorCount;
647     }
648     else
649     {
650         return 0;
651     }
652 }
653
654 } // namespace gmx