source: sasmodels/sasmodels/kernelcl.py @ 8064d5e

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 8064d5e was 8064d5e, checked in by Paul Kienzle <pkienzle@…>, 13 months ago

Merge branch 'beta_approx' into py3

  • Property mode set to 100644
File size: 24.8 KB
Line 
1"""
2GPU driver for C kernels
3
4TODO: docs are out of date
5
6There should be a single GPU environment running on the system.  This
7environment is constructed on the first call to :func:`env`, and the
8same environment is returned on each call.
9
10After retrieving the environment, the next step is to create the kernel.
11This is done with a call to :meth:`GpuEnvironment.make_kernel`, which
12returns the type of data used by the kernel.
13
14Next a :class:`GpuData` object should be created with the correct kind
15of data.  This data object can be used by multiple kernels, for example,
16if the target model is a weighted sum of multiple kernels.  The data
17should include any extra evaluation points required to compute the proper
18data smearing.  This need not match the square grid for 2D data if there
19is an index saying which q points are active.
20
21Together the GpuData, the program, and a device form a :class:`GpuKernel`.
22This kernel is used during fitting, receiving new sets of parameters and
23evaluating them.  The output value is stored in an output buffer on the
24devices, where it can be combined with other structure factors and form
25factors and have instrumental resolution effects applied.
26
27In order to use OpenCL for your models, you will need OpenCL drivers for
28your machine.  These should be available from your graphics card vendor.
29Intel provides OpenCL drivers for CPUs as well as their integrated HD
30graphics chipsets.  AMD also provides drivers for Intel CPUs, but as of
31this writing the performance is lacking compared to the Intel drivers.
32NVidia combines drivers for CUDA and OpenCL in one package.  The result
33is a bit messy if you have multiple drivers installed.  You can see which
34drivers are available by starting python and running:
35
36    import pyopencl as cl
37    cl.create_some_context(interactive=True)
38
39Once you have done that, it will show the available drivers which you
40can select.  It will then tell you that you can use these drivers
41automatically by setting the SAS_OPENCL environment variable, which is
42PYOPENCL_CTX equivalent but not conflicting with other pyopnecl programs.
43
44Some graphics cards have multiple devices on the same card.  You cannot
45yet use both of them concurrently to evaluate models, but you can run
46the program twice using a different device for each session.
47
48OpenCL kernels are compiled when needed by the device driver.  Some
49drivers produce compiler output even when there is no error.  You
50can see the output by setting PYOPENCL_COMPILER_OUTPUT=1.  It should be
51harmless, albeit annoying.
52"""
53from __future__ import print_function
54
55import os
56import warnings
57import logging
58import time
59
60try:
61    from time import perf_counter as clock
62except ImportError: # CRUFT: python < 3.3
63    import sys
64    if sys.platform.count("darwin") > 0:
65        from time import time as clock
66    else:
67        from time import clock
68
69import numpy as np  # type: ignore
70
71# Attempt to setup opencl. This may fail if the pyopencl package is not
72# installed or if it is installed but there are no devices available.
73try:
74    import pyopencl as cl  # type: ignore
75    from pyopencl import mem_flags as mf
76    from pyopencl.characterize import get_fast_inaccurate_build_options
77    # Ask OpenCL for the default context so that we know that one exists
78    cl.create_some_context(interactive=False)
79    HAVE_OPENCL = True
80    OPENCL_ERROR = ""
81except Exception as exc:
82    HAVE_OPENCL = False
83    OPENCL_ERROR = str(exc)
84
85from . import generate
86from .generate import F32, F64
87from .kernel import KernelModel, Kernel
88
89# pylint: disable=unused-import
90try:
91    from typing import Tuple, Callable, Any
92    from .modelinfo import ModelInfo
93    from .details import CallDetails
94except ImportError:
95    pass
96# pylint: enable=unused-import
97
98# CRUFT: pyopencl < 2017.1  (as of June 2016 needs quotes around include path)
99def quote_path(v):
100    """
101    Quote the path if it is not already quoted.
102
103    If v starts with '-', then assume that it is a -I option or similar
104    and do not quote it.  This is fragile:  -Ipath with space needs to
105    be quoted.
106    """
107    return '"'+v+'"' if v and ' ' in v and not v[0] in "\"'-" else v
108
109def fix_pyopencl_include():
110    """
111    Monkey patch pyopencl to allow spaces in include file path.
112    """
113    import pyopencl as cl
114    if hasattr(cl, '_DEFAULT_INCLUDE_OPTIONS'):
115        cl._DEFAULT_INCLUDE_OPTIONS = [quote_path(v) for v in cl._DEFAULT_INCLUDE_OPTIONS]
116
117if HAVE_OPENCL:
118    fix_pyopencl_include()
119
120# The max loops number is limited by the amount of local memory available
121# on the device.  You don't want to make this value too big because it will
122# waste resources, nor too small because it may interfere with users trying
123# to do their polydispersity calculations.  A value of 1024 should be much
124# larger than necessary given that cost grows as npts^k where k is the number
125# of polydisperse parameters.
126MAX_LOOPS = 2048
127
128
129# Pragmas for enable OpenCL features.  Be sure to protect them so that they
130# still compile even if OpenCL is not present.
131_F16_PRAGMA = """\
132#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp16)
133#  pragma OPENCL EXTENSION cl_khr_fp16: enable
134#endif
135"""
136
137_F64_PRAGMA = """\
138#if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp64)
139#  pragma OPENCL EXTENSION cl_khr_fp64: enable
140#endif
141"""
142
143def use_opencl():
144    sas_opencl = os.environ.get("SAS_OPENCL", "OpenCL").lower()
145    return HAVE_OPENCL and sas_opencl != "none" and not sas_opencl.startswith("cuda")
146
147ENV = None
148def reset_environment():
149    """
150    Call to create a new OpenCL context, such as after a change to SAS_OPENCL.
151    """
152    global ENV
153    ENV = GpuEnvironment() if use_opencl() else None
154
155def environment():
156    # type: () -> "GpuEnvironment"
157    """
158    Returns a singleton :class:`GpuEnvironment`.
159
160    This provides an OpenCL context and one queue per device.
161    """
162    if ENV is None:
163        if not HAVE_OPENCL:
164            raise RuntimeError("OpenCL startup failed with ***"
165                               + OPENCL_ERROR + "***; using C compiler instead")
166        reset_environment()
167        if ENV is None:
168            raise RuntimeError("SAS_OPENCL=None in environment")
169    return ENV
170
171def has_type(device, dtype):
172    # type: (cl.Device, np.dtype) -> bool
173    """
174    Return true if device supports the requested precision.
175    """
176    if dtype == F32:
177        return True
178    elif dtype == F64:
179        return "cl_khr_fp64" in device.extensions
180    else:
181        # Not supporting F16 type since it isn't accurate enough
182        return False
183
184def get_warp(kernel, queue):
185    # type: (cl.Kernel, cl.CommandQueue) -> int
186    """
187    Return the size of an execution batch for *kernel* running on *queue*.
188    """
189    return kernel.get_work_group_info(
190        cl.kernel_work_group_info.PREFERRED_WORK_GROUP_SIZE_MULTIPLE,
191        queue.device)
192
193def compile_model(context, source, dtype, fast=False):
194    # type: (cl.Context, str, np.dtype, bool) -> cl.Program
195    """
196    Build a model to run on the gpu.
197
198    Returns the compiled program and its type.
199
200    Raises an error if the desired precision is not available.
201    """
202    dtype = np.dtype(dtype)
203    if not all(has_type(d, dtype) for d in context.devices):
204        raise RuntimeError("%s not supported for devices"%dtype)
205
206    source_list = [generate.convert_type(source, dtype)]
207
208    if dtype == generate.F16:
209        source_list.insert(0, _F16_PRAGMA)
210    elif dtype == generate.F64:
211        source_list.insert(0, _F64_PRAGMA)
212
213    # Note: USE_SINCOS makes the intel cpu slower under opencl
214    if context.devices[0].type == cl.device_type.GPU:
215        source_list.insert(0, "#define USE_SINCOS\n")
216    options = (get_fast_inaccurate_build_options(context.devices[0])
217               if fast else [])
218    source = "\n".join(source_list)
219    program = cl.Program(context, source).build(options=options)
220    #print("done with "+program)
221    return program
222
223
224# for now, this returns one device in the context
225# TODO: create a context that contains all devices on all platforms
226class GpuEnvironment(object):
227    """
228    GPU context, with possibly many devices, and one queue per device.
229
230    Because the environment can be reset during a live program (e.g., if the
231    user changes the active GPU device in the GUI), everything associated
232    with the device context must be cached in the environment and recreated
233    if the environment changes.  The *cache* attribute is a simple dictionary
234    which holds keys and references to objects, such as compiled kernels and
235    allocated buffers.  The running program should check in the cache for
236    long lived objects and create them if they are not there.  The program
237    should not hold onto cached objects, but instead only keep them active
238    for the duration of a function call.  When the environment is destroyed
239    then the *release* method for each active cache item is called before
240    the environment is freed.  This means that each cl buffer should be
241    in its own cache entry.
242    """
243    def __init__(self):
244        # type: () -> None
245        # find gpu context
246        context_list = _create_some_context()
247
248        # Find a context for F32 and for F64 (maybe the same one).
249        # F16 isn't good enough.
250        self.context = {}
251        for dtype in (F32, F64):
252            for context in context_list:
253                if has_type(context.devices[0], dtype):
254                    self.context[dtype] = context
255                    break
256            else:
257                self.context[dtype] = None
258
259        # Build a queue for each context
260        self.queue = {}
261        context = self.context[F32]
262        self.queue[F32] = cl.CommandQueue(context, context.devices[0])
263        if self.context[F64] == self.context[F32]:
264            self.queue[F64] = self.queue[F32]
265        else:
266            context = self.context[F64]
267            self.queue[F64] = cl.CommandQueue(context, context.devices[0])
268
269        # Byte boundary for data alignment
270        #self.data_boundary = max(context.devices[0].min_data_type_align_size
271        #                         for context in self.context.values())
272
273        # Cache for compiled programs, and for items in context
274        self.compiled = {}
275        self.cache = {}
276
277    def has_type(self, dtype):
278        # type: (np.dtype) -> bool
279        """
280        Return True if all devices support a given type.
281        """
282        return self.context.get(dtype, None) is not None
283
284    def compile_program(self, name, source, dtype, fast, timestamp):
285        # type: (str, str, np.dtype, bool, float) -> cl.Program
286        """
287        Compile the program for the device in the given context.
288        """
289        # Note: PyOpenCL caches based on md5 hash of source, options and device
290        # so we don't really need to cache things for ourselves.  I'll do so
291        # anyway just to save some data munging time.
292        tag = generate.tag_source(source)
293        key = "%s-%s-%s%s"%(name, dtype, tag, ("-fast" if fast else ""))
294        # Check timestamp on program
295        program, program_timestamp = self.compiled.get(key, (None, np.inf))
296        if program_timestamp < timestamp:
297            del self.compiled[key]
298        if key not in self.compiled:
299            context = self.context[dtype]
300            logging.info("building %s for OpenCL %s", key,
301                         context.devices[0].name.strip())
302            program = compile_model(self.context[dtype],
303                                    str(source), dtype, fast)
304            self.compiled[key] = (program, timestamp)
305        return program
306
307    def free_buffer(self, key):
308        if key in self.cache:
309            self.cache[key].release()
310            del self.cache[key]
311
312    def __del__(self):
313        for v in self.cache.values():
314            release = getattr(v, 'release', lambda: None)
315            release()
316        self.cache = {}
317
318_CURRENT_ID = 0
319def unique_id():
320    global _CURRENT_ID
321    _CURRENT_ID += 1
322    return _CURRENT_ID
323
324def _create_some_context():
325    # type: () -> cl.Context
326    """
327    Protected call to cl.create_some_context without interactivity.
328
329    Uses SAS_OPENCL or PYOPENCL_CTX if they are set in the environment,
330    otherwise scans for the most appropriate device using
331    :func:`_get_default_context`.  Ignore *SAS_OPENCL=OpenCL*, which
332    indicates that an OpenCL device should be used without specifying
333    which one (and not a CUDA device, or no GPU).
334    """
335    # Assume we do not get here if SAS_OPENCL is None or CUDA
336    sas_opencl = os.environ.get('SAS_OPENCL', 'opencl')
337    if sas_opencl.lower() != 'opencl':
338        # Setting PYOPENCL_CTX as a SAS_OPENCL to create cl context
339        os.environ["PYOPENCL_CTX"] = sas_opencl
340
341    if 'PYOPENCL_CTX' in os.environ:
342        try:
343            return [cl.create_some_context(interactive=False)]
344        except Exception as exc:
345            warnings.warn(str(exc))
346            warnings.warn("pyopencl.create_some_context() failed")
347            warnings.warn("the environment variable 'SAS_OPENCL' or 'PYOPENCL_CTX' might not be set correctly")
348
349    return _get_default_context()
350
351def _get_default_context():
352    # type: () -> List[cl.Context]
353    """
354    Get an OpenCL context, preferring GPU over CPU, and preferring Intel
355    drivers over AMD drivers.
356    """
357    # Note: on mobile devices there is automatic clock scaling if either the
358    # CPU or the GPU is underutilized; probably doesn't affect us, but we if
359    # it did, it would mean that putting a busy loop on the CPU while the GPU
360    # is running may increase throughput.
361    #
362    # Macbook pro, base install:
363    #     {'Apple': [Intel CPU, NVIDIA GPU]}
364    # Macbook pro, base install:
365    #     {'Apple': [Intel CPU, Intel GPU]}
366    # 2 x nvidia 295 with Intel and NVIDIA opencl drivers installed
367    #     {'Intel': [CPU], 'NVIDIA': [GPU, GPU, GPU, GPU]}
368    gpu, cpu = None, None
369    for platform in cl.get_platforms():
370        # AMD provides a much weaker CPU driver than Intel/Apple, so avoid it.
371        # If someone has bothered to install the AMD/NVIDIA drivers, prefer
372        # them over the integrated graphics driver that may have been supplied
373        # with the CPU chipset.
374        preferred_cpu = (platform.vendor.startswith('Intel')
375                         or platform.vendor.startswith('Apple'))
376        preferred_gpu = (platform.vendor.startswith('Advanced')
377                         or platform.vendor.startswith('NVIDIA'))
378        for device in platform.get_devices():
379            if device.type == cl.device_type.GPU:
380                # If the existing type is not GPU then it will be CUSTOM
381                # or ACCELERATOR so don't override it.
382                if gpu is None or (preferred_gpu and gpu.type == cl.device_type.GPU):
383                    gpu = device
384            elif device.type == cl.device_type.CPU:
385                if cpu is None or preferred_cpu:
386                    cpu = device
387            else:
388                # System has cl.device_type.ACCELERATOR or cl.device_type.CUSTOM
389                # Intel Phi for example registers as an accelerator
390                # Since the user installed a custom device on their system
391                # and went through the pain of sorting out OpenCL drivers for
392                # it, lets assume they really do want to use it as their
393                # primary compute device.
394                gpu = device
395
396    # order the devices by gpu then by cpu; when searching for an available
397    # device by data type they will be checked in this order, which means
398    # that if the gpu supports double then the cpu will never be used (though
399    # we may make it possible to explicitly request the cpu at some point).
400    devices = []
401    if gpu is not None:
402        devices.append(gpu)
403    if cpu is not None:
404        devices.append(cpu)
405    return [cl.Context([d]) for d in devices]
406
407
408class GpuModel(KernelModel):
409    """
410    GPU wrapper for a single model.
411
412    *source* and *model_info* are the model source and interface as returned
413    from :func:`generate.make_source` and :func:`generate.make_model_info`.
414
415    *dtype* is the desired model precision.  Any numpy dtype for single
416    or double precision floats will do, such as 'f', 'float32' or 'single'
417    for single and 'd', 'float64' or 'double' for double.  Double precision
418    is an optional extension which may not be available on all devices.
419    Half precision ('float16','half') may be available on some devices.
420    Fast precision ('fast') is a loose version of single precision, indicating
421    that the compiler is allowed to take shortcuts.
422    """
423    def __init__(self, source, model_info, dtype=generate.F32, fast=False):
424        # type: (Dict[str,str], ModelInfo, np.dtype, bool) -> None
425        self.info = model_info
426        self.source = source
427        self.dtype = dtype
428        self.fast = fast
429        self.timestamp = generate.ocl_timestamp(self.info)
430        self._cache_key = unique_id()
431
432    def __getstate__(self):
433        # type: () -> Tuple[ModelInfo, str, np.dtype, bool]
434        return self.info, self.source, self.dtype, self.fast
435
436    def __setstate__(self, state):
437        # type: (Tuple[ModelInfo, str, np.dtype, bool]) -> None
438        self.info, self.source, self.dtype, self.fast = state
439
440    def make_kernel(self, q_vectors):
441        # type: (List[np.ndarray]) -> "GpuKernel"
442        return GpuKernel(self, q_vectors)
443
444    @property
445    def Iq(self):
446        return self._fetch_kernel('Iq')
447
448    def fetch_kernel(self, name):
449        # type: (str) -> cl.Kernel
450        """
451        Fetch the kernel from the environment by name, compiling it if it
452        does not already exist.
453        """
454        gpu = environment()
455        key = self._cache_key
456        if key not in gpu.cache:
457            program = gpu.compile_program(
458                self.info.name,
459                self.source['opencl'],
460                self.dtype,
461                self.fast,
462                self.timestamp)
463            variants = ['Iq', 'Iqxy', 'Imagnetic']
464            names = [generate.kernel_name(self.info, k) for k in variants]
465            kernels = [getattr(program, k) for k in names]
466            data = dict((k, v) for k, v in zip(variants, kernels))
467            # keep a handle to program so GC doesn't collect
468            data['program'] = program
469            gpu.cache[key] = data
470        else:
471            data = gpu.cache[key]
472        return data[name]
473
474# TODO: check that we don't need a destructor for buffers which go out of scope
475class GpuInput(object):
476    """
477    Make q data available to the gpu.
478
479    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
480    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
481    to get the best performance on OpenCL, which may involve shifting and
482    stretching the array to better match the memory architecture.  Additional
483    points will be evaluated with *q=1e-3*.
484
485    *dtype* is the data type for the q vectors. The data type should be
486    set to match that of the kernel, which is an attribute of
487    :class:`GpuProgram`.  Note that not all kernels support double
488    precision, so even if the program was created for double precision,
489    the *GpuProgram.dtype* may be single precision.
490
491    Call :meth:`release` when complete.  Even if not called directly, the
492    buffer will be released when the data object is freed.
493    """
494    def __init__(self, q_vectors, dtype=generate.F32):
495        # type: (List[np.ndarray], np.dtype) -> None
496        # TODO: do we ever need double precision q?
497        self.nq = q_vectors[0].size
498        self.dtype = np.dtype(dtype)
499        self.is_2d = (len(q_vectors) == 2)
500        # TODO: stretch input based on get_warp()
501        # not doing it now since warp depends on kernel, which is not known
502        # at this point, so instead using 32, which is good on the set of
503        # architectures tested so far.
504        if self.is_2d:
505            width = ((self.nq+15)//16)*16
506            self.q = np.empty((width, 2), dtype=dtype)
507            self.q[:self.nq, 0] = q_vectors[0]
508            self.q[:self.nq, 1] = q_vectors[1]
509        else:
510            width = ((self.nq+31)//32)*32
511            self.q = np.empty(width, dtype=dtype)
512            self.q[:self.nq] = q_vectors[0]
513        self.global_size = [self.q.shape[0]]
514        self._cache_key = unique_id()
515
516    @property
517    def q_b(self):
518        """Lazy creation of q buffer so it can survive context reset"""
519        env = environment()
520        key = self._cache_key
521        if key not in env.cache:
522            context = env.context[self.dtype]
523            #print("creating inputs of size", self.global_size)
524            buffer = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
525                               hostbuf=self.q)
526            env.cache[key] = buffer
527        return env.cache[key]
528
529    def release(self):
530        # type: () -> None
531        """
532        Free the buffer associated with the q value
533        """
534        environment().free_buffer(id(self))
535
536    def __del__(self):
537        # type: () -> None
538        self.release()
539
540class GpuKernel(Kernel):
541    """
542    Callable SAS kernel.
543
544    *model* is the GpuModel object to call
545
546    The following attributes are defined:
547
548    *info* is the module information
549
550    *dtype* is the kernel precision
551
552    *dim* is '1d' or '2d'
553
554    *result* is a vector to contain the results of the call
555
556    The resulting call method takes the *pars*, a list of values for
557    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
558    vectors for the polydisperse parameters.  *cutoff* determines the
559    integration limits: any points with combined weight less than *cutoff*
560    will not be calculated.
561
562    Call :meth:`release` when done with the kernel instance.
563    """
564    def __init__(self, model, q_vectors):
565        # type: (cl.Kernel, np.dtype, ModelInfo, List[np.ndarray]) -> None
566        dtype = model.dtype
567        self.q_input = GpuInput(q_vectors, dtype)
568        self._model = model
569        # F16 isn't sufficient, so don't support it
570        self._as_dtype = np.float64 if dtype == generate.F64 else np.float32
571        self._cache_key = unique_id()
572
573        # attributes accessed from the outside
574        self.dim = '2d' if self.q_input.is_2d else '1d'
575        self.info = model.info
576        self.dtype = model.dtype
577
578        # holding place for the returned value
579        nout = 2 if self.info.have_Fq and self.dim == '1d' else 1
580        extra_q = 4  # total weight, form volume, shell volume and R_eff
581        self.result = np.empty(self.q_input.nq*nout+extra_q, dtype)
582
583    @property
584    def _result_b(self):
585        """Lazy creation of result buffer so it can survive context reset"""
586        env = environment()
587        key = self._cache_key
588        if key not in env.cache:
589            context = env.context[self.dtype]
590            width = ((self.result.size+31)//32)*32 * self.dtype.itemsize
591            buffer = cl.Buffer(context, mf.READ_WRITE, width)
592            env.cache[key] = buffer
593        return env.cache[key]
594
595    def _call_kernel(self, call_details, values, cutoff, magnetic, effective_radius_type):
596        # type: (CallDetails, np.ndarray, np.ndarray, float, bool) -> np.ndarray
597        env = environment()
598        queue = env.queue[self._model.dtype]
599        context = queue.context
600
601        # Arrange data transfer to/from card
602        q_b = self.q_input.q_b
603        result_b = self._result_b
604        details_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
605                              hostbuf=call_details.buffer)
606        values_b = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,
607                             hostbuf=values)
608
609        name = 'Iq' if self.dim == '1d' else 'Imagnetic' if magnetic else 'Iqxy'
610        kernel = self._model.fetch_kernel(name)
611        kernel_args = [
612            np.uint32(self.q_input.nq), None, None,
613            details_b, values_b, q_b, result_b,
614            self._as_dtype(cutoff),
615            np.uint32(effective_radius_type),
616        ]
617        #print("Calling OpenCL")
618        #call_details.show(values)
619        #Call kernel and retrieve results
620        wait_for = None
621        last_nap = clock()
622        step = 1000000//self.q_input.nq + 1
623        for start in range(0, call_details.num_eval, step):
624            stop = min(start + step, call_details.num_eval)
625            #print("queuing",start,stop)
626            kernel_args[1:3] = [np.int32(start), np.int32(stop)]
627            wait_for = [kernel(queue, self.q_input.global_size, None,
628                               *kernel_args, wait_for=wait_for)]
629            if stop < call_details.num_eval:
630                # Allow other processes to run
631                wait_for[0].wait()
632                current_time = clock()
633                if current_time - last_nap > 0.5:
634                    time.sleep(0.001)
635                    last_nap = current_time
636        cl.enqueue_copy(queue, self.result, result_b, wait_for=wait_for)
637        #print("result", self.result)
638
639        # Free buffers
640        for v in (details_b, values_b):
641            if v is not None:
642                v.release()
643
644    def release(self):
645        # type: () -> None
646        """
647        Release resources associated with the kernel.
648        """
649        environment().free_buffer(id(self))
650        self.q_input.release()
651
652    def __del__(self):
653        # type: () -> None
654        self.release()
Note: See TracBrowser for help on using the repository browser.